简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

缓存Redis 在项目总结整理

发表于 2018-04-05 | 分类于 Redis

数据类型的运用

string

  • 缓存数据
    1
    2
    3
    不管是简单和复杂的数据都可以直接转为string存储。
    商品信息,省市区信息,活动配置等一系列不常变化的冷数据缓存
    非常热门数据的缓存,游戏排行,后台每秒更新一次数据
  • 简单计数:活动参加人数
  • 定时过期:一个人一天只能进行一次签到
  • 分布式锁

list

  • 用户排队 push,pop
  • 有序消息 push,pop
  • 实现生产者和消费者模型,阻塞式访问 BRPOP 和 BLPOP 命令

set

  • 去重列表:活动参加人数
  • 标签:用户标签,商家标签
  • 交并补
    1
    2
    3
    4
    5
    春节活动一共有 abcde 5个任务,用户A已经完成a,b,用户B已经完成 c,d

    交集:用户A,用户B 都完成的任务
    并集:用户A,用户B 任一完成的任务
    差集:用户A还没有完成的任务
  • 获取随机元素:从礼品库 set 中随机获得一个礼品

hash

同一资源的不同属性,如:用户在一共获得了不同种类奖品数量。可以对单个奖品直接更新操作。

zset

  • 排行榜:商品收藏排行,点赞排行等
  • 根据分数获取 top 10
  • 查询某个用户的分数:查询 得分在90-100 之间的用户

有时候我们的得分并不是由某一项业务值决定的,可能是由两项业务值来排序的,比如先看用户的实际得分,在看用户等级,那么我们在设计score的时候可以用小数点之前的值表示得分,小数点之后的值表示等级,如果有其他特殊要求,还可以考虑得分加上某个极大值来处理。

注意事项

  1. 每个 key 都应该有合理的失效时间

  2. string的过期时间在重新设值后会被覆盖

  3. string类型的 set 操作可以覆盖类型

  4. 合理使用相应的数据结构

  5. 不要用list存大量数据并检索

  6. 合理规划 key 的数量:判断用户有没有参加应该用set,不应该每个用户一个key

  7. 业务数据隔离 用户 redis 业务 redis 活动 redis 应该做区分,活动的 redis 在活动结束后可以自由清理

  8. 合理使用管道,lua 脚本和 redis 事务,提高性能,尤其是在脚本中使用 redis 的时候

  9. 在有大量 key 的 Reids 线上系统,要在主库禁用 keys * 操作,防止卡死

Redis-使用Jedis操作

发表于 2018-04-05 | 分类于 Redis

Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通用对象池工具common-pool作为资源的管理工具,下面是使用JedisPool操作Redis的代码示例:

1.Jedis连接池(通常JedisPool是单例的)

GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

2.获取Jedis对象不再是直接生成一个Jedis对象,而是直接从连接池里获取

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Jedis jedis = null;
try {
// 1. 从连接池获取 jedis 对象
jedis = jedisPool.getResource();
// 2. 执行操作
jedis.get("hello");
} catch (Exception e) {
} finally {
if (jedis != null) {
// 如果使用 JedisPool , close 操作不是关闭连接,代表归还连接池
jedis.close();


}
}

这里可以看到在finally中依然是jedis.close()操作,为什么会把连接关闭呢,这不和连接池的原则违背了吗?但实际上Jedis的close()实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void close() {
// 使用 Jedis 连接池
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
// 直连
} else {
client.close();
}
}

参数说
dataSource!=null代表使用的是连接池,所以jedis.close()代表归还 连接给连接池,而且Jedis会判断当前连接是否已经断开。
dataSource=null代表直连,jedis.close()代表关闭连接。

前面GenericObjectPoolConfig使用的是默认配置,实际它提供有很多参数,例如池子中最大连接数、最大空闲连接数、最小空闲连接数、连接活性检测,等等,例如下面代码:
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 设置最大连接数为默认值的 5 倍
poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5);
// 设置最大空闲连接数为默认值的 3 倍
poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
// 设置最小空闲连接数为默认值的 2 倍
poolConfig.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE * 2);
// 设置开启 jmx 功能
poolConfig.setJmxEnabled(true);
// 设置连接池没有连接后客户端的最大等待时间 ( 单位为毫秒 )
poolConfig.setMaxWaitMillis(3000);

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Jedis getRedisConnect() {
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 设置最大连接数为默认值的 5 倍
poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5);
// 设置最大空闲连接数为默认值的 3 倍
poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
// 设置最小空闲连接数为默认值的 2 倍
poolConfig.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE * 2);
// 设置开启 jmx 功能
poolConfig.setJmxEnabled(true);
// 设置连接池没有连接后客户端的最大等待时间 ( 单位为毫秒 )
poolConfig.setMaxWaitMillis(3000);
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
Jedis jedis = jedisPool.getResource();
return jedis;
}

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
maven项目
pom.xml
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
</dependencies>

Java demo

1
2
3
4
5
6
7
8
9
10
11
public class Demo01 {
@Test
public void testRedis() throws Exception {
Jedis jedis = new Jedis("192.168.18.140",6379);
List<String> list = jedis.mget("user");
for (String string : list) {
System.out.println(string);
}
jedis.close();
}
}

连接不到Redis,是因为在redis3.2之后,redis增加了protected-mode,在这个模式下,即使注释掉了bind 127.0.0.1,再访问redisd时候还是报错。

#bind 127.0.0.1
protected-mode 修改为no
protected-mode no

测试没问题
zhangsanfeng
100

相关操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// 连接redis ,redis的默认端口是6379
Jedis jedis = new Jedis("ip", 6379);

// 验证密码,如果没有设置密码这段代码省略
jedis.auth("password");jedis.connect();// 连接
jedis.disconnect();// 断开连接

Set<String> keys = jedis.keys("*"); // 列出所有的key
Set<String> keys = jedis.keys("key"); // 查找特定的key

// 移除给定的一个或多个key,如果key不存在,则忽略该命令.
jedis.del("key1");jedis.del("key1","key2","key3","key4","key5");

// 移除给定key的生存时间(设置这个key永不过期)
jedis.persist("key1");

//检查给定key是否存在
jedis.exists("key1");

// 将key改名为newkey,当key和newkey相同或者key不存在时,返回一个错误
jedis.rename("key1","key2");

// 返回key所储存的值的类型。
// none(key不存在),string(字符串),list(列表),set(集合),zset(有序集),hash(哈希表)
jedis.type("key1");

// 设置key生存时间,当key过期时,它会被自动删除。
jedis.expire("key1",5);// 5秒过期


// 字符串值value关联到key。
jedis.set("key1","value1");


// 将值value关联到key,并将key的生存时间设为seconds(秒)。
jedis.setex("foo",5,"haha");


// 清空所有的key
jedis.flushAll();


// 返回key的个数
jedis.dbSize();


// 哈希表key中的域field的值设为value。
jedis.hset("key1","field1","field1-value");jedis.hset("key1","field2","field2-value");


Map map = new HashMap();map.put("field1","field1-value");map.put("field2","field2-value");jedis.hmset("key1",map);


// 返回哈希表key中给定域field的值
jedis.hget("key1","field1");


// 返回哈希表key中给定域field的值(多个)
List list = jedis.hmget("key1","field1","field2");for(
int i = 0;i<list.size();i++)
{
System.out.println(list.get(i));
}


// 返回哈希表key中所有域和值
Map<String, String> map = jedis.hgetAll("key1");for(
Map.Entry entry:map.entrySet())
{
System.out.print(entry.getKey() + ":" + entry.getValue() + "\t");
}


// 删除哈希表key中的一个或多个指定域
jedis.hdel("key1","field1");jedis.hdel("key1","field1","field2");


// 查看哈希表key中,给定域field是否存在。
jedis.hexists("key1","field1");


// 返回哈希表key中的所有域
jedis.hkeys("key1");


// 返回哈希表key中的所有值
jedis.hvals("key1");


// 将值value插入到列表key的表头。
jedis.lpush("key1","value1-0");jedis.lpush("key1","value1-1");jedis.lpush("key1","value1-2");


// 返回列表key中指定区间内的元素,区间以偏移量start和stop指定.
// 下标(index)参数start和stop从0开始;
// 负数下标代表从后开始(-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推)
List list = jedis.lrange("key1", 0, -1);// stop下标也在取值范围内(闭区间)
for(
int i = 0;i<list.size();i++)
{
System.out.println(list.get(i));
}


// 返回列表key的长度。
jedis.llen("key1")


// 将member元素加入到集合key当中。
jedis.sadd("key1","value0");jedis.sadd("key1","value1");


// 移除集合中的member元素。
jedis.srem("key1","value1");


// 返回集合key中的所有成员。
Set set = jedis.smembers("key1");


// 判断元素是否是集合key的成员
jedis.sismember("key1","value2");


// 返回集合key的元素的数量
jedis.scard("key1");


// 返回一个集合的全部成员,该集合是所有给定集合的交集
jedis.sinter("key1","key2");


// 返回一个集合的全部成员,该集合是所有给定集合的并集
jedis.sunion("key1","key2");


// 返回一个集合的全部成员,该集合是所有给定集合的差集
jedis.sdiff("key1","key2");

mybaits延迟加载和Interceptor

发表于 2018-04-05 | 分类于 mybatis

延迟加载

MyBatis中的延迟加载,也称为懒加载,是指在进行关联查询时,按照设置延迟规则推迟对关联对象的select查询。延迟加载可以有效的减少数据库压力。

直接加载

执行完对主加载对象的select语句,马上执行对关联对象的select查询。

1
2
3
4
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="false"/>
</settings>

侵入式延迟

执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的详情时,就会马上执行关联对象的select查询。即对关联对象的查询执行,侵入到了主加载对象的详情访问中。也可以这样理解:将关联对象的详情侵入到了主加载对象的详情中,即将关联对象的详情作为主加载对象的详情的一部分出现了。

1
2
3
4
5
6
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式延迟加载开关 -->
<setting name="aggressiveLazyLoading" value="true"/>
</settings>

深度延迟

执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象的详情时也不会执行关联对象的select查询。只有当真正访问关联对象的详情时,才会执行对关联对象的select查询。

1
2
3
4
5
6
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式延迟加载开关 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

Interceptor(转)

Mybatis核心对象

  • Configuration 初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如,插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中
  • SqlSessionFactory SqlSession工厂
  • SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
  • Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  • ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
  • ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  • TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
  • MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装,
  • SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
  • BoundSql 表示动态生成的SQL语句以及相应的参数信息

MyBatis 拦截器原理实现

Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理。

20200410094537

https://blog.csdn.net/weixin_39494923/article/details/91534658

mybatis简单环境搭建例子

发表于 2018-04-05 | 分类于 mybatis

MyBatis 简介

Mybatis 开源免费框架,原名叫iBatis,2010在google code,2013年迁移到 github是数据访问层框架,底层是对 JDBC 的封装。不需要编写实现类,只需要写需要执行的 sql 命令。

导入包

20200401092549

在 src 下新建全局配置文件(编写 JDBC连接信息)

在全局配置文件中引入 DTD 或 schema 如果导入 dtd 后没有提示Window–> preference –> XML –> XMl catalog –> add 按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 默认当前所使用的环境 -->
<environments default="default">
<!-- 声明可以使用的环境 -->
<environment id="default">
<!-- 使用原生态JDBC事务 -->
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/ssm"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/hu/mapper/UserMapper.xml"/>
</mappers>
</configuration>

type 属性可取值JDBC事务管理使用 JDBC 原生事务管理方式MANAGED 把事务管理转交给其他容器,相当于把JDBC 事务setAutoMapping(false);

type 属性 POOLED 使用数据库连接池 UNPOOLED 不实用数据库连接池,和直接使用 JDBC 一样

新建以 mapper 结尾的包,在包下新建:实体类名+Mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 理解成实现类的全路径(包名+类名) -->
<mapper namespace="com.hu.mapper.UserMapper">
<!--
id:方法名
parameterType:定义参数类型
resultType:返回值类型

如果方法返回值是List,在resultType中写List的泛型,因为MyBatis是对JDBC封装,一行一行读取数据。
-->
<select id="selectAll" resultType="com.hu.pojo.User">
select * from user
</select>
<select id="selectById" resultType="int">
select count(*) from user
</select>
<select id="selectAllMap" resultType="com.hu.pojo.User">
select * from user
</select>
</mapper>

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo1 {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
//使用了工厂设计模式 实例化工厂对象用了构建者设计模式。
//构建者设计模式的意义:简化对象实例化过程。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();//生产SqlSession
//获得list
List<User> users = session.selectList("com.hu.mapper.UserMapper.selectAll");
for (User user : users) {
System.out.println(user.toString());
}
//获得单个值
int count = session.selectOne("com.hu.mapper.UserMapper.selectById");
System.out.println(count);
//获得Map
Map<Long, User> map = session.selectMap("com.hu.mapper.UserMapper.selectAllMap", "id");
System.out.println(map);
session.close();
}
}
1
2
3
4
5
1  李宁   44
2 谢晓峰 30
3 张三丰 100
3
{1=1 李宁 44, 2=2 谢晓峰 30, 3=3 张三丰 100}

三种查询方式

1
2
3
1.selectList() 返回值为 List<resultType 属性控制>,适用于查询结果都需要遍历的需求
2.selectOne() 返回值 Object,适用于返回结果只是变量或一行数据时
3.selectMap() 返回值 Map,适用于需要在查询结果中通过某列的值取到这行数据的需求

springboot整合web

发表于 2018-03-14 | 分类于 spring boot

整合Servlet、Filter、Listener

servlet

1
2
3
4
5
6
7
8
//@WebServlet(name = "HelloServlet",urlPatterns = "/helloservlet")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
System.out.println("HelloServlet");
}
}

启动器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class AppServlet {
public static void main(String[] args) {

SpringApplication.run(AppServlet.class, args);
}
//Servlet 组件的注册,不用再在Servlet上添加注解
@Bean
public ServletRegistrationBean getServletRegistrationBean(){
ServletRegistrationBean bean = new ServletRegistrationBean(new HelloServlet());
bean.addUrlMappings("/helloservlet");
return bean;
}
}

运行可以直接在浏览器访问

Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//@WebFilter
public class HelloFilter implements Filter {
@Override
public void destroy() {
}

@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException {
System.out.println("进入 Filter");
arg2.doFilter(arg0, arg1);
System.out.println("离开 Filter");
}

@Override
public void init(FilterConfig arg0) throws ServletException {
}
}

@SpringBootApplication
public class AppFilter {
public static void main(String[] args) {

SpringApplication.run(AppFilter.class, args);
}

//Servlet 组件的注册,不用再在Servlet上添加注解
@Bean
public ServletRegistrationBean getServletRegistrationBean(){
ServletRegistrationBean bean = new ServletRegistrationBean(new HelloServlet());
bean.addUrlMappings("/helloservlet");//bean.addInitParameter("paramName", "paramValue");
return bean;
}
/**
* 注册 Filter
*/
@Bean
public FilterRegistrationBean getFilterRegistrationBean() {
FilterRegistrationBean bean = new FilterRegistrationBean(new HelloFilter());
//bean.addUrlPatterns(new String[]{"*.do","*.jsp"});
bean.addUrlPatterns("/helloservlet");
return bean;
}
}

Listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//@WebListener
public class HelloListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent arg0) {
}

@Override
public void contextInitialized(ServletContextEvent arg0) {
System.out.println("HelloListener..init.....");
}
}

/**
* 注册 listener
*/
@Bean
public ServletListenerRegistrationBean<HelloListener> getServletListenerRegistrationBean(){
ServletListenerRegistrationBean<HelloListener> bean= new ServletListenerRegistrationBean<HelloListener>(new HelloListener());
return bean;
}

运行后直接在控制台显示HelloListener..init…..

访问静态资源和上传

注意目录名称必须是 static,在 src/main/webapp 目录名称必须要 webapp

html放在static中

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<form action="fileUploadController" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="filename"/><br/>
<input type="submit"/>
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController //表示该类下的方法的返回值会自动做 json 格式的转换
public class FileUploadController {
/*
* 处理文件上传
*/
@RequestMapping("/fileUploadController")
public Map<String, Object> fileUpload(MultipartFile filename)throws Exception{
System.out.println(filename.getOriginalFilename());
filename.transferTo(new File("C:/"+filename.getOriginalFilename()));
Map<String, Object> map = new HashMap<>();
map.put("msg", "ok");
return map;
}
}

@SpringBootApplication
public class App {
public static void main(String[] args) {

SpringApplication.run(App.class, args);
}
}

启动访问index.html

上传成功

数据访问

@RestController
默认类中的方法都会以json的格式返回。

自定义Property,配置在application.properties中。

1
2
com.hu.title=QQQ
com.hu.description=QQQa

自定义配置类@Component

1
2
3
4
5
6
7
public class NeoProperties {
@Value("${com.hu.title}")
private String title;
@Value("${com.hu.description}")
private String description;
//省略getter settet方法
}

整合视图层

jsp

在maven项目main文件夹下创建webapp/WEB-INF/jsp,添加user.jsp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<table border="1" align="center" width="50%">
<tr>
<th>ID</th>
<th>Name</th>
<th>Age</th>
</tr>
<c:forEach items="${list }" var="user">
<tr>
<td>${user.id }</td>
<td>${user.name }</td>
<td>${user.age }</td>
</tr>
</c:forEach>
</table>
</body>
</html>

IDEA中设置

20200420155905

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class UserController {
//此处模拟由数据库得到的数据
static List<User> list;
static{
list = new ArrayList<>();
list.add(new User(1, "江小白", 20));
list.add(new User(2, "张三丰", 22));
list.add(new User(3, "王八蛋", 24));
}
@RequestMapping("show")
public String showUser(Model model) {
model.addAttribute("list", list);
//跳转视图
return "user";
}
}

application.properties配置文件

1
2
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

POM.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 需要继承父项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-view</artifactId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 注入 SpringBoot 启动坐标 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 对jsp的支持的依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!-- jstl标签库 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
</dependencies>
</project>

启动器:

1
2
3
4
5
6
7
8
9
// 启动器存放的位置。启动器可以和 controller 位于同一个包下,或者位于 controller 的上一级包中;
// 如放到 controller 的平级或及子包以下不能访问到,除非加上@ComponentScan("com.hu.controller")。
// @ComponentScan("com.hu.controller")
@SpringBootApplication
public class ViewApp {
public static void main(String[] args) {
SpringApplication.run(ViewApp.class, args);
}
}

注意:There was an unexpected error (type=Not Found, status=404).

/WEB-INF/jsp/user.jsp问题:

解决方式:

20200420155827

freemarker

application.properties配置,这里基本是默认配置,可以不写也可以运行成功。

1
2
3
4
5
6
7
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.charset=utf-8
spring.freemarker.cache=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
spring.freemarker.suffix=.ftl

加入依赖:

1
2
3
4
5
<!-- freemarker 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

创建文件:src\main\resources\templates\userhtml.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<head>
<title>展示用户数据</title>
<meta charset="utf-9"></meta>
</head>
<body>
<table border="1" align="center" width="50%">
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
</tr>
<#list list as user >
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.age}</td>
</tr>
</#list>
</table>
</body>
</html>

控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class UserController {
//此处模拟由数据库得到的数据
static List<User> list;
static{
list = new ArrayList<>();
list.add(new User(1, "江小白", 20));
list.add(new User(2, "张三丰", 22));
list.add(new User(3, "王八蛋", 24));
}
@RequestMapping("userhtml")
public String showUserToHTML(Model model) {
model.addAttribute("list", list);
//跳转视图
return "userhtml";
}
}

Thymeleaf

主要代码如下:

1
2
3
4
5
<!-- thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

模板:templates/userthy.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<head>
<title>Thymeleaf Demo</title>
<meta charset="utf-9"></meta>
</head>
<body>
<table border="1" align="center" width="50%">
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
</tr>
<tr th:each="user : ${list}">
<td th:text="${user.id}"</td>
<td th:text="${user.name}"</td>
<td th:text="${user.age}"</td>
</tr>
</table>
</body>
</html>

控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class UserController {
//此处模拟由数据库得到的数据
static List<User> list;
static{
list = new ArrayList<>();
list.add(new User(1, "江小白", 20));
list.add(new User(2, "张三丰", 22));
list.add(new User(3, "王八蛋", 24));
}
@RequestMapping("userthy")
public String showUserToThy(Model model) {
model.addAttribute("list", list);
//跳转视图
return "userthy";
}
}

Shiro与springmvc spring mybatis简单整合

发表于 2018-02-23 | 分类于 shiro

web.xml中配置shiro的filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- shiro过虑器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置true由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean-->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

在spring中配置shiro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- web.xml中shiro的filter对应的bean -->
<!-- Shiro 的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
<property name="loginUrl" value="/login.do" />
<!-- 认证成功统一跳转到index.do,建议不配置,shiro认证成功自动到上一个请求路径 -->
<property name="successUrl" value="/index.do"/>
<!-- 通过unauthorizedUrl指定没有权限操作时跳转页面-->
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
<property name="filterChainDefinitions">
<value>
<!-- /** = authc 所有url都必须认证通过才可以访问-->
/login.jsp=anon
/** = authc
<!-- /** = anon所有url都可以匿名访问 -->


</value>
</property>
</bean>


<!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>
<!-- realm -->
<bean id="userRealm" class="cn.siggy.realm.UserRealm">
<!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="1" />
</bean>

登录

原理

Shiro 内置了很多默认的过滤器,比如身份验证、授权等相关的。默认过滤器可以参考 org.apache.shiro.web.filter.mgt.DefaultFilter中的过滤器:

过滤器简称 对应的java类
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数

使用FormAuthenticationFilter过虑器实现 ,原理如下:

将用户没有认证时,请求loginurl进行认证,用户身份和用户密码提交数据到loginurl
FormAuthenticationFilter拦截住取出request中的username和password(两个参数名称是可以配置的)
FormAuthenticationFilter调用realm传入一个token(username和password)
realm认证时根据username查询用户信息(在Activeuser中存储,包括 userid、usercode、username、menus)。
如果查询不到,realm返回null,FormAuthenticationFilter向request域中填充一个参数(记录了异常信息)

登陆页面

由于FormAuthenticationFilter的用户身份和密码的input的默认值(username和password),修改页面的账号和密码 的input的名称为username和password

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Controller
public class LoginController {

@RequestMapping("/login.do")
public String login(HttpServletRequest req,Model model){
String exceptionClassName = (String)req.getAttribute("shiroLoginFailure");
String error = null;
if(UnknownAccountException.class.getName().equals(exceptionClassName)) {
error = "用户名/密码错误";
} else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName))
{
error = "用户名/密码错误";
} else if(exceptionClassName != null) {
error = "其他错误:" + exceptionClassName;
}
model.addAttribute("error", error);
return "redirect:login.jsp";
}
}

shiro介绍和使用

发表于 2018-02-21 | 分类于 shiro

框架介绍

1、shiro框架及其基本功能

Apache Shiro 是Java 的一个安全框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
Shiro 的 API 也是非常简单;其基本功能点如下图所示:

20200421181655

Authentication :身份认证/登录,验证用户是不是拥有相应的身份;
Authorization :授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager :会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography :加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support :Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency :shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing :提供测试支持;
Run As :允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me :记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

注:Shiro 不会去维护用户、维护权限;这 些需要我们 自己去 设计/ 提供 ; 然后通过相应的 接口注入给 给 Shiro 即可。

2、shiro架构原理

20200421181738

应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个 API 的含义:

Subject :主体,代表了当前"用户",这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;SecurityManager :安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
Realm: 域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个 Shiro 应用:

1、 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
2、 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

注:从以上也可以看出 ,Shiro 不提供维护用户/ 权限, 而是通过 Realm 让开发人员自己注入。

20200421181854

Subject :主体,可以看到主体可以是任何可以与应用交互的"用户";
SecurityManager :  相 当 于 SpringMVC 中 的 DispatcherServlet 或 者 Struts2 中 的FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator :认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer :授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm :可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
SessionManager :如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO中可以使用 Cache 进行缓存,以提高性能。
CacheManager :缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
Cryptography :密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。

一个简单的示例

maven配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hu</groupId>
<artifactId>shiro_01</artifactId>
<version>0.0.1-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
</project>

在src下创建shiro的ini配置文件/shiro_01/src/main/resources/shiro.ini:

1
2
[users]
zhang=123

登录测试java代码,这里使用junit测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class LoginLogoutTest {
@Test
public void testHelloworld() {
// 1、获取 SecurityManager 工厂,此处使用ini配置文件初始化 SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 2、获得SecurityManager 实例 并绑定给 SecurityUtils,全局设置,设置一次即可
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 3、得到 Subject,并且通过用户名/密码创建 身份验证 Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
//封装token信息
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
// 4、登录,即身份验证
/**
* 用户认证:
* Subject接收token,通过其实现类DelegatingSubject将token委托给SecurityManager来完成认证。
* 默认情况下:SecurityManager接口是通过DefaultSecurityManager类来完成相关的功能,并且由DefaultSecurityManager中的login方法来完成认证过程。
* 在login中调用了该类的authenticate()方法来完成认证,而该方法由AuthenticatingSecurityManager来完成的。
* 在该类的authenticate()方法中,通过调用认证器authenticator来完成工作。
* 而认证器authenticator又是通过ModularRealmAuthenticator类来完成认证;
* 通过ModularRealmAuthenticator中的doAuthenticate方法来获取Realm信息。
* 如果是单个realm直接将token和realm中的数据进行比较判断是否认证成功;如果是多个realm则通过Authentication Strategy来完成对应的操作。
* subject.isAuthenticated()判断是否认证成功。
*/
subject.login(token);
if (subject.isAuthenticated()) {
System.out.println("登录成功");
}else {
System.out.println("用户名或密码错误!");
}
} catch (AuthenticationException e) {
// 5、身份验证失败
System.out.println("登录失败");
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated()); // 断言用户已经登录
// 6、退出
subject.logout();
}
}

运行结果输出登录成功

注:如果身份验证失败请捕获 AuthenticationException 或其子类

常见的如 :DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的
凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如”用户名/密码错误”而不是”用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;

最后可以调用 subject.logout 退出,其会自动委托给 SecurityManager.logout 方法退出。

1、收集用户身份/凭证,即如用户名/密码;
2、调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功;
3、最后调用 Subject.logout 进行退出操作。

认证Authentication

在应用中谁能证明他就是他本人。一般提供如他们的身份ID 一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明。

在 shiro 中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。

最常见的 principals 和 credentials 组合就是用户名/密码了。接下来先进行一个基本的身份认证。

另外两个相关的概念是之前提到的 Subject 及 Realm,分别是主体及验证主体的数据源。

认证流程

20200421182228

1、首先调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager()设置;
2、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

Realm

域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,安全数据源。前面的例子使用的是ini配置org.apache.shiro.realm.text.IniRealm。

Realm接口:

20200421182328

String getName(); //返回一个唯一的 Realm 名字
boolean supports(AuthenticationToken token); //判断此 Realm 是否支持此 Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; //根据 Token 获取认证信息

20200421182355

最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,AuthorizingRealm负责授权,通常自定义的realm继承AuthorizingRealm。

单个Realm配置

类可以通过实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyRealm implements Realm {

@Override
public String getName() {
return "my Realm ";
}

@Override
public boolean supports(AuthenticationToken token) {
// 仅支持 UsernamePasswordToken 类型的 Token
return token instanceof UsernamePasswordToken;
}

@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); // 得到用户名
String password = new String((char[]) token.getCredentials()); // 得到密码
System.out.println(getName()+" "+username+" "+password);
if (!"zhang".equals(username)) {
throw new UnknownAccountException(); // 如果用户名错误
}
if (!"123".equals(password)) {
throw new IncorrectCredentialsException(); // 如果密码错误
}
// 返回认证信息:如果身份认证验证成功,返回一个 AuthenticationInfo 实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}

或者继承来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "my Realm 1";
}

/*
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}

/*
* 用于认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从token中获取信息
String username = (String) token.getPrincipal(); // 得到用户名
// 根据用户名到数据库中取出用户信息 如果查询不到 返回null
String password = "123";// 假如从数据库中获取密码为123
System.out.println(getName()+" "+username+" "+password);
// 返回认证信息:如果身份认证验证成功,返回一个 AuthenticationInfo 实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}

ini配置:

[users]
hu=123

[main]

#引入 自定义 realm
userRealm=com.hu.test.MyRealm
#将realm设置到securityManager
securityManager.realms=$userRealm

多个Realm配置

ini配置:
#声明一个 realm
myRealm1=com.hu.realm.MyRealm1
myRealm2=com.hu.realm.MyRealm2
#指定 securityManager 的 realms 实现
securityManager.realms=$myRealm1,$myRealm2

securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,如果删除”securityManager.realms=$myRealm1,$myRealm2”,那么 securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现 )。
当我们显示指定realm后,其他没有指定realm将被忽略,”securityManager.realms=$myRealm1”,那么 myRealm2 不会被自动设置进去。

Shiro默认提供的的Realm

20200421182617

一般使用继承 AuthorizingRealm(授权),其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)。其中主要默认实现如下:

org.apache.shiro.realm.text.IniRealm:[users]部分指定用户名/密码及其角色;[roles]部分指定角色即权限信息;

org.apache.shiro.realm.text.PropertiesRealm: user.username=password,role1,role2 指定用户名/密码及其角色;role.role1=permission1,permission2 指定角色及权限信息;

org.apache.shiro.realm.jdbc.JdbcRealm: 通过 sql 查询相应的信息,如:
select password fromusers where username = ? 获取用户密码
select password, password_salt from users whereusername = ? 获取用户密码
select role_name from user_roles where username = ? 获取用户角色;
select permission from roles_permissions where role_name = ? 获取角色对应的权限信息,也可以调用相应的 api 进行自定义 sql;

JDBC Realm 使用

数据库及依赖: mysql 数据库及 druid 连接池

1
2
3
4
5
6
7
8
9
10
11
<!-- 数据库依赖  使用 mysql 数据库及 druid 连接池 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>0.2.23</version>
</dependency>

ini配置

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/maven_shiro
dataSource.username=root
dataSource.password=123456
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm

变量名=全限定类名 会自动创建一个类实例

变量名.属性=值 自动调用相应的 setter 方法进行赋值

$变量名 引用之前的一个对象实例

Junit测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoginLogoutTest2 {
@Test
public void testHelloworld() {
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-jdbc-realm.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
try {
subject.login(token);
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated());
subject.logout();
}
}

结果:信息: {dataSource-1} inited 登录成功

Authenticator 及 AuthenticationStrategy

Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点。

public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;
如果验证成功,将返回 AuthenticationInfo 验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 实现。

SecurityManager 接口继承了 Authenticator,另外还有一个 ModularRealmAuthenticator 实现,其委托给多个 Realm 进行验证,验证规则通过 AuthenticationStrategy 接口指定,默认提供的实现,三种认证策略:

FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy不同,返回所有 Realm 身份验证成功的认证信息;
AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

ModularRealmAuthenticator 默认使用 AtLeastOneSuccessfulStrategy 策略。

ini 配置文件:

#配置验证器  指定 securityManager 的 authenticator 实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#配置验证策略  指定 securityManager.authenticator 的 authenticationStrategy
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

假如有三个realm:
realm1,realm2,realm3

realm1=com.hu.realm.Realm1
realm2=com.hu.realm.Realm2
realm3=com.hu.realm.Realm3
securityManager.realms=$realm1,$realm3

测试成功

自定义Realm

散列算法

一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt,比如加密密码admin产生的散列值是21232f297a57a5a743894a0e4a801fc3,可以到一些md5 解密网站很容易的通过散列值得到密码admin,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是”密码+用户名+ID”,这样生成的散列值相对来说更难破解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShiroMD5 {
//shiro提供了现成的加密类 Md5Hash
@Test
public void testMd5() {
// MD5加密
String password = new Md5Hash("123").toString();
System.out.println("直接加密:" + password);
// 加盐 salt 默认一次散列
String password_salt = new Md5Hash("123", "hu").toString();
System.out.println("加盐salt后散列:" + password_salt);
// 散列2次
String password_salt_2 = new Md5Hash("123", "hu", 2).toString();
System.out.println("散列2次:" + password_salt_2);
// 使用SimpleHash
SimpleHash hash = new SimpleHash("MD5", "123", "hu", 2);
System.out.println("simpleHash:" + hash.toString());
}
}

自定义散列

ini配置:
[users]
hu=123

[main]
#定义凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次数
credentialsMatcher.hashIterations=2

#引入 自定义 realm
userRealm=com.hu.test.MyRealmMD5
userRealm.credentialsMatcher=$credentialsMatcher
#将realm设置到securityManager
securityManager.realms=$userRealm

自定义Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyRealmMD5 extends AuthorizingRealm {
@Override
public String getName() {
return "my Realm MD5";
}
/*
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/*
* 用于认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println(getName());
// 从token中获取信息
String username = (String) token.getPrincipal(); // 得到用户名
// 根据用户名到数据库中取出用户信息 如果查询不到 返回null
// 按照固定规则加密码结果 ,此密码 要在数据库存储,原始密码 是123,盐是hu 2次散列
String password = "5f97546d9e0f0d775b9175286a484c09";// 假如从数据库中获取密码加密
// 返回认证信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes("hu"), getName());
return simpleAuthenticationInfo;


}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testHelloworld() {
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiromd5.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated());
subject.logout();
}

结果:

SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
my Realm MD5
登录成功

授权Authorization

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

主体:即访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。

资源:在应用中用户可以访问的任何东西,比如访问JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。

权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的
权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)打印文档等等。

角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。

授权流程

20200421184616

授权方式

Shiro 支持三种方式的授权。

编程式:通过写 if/else 授权代码块完成:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
//有权限
} else {
    //无权限
}

注解式:通过在执行的 Java 方法上放置相应的注解完成:

@RequiresRoles("admin")
public void hello() {
    //有权限
}
没有权限将抛出相应的异常;

JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:

<shiro:hasRole name="admin">
    <!- 有权限 ->
</shiro:hasRole>

授权实现

ini配置文件配置用户拥有的角色及角色-权限关系(shiro-permission.ini)

[users]
hu=123,role1,role2
yun=121,role1
[roles]
role1=user:create,user:update
role2=user:create,user:delete

规则:"用户名=密码,角色1,角色2" "角色=权限1,权限2",即首先根据用户名找到角色,然后根据角色再找到权限,角色是权限集合。
Shiro  不进行权限的维护,需要我们通过Realm返回相应的权限信息。只需要维护"用户--角色"之间的关系即可。
权限字符串的规则是:"资源标识符:操作:资源实例标识符",意思是对哪个资源的哪个实例具有什么操作,":"是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001

示例

[users]
hu=123,role1,role2
yun=121,role2
[roles]
role1=user:create,user:update
role2=user:create,user:delete

java伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户认证状态
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用户认证状态:" + isAuthenticated+"\n-------------------------");

// 判断拥有角色:role1
System.out.println(subject.hasRole("role1"));
// 判断拥有角色:role1 and role2
System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
// 判断拥有角色:role1 role2 role3
boolean[] result = subject.hasRoles(Arrays.asList("role1", "role2", "role3"));
System.out.println(Arrays.toString(result));

//还可以通过checkRole来检测是否具有某个角色,如果不具有这个角色,则抛出UnauthorizedException异常
subject.checkRole("role1");

// 判断拥有权限:user:create
System.out.println(subject.isPermitted("user:create"));
// 判断拥有权限:user:update and user:delete
System.out.println(subject.isPermittedAll("user:update", "user:delete"));
// 判断没有权限:user:view
System.out.println(subject.isPermitted("user:view"));

subject.logout();

结果:

登录成功
用户认证状态:true
-------------------------
true
true
[true, true, false]
true
true
false

Permission

字符串通配符权限

规则:资源标识符:操作:对象实例ID,即对哪个资源的哪个实例可以进行什么操作。
其默认支持通配符权限字符串,:表示资源/操作/实例的分割;,表示操作的分割;*表示任意资源/操作/实例。

1 、单个资源单个权限
    subject().checkPermissions("system:user:update");
2、单个资源多个权限
ini:role1=system:user:update,system:user:delete
Java:subject().checkPermissions("system:user:update", "system:user:delete");
用户拥有资源system:user的update和delete权限。如上可以简写成:
ini:role1="system:user:update,delete"
Java:subject().checkPermissions("system:user:update,delete");

3、单个资源全部权限
ini:role1=system:user:create,update,delete,view
Java:subject().checkPermissions("system:user:create,delete,update:view");
用户拥有资源system:user的create、update、delete和view所有权限。
如上可以简写成:
ini:role1=system:user:*

4、所有资源全部权限
ini:role1=*:view
Java:subject().checkPermissions("user:view");
用户拥有所有资源的"view"所有权限。

5、实例级别的权限
单个实例单个权限
ini:roll=user:view:1
Java:subject().checkPermissions("user:view:1");

单个实例多个权限
ini:roll="user:update,delete:1"
Java:
subject().checkPermissions("user:delete,update:1");
subject().checkPermissions("user:update:1", "user:delete:1");

单个实例所有权限
ini:roll=user:*:1
Java:subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");

所有实例单个权限
ini:roll=user:auth:*
Java:subject().checkPermissions("user:auth:1", "user:auth:2");

所有实例所有权限
ini:roll=user:*:*
Java:subject().checkPermissions("user:view:1", "user:auth:2");

自定义Realm实现授权

ini:
[users]
hu=123,role1
yun=121,role2
[roles]
role1=user:create,user:update
role2=user:create,user:delete

[main]
#自定义 realm
userRealm=com.hu.test.MyRealmRole
#将realm设置到securityManager
securityManager.realms=$userRealm

Realm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 用于授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 根据身份信息获取权限数据
// 模拟
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");
permissions.add("user:update");
// 将权限信息保存到AuthorizationInfo中
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class LoginLogoutTestRole {
@Test
public void testHelloworld() {
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
// 封装token信息
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
try {
subject.login(token);
if (subject.isAuthenticated()) {
System.out.println("登录成功");
} else {
System.out.println("用户名或密码错误!");
}
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
// 用户认证状态
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用户认证状态:" + isAuthenticated+"\n-------------------------");

// 判断拥有权限:user:create
System.out.println(subject.isPermitted("user:create"));
System.out.println(subject.isPermitted("user:update"));
System.out.println(subject.isPermitted("user:delete"));

subject.logout();
}
}

结果:

my Realm Role    hu        123
登录成功
用户认证状态:true
-------------------------
true
true
false

Authorizer 的职责是进行授权(访问控制),是 Shiro API 中授权核心的入口点,其提供了相应的角色/权限判断接口,具体请参考其 Javadoc。SecurityManager 继承了 Authorizer 接口,且提供了 ModularRealmAuthorizer 用于多 Realm 时的授权匹配。PermissionResolver 用于解析权限字符串到 Permission 实例,而 RolePermissionResolver 用于根据角色解析相应的权限集合。

我们可以通过如下 ini 配置更改 Authorizer 实现:

authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
securityManager.authorizer=$authorizer

对于 ModularRealmAuthorizer,相应的 AuthorizingSecurityManager 会在初始化完成后自动将相应的 realm 设置进去,我们也可以通过调用其 setRealms()方法进行设置。

设置 ModularRealmAuthorizer 的 permissionResolver,其会自动设置到相应的 Realm 上(其实现了 PermissionResolverAware 接口)

permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver

设置 ModularRealmAuthorizer 的 rolePermissionResolver,其会自动设置到相应的 Realm 上(其实现了 RolePermissionResolverAware 接口)

rolePermissionResolver=com.hu.test.MyRolePermissionResolver
authorizer.rolePermissionResolver=$rolePermissionResolver

会话管理

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。是完整的会话模块。

登录成功后使用 Subject.getSession()即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。

Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();

session.getId();//获取当前会话的唯一标识。
session.getHost();//获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost()提供的。

session.getTimeout();
session.setTimeout(毫秒);

获取/设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。

session.getStartTimestamp();
session.getLastAccessTime();
获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch()去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch()来更新最后访问时间。

session.touch();
session.stop();

更新会话最后访问时间及销毁会话;当 Subject.logout()时会自动调用 stop 方法来销毁会话。如果在web中,调用javax.servlet.http.HttpSession. invalidate()也会自动调用Shiro Session.stop方法进行销毁 Shiro 的会话。

session.setAttribute("key", "123");
session.getAttribute("key")
session.removeAttribute("key");

设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。

会话管理器

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实 现 直 接 把 会 话 管 理 委 托 给 相 应 的 SessionManager ,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了SessionsSecurityManager。

Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话 Key 获取会话

用于 Web 环境的 WebSessionManager 又提供了如下接口

boolean isServletContainerSessions();//是否使用 Servlet 容器的会话  
void validateSessions();//验证所有会话是否过期

Shiro 提供了三个默认实现:

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager : 用于Web环境的实现 , 可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。


替换 SecurityManager 默认的 SessionManager 可以在 ini 中配置(shiro.ini)
[main]
sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
securityManager.sessionManager=$sessionManager

Web 环境下的 ini 配置(shiro-web.ini):
[main]
sessionManager=org.apache.shiro.web.session.mgt.ServletContainerSessionManager
securityManager.sessionManager=$sessionManager

设置会话的全局过期时间(毫秒为单位),默认 30 分钟

sessionManager. globalSessionTimeout=1800000

默认情况下 globalSessionTimeout 将应用给所有 Session。可以单独设置每个 Session 的timeout 属性来为每个 Session 设置其超时时间。

如果使用 ServletContainerSessionManager 进行会话管理,Session 的超时依赖于底层Servlet 容器的超时时间,可以在 web.xml 中配置其会话的超时时间(分钟为单位)

<session-config>
<session-timeout>30</session-timeout>
</session-config>

在 Servlet 容器中,默认使用 JSESSIONID Cookie 维护会话,且会话默认是跟容器绑定的;在某些情况下可能需要使用自己的会话机制,此时我们可以使用DefaultWebSessionManager来维护会话:

sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie
sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager
sessionIdCookie.name=sid
#sessionIdCookie.domain=sishuok.com
#sessionIdCookie.path=
sessionIdCookie.maxAge=1800
sessionIdCookie.httpOnly=true
sessionManager.sessionIdCookie=$sessionIdCookie
sessionManager.sessionIdCookieEnabled=true
securityManager.sessionManager=$sessionManager

会话监听

会话监听器用于监听会话创建、过期及停止事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MySessionListener1 implements SessionListener {
@Override
public void onStart(Session session) {//会话创建时触发
System.out.println("会话创建:" + session.getId());
}
@Override
public void onExpiration(Session session) {//会话过期时触发
System.out.println("会话过期:" + session.getId());
}
@Override
public void onStop(Session session) {//退出/会话过期时触发
System.out.println("会话停止:" + session.getId());
}
}

如果只想监听某一个事件,可以继承 SessionListenerAdapter 实现:
public class MySessionListener2 extends SessionListenerAdapter {
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}

在 shiro-web.ini 配置文件中可以进行如下配置设置会话监听器:

sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1
sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2
sessionManager.sessionListeners=$sessionListener1,$sessionListener2

会话存储/持久化

Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现,如 DefaultSessionManager 在创建完 session 后会调用该方法;如保存到关系数据库/文件系统/NoSQL 数据库;即可以实现会话的持久化;返回会话 ID;主要此处返回的ID.equals(session.getId());

Serializable create(Session session);
//根据会话 ID 获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能

20200421184538

AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等;CachingSessionDAO提供了对开发者透明的会话缓存的功能,只需要设置相应的 CacheManager 即可;MemorySessionDAO 直接在内存中进行会话维护;而 EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。

sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionManager.sessionDAO=$sessionDAO

Shiro 提供了使用 Ehcache 进行会话存储,Ehcache 可以配合 TerraCotta 实现容器无关的布式集群。

缓存机制

Shiro 提供了类似于 Spring 的 Cache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。

Shiro 提供的 的 Cache :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public interface Cache<K, V> {
//根据 Key 获取缓存中的值
public V get(K key) throws CacheException;
//往缓存中放入 key-value,返回缓存中之前的值
public V put(K key, V value) throws CacheException;
//移除缓存中 key 对应的值,返回该值
public V remove(K key) throws CacheException;
//清空整个缓存
public void clear() throws CacheException;
//返回缓存大小
public int size();
//获取缓存中所有的 key
public Set<K> keys();
//获取缓存中所有的 value
public Collection<V> values();
}

Shiro 提供的 的 CacheManager 接口:

public interface CacheManager {
    //根据缓存名字获取一个 Cache
    public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}

Shiro 还 提供了 了 CacheManagerAware 用 于注入 CacheManager :

public interface CacheManagerAware {
    //注入 CacheManager
    void setCacheManager(CacheManager cacheManager);
}

Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如 Realm)是实现了 CacheManagerAware 并自动注入相应的 CacheManager。

Realm缓存

Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现;另外 AuthenticatingRealm 及 AuthorizingRealm 分别提供了对 AuthenticationInfo 和AuthorizationInfo 信息的缓存。

userRealm=com......realm.UserRealm
userRealm.credentialsMatcher=$credentialsMatcher
userRealm.cachingEnabled=true
userRealm.authenticationCachingEnabled=true
userRealm.authenticationCacheName=authenticationCache
userRealm.authorizationCachingEnabled=true
userRealm.authorizationCacheName=authorizationCache
securityManager.realms=$userRealm
cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile=classpath:shiro-ehcache.xml
securityManager.cacheManager=$cacheManager

userRealm.cachingEnabled:启用缓存,默认 false;
userRealm.authenticationCachingEnabled:启用身份验证缓存,即缓存 AuthenticationInfo 信息,默认 false;
userRealm.authenticationCacheName:缓存 AuthenticationInfo 信息的缓存名称;
userRealm. authorizationCachingEnabled:启用授权缓存,即缓存 AuthorizationInfo 信息,默认 false;
userRealm. authorizationCacheName:缓存 AuthorizationInfo 信息的缓存名称;
cacheManager:缓存管理器,此处使用 EhCacheManager,即 Ehcache 实现,需要导入相应的 Ehcache 依赖,

Session缓存

当我们设置了 SecurityManager 的 CacheManager 时,如:securityManager.cacheManager=$cacheManager
当我们设置 SessionManager 时:

sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
securityManager.sessionManager=$sessionManager

如 securityManager 实现了 SessionsSecurityManager,其会自动判断 SessionManager 是否实现了 CacheManagerAware 接口,如果实现了会把 CacheManager 设置给它。然后sessionManager 会判断相应的 sessionDAO(如继承自 CachingSessionDAO)是否实现了CacheManagerAware,如果实现了会把 CacheManager 设置给它;其会先查缓存,如果找不到才查数据库。

对于 CachingSessionDAO,可以通过如下配置设置缓存的名称:

sessionDAO=com.github.zhangkaitao.shiro.chapter11.session.dao.MySessionDAO
sessionDAO.activeSessionsCacheName=shiro-activeSessionCache

activeSessionsCacheName 默认就是 shiro-activeSessionCache。

MySQL+MyCat 分库分表

发表于 2018-02-20 | 分类于 mysql

MyCat介绍

系统开发中,数据库是非常重要的一个点。除了程序的本身的优化,如:SQL语句优化、代码优化,数据库的处理本身优化也是非常重要的。主从、热备、分表分库等都是系统发展迟早会遇到的技术问题问题。

MyCat是一个开源的分布式数据库系统,是一个实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原生协议与多个MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里。

MyCat发展到目前的版本,已经不是一个单纯的MySQL代理了,它的后端可以支持MySQL、SQL Server、Oracle、DB2、PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储,未来还会支持更多类型的存储。而在最终用户看来,无论是那种存储方式,在MyCat里,都是一个传统的数据库表,支持标准的SQL语句进行数据的操作,这样一来,对前端业务系统来说,可以大幅降低开发难度,提升开发速度。

20200420190855

MyCat 是使用 Java 编写的数据库中间件,运行在代码应用和 MySQL 数据库之间的应用。它的前身是阿里开发的数据库中间件corba,实现 MySQL 数据库分库分表集群管理的中间件,曾经出现过重大事故而放弃,被部分人员进行二次开发,形成 Mycat,使用 MyCat 之后,编写的所有的 SQL 语句必须严格遵守 SQL 标准规范.
使用 MyCat 中间件后的结构图如下:

20200420190928

当我们的应用只需要一台数据库服务器的时候我们并不需要Mycat,而如果你需要分库甚至分表,这时候应用要面对很多个数据库的时候,这个时候就需要对数据库层做一个抽象,来管理这些数据库,而最上面的应用只需要面对一个数据库层的抽象或者说数据库中间件就好了,这就是Mycat的核心作用。

所以可以这样理解:数据库是对底层存储文件的抽象,而Mycat是对数据库的抽象。

数据库切分

逻辑上的切分,在物理层面,是使用多库[database],多表[table]实现切分。分为横向和纵向切分;
横向切分:把一个表切分成多个表,相比纵向切分配置麻烦,无法实现表连接查询. 将一张表的字段,分散到若干张表中,将若干表连接到一起,才是当前表的完整数据。
纵向切分:把一个数据库切分成多个数据库,配置方便,只能实现两张表的表连接查询,将一张表中的数据,分散到若干个 database 的同结构表中。多个表的数据的集合是当前表格的数据。

Mycat 中的DB

Mycat 中定义的 database是逻辑上的,但是物理上未必存在。主要是针对纵向切分提供的概念,访问 MyCat,就是将 MyCat 当做 MySQL 使用。

db 数据库是 MyCat 中定义的 database。通过 SQL 访问 MyCat 中的 db 库的时候,对应的是 MySQL 中的 db1,db2,db3 三个库。物理上的 database 是 db1,db2,db3.逻辑上的
database 就是 db。

Mycat 中的表

逻辑意义上的表,一个MaCat的table可以对应物理库上的多个表。

Mycat 默认端口:8066

数据主机:dataHost

物理 MySQL 存放的主机地址,可以使用主机名、IP、域名定义。
数据节点:dataNode 物理的 database 是什么,数据保存的物理节点就是 database。
分片规则:当控制数据的时候,如何访问物理 database 和 table, 就是访问 dataHost 和 dataNode 的算法。
在 Mycat 处理具体的数据 CRUD 的时候,如何访问 dataHost 和 dataNode 的算法.如:哈希算法,crc16 算法等

MyCat实现分库分表读写分离

一、搭建准备

MyCat:192.168.1.105
MySQL master:192.168.1.103
MySQL slave:192.168.1.104

在需要搭建环境的机器上安装jdk,MySQL。这里使用上个例子的节点,关闭MySQL-Proxy功能,在原有的MySQL-Proxy机器上安装MyCat。
两台数据库上设置可供mycat访问的用户密码
mysql> grant all privileges on *.* to 'mycat'@'%' identified by 'mycat' with grant option;
mysql> flush privileges;

二、安装配置MyCat

1、下载Mycat官网:http://www.mycat.io/

下载Mycat-server-1.6.6.1-release-20181031195535-linux.tar.gz到Linux并解压到/usr/local/mycat
bin        mycat命令,启动、重启、停止等
catlet        catlet为Mycat的一个扩展功能
conf        Mycat 配置信息,重点关注
lib        Mycat引用的jar包,Mycat是java开发的
logs        日志文件,包括Mycat启动的日志和运行的日志。
直接运行测试一下:
[root@localhost mycat]# bin/mycat start
Starting Mycat-server...
[root@localhost mycat]# bin/mycat status
Mycat-server is running (20038).
远程登录看一下
[root@localhost mysql-proxy]# mysql -uroot -p123456 -h192.168.1.105 -P8066
mysql> show databases;
+----------+
| DATABASE |
+----------+
| TESTDB   |
+----------+
mysql> use TESTDB;
mysql> show tables;
+------------------+
| Tables in TESTDB |
+------------------+
| company          |
| customer         |
| customer_addr    |
| employee         |
| goods            |
| hotnews          |
| orders           |
| order_items      |
| travelrecord     |
+------------------+

初步测试MyCat没问题,配置文件中配置的逻辑表和库都能查到,但是不能查看数据,因为数据永远在物理库中,并且默认也米有配置对应真实的物理库。

2、配置示例测试

示例、配置分库,比如一个库demo的user表中有20w数据,现在进行分3个库,表结构一样,大概分别存6w左右数据。

第一步,创建好分库,表可以先不用创建。分别为demo1,demo2,demo3。

schema.xml配置如下:需要分库的数据库也放进来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--逻辑库表创建-->
<schema name="demo" checkSQLschema="false" sqlMaxLimit="100">
<table name="user" dataNode="demo1,demo2,demo3" rule="crc32slot" />
</schema>
<schema name="olddemo" checkSQLschema="false" sqlMaxLimit="100">
<table name="user" dataNode="demo_old"/>
</schema>

<!--逻辑节点,对应物理库-->
<dataNode name="demo1" dataHost="host103" database="demo1" />
<dataNode name="demo2" dataHost="host103" database="demo2" />
<dataNode name="demo3" dataHost="host103" database="demo3" />
<dataNode name="demo_old" dataHost="host103" database="demo" />

<!--物理库配置-->
<dataHost name="host103" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.1.103:3306" user="mycat" password="mycat">
<readHost host="hostS1" url="192.168.1.104:3306" user="mycat" password="mycat" />
</writeHost>
</dataHost>
</mycat:schema>
rule.xml中修改,因为使用crc32slot分片规则,这个规要配置分片的数据库节点数量
<function name="crc32slot" class="io.mycat.route.function.PartitionByCRC32PreSlot">
<property name="count">3</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
</function>
server.xml配置用户和该用户使用规则
<user name="root" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">demo,olddemo</property><!---可以访问哪些逻辑库---->

<!-- 表级 DML 权限设置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>

第二步:创建表,直接在MyCat里创建,都会分别同步到demo123库中

1
2
3
4
5
CREATE TABLE `user` (
`id` int(10) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)

在三个库查看表结构,都ok,slave节点也查看。

mysql> show tables from demo3;
+-----------------+
| Tables_in_demo3 |
+-----------------+
| user            |
+-----------------+

mysql> desc demo1.user;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(10)     | NO   | PRI | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
| _slot | int(11)     | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

发现多了一个_slot字段,这是因为使用了crc32slot分片规则得到的hash值。、

通过写脚本或者其他方式把源数据通过到新库。

读写分离设置

balance=”1”,全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且 M1 与 M2 互为主备),正常情况下,M2,S1,S2 都参与 select 语句的负载均衡。

balance=”3”,所有读请求随机的分发到 wiriterHost 对应的 readhost 执行,writerHost 不负担读压力,注意 balance=3 只在 1.4 及其以后版本有,1.3 没有。

示例:分表,单裤分表

场景:demo库中t_user表分成两个库t_users1/t_users2

核心配置:

1
2
3
4
5
6
7
8
<schema name="olddemo" checkSQLschema="false" sqlMaxLimit="1000000">
<table name="user" dataNode="demo_old"/>
<table name="t_users" subTables="t_users$1-2" primaryKey="id" dataNode="demo_old" rule="mod-long" />
</schema>
rule:
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<property name="count">2</property>
</function>

在数据库分别创建好t_users1/t_users2

1
2
3
4
5
CREATE TABLE `t_users1` (
`id` int(10) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)

启动并连接到MyCat:把老表中的数据通过MyCat复制到新表,在、查看数据,可以看出,根据mod-long规则每条数据交替存储到新表。

查询分析可以看出去两个物理表中查询数据:

20200420190612

查询结果是把两个表结果合并起来:

20200420190637

bin/mycat console 启动不成功可以通过这个命令查看原因
logs/wrapper.log   日志中记录的是所有的 mycat 操作. 查看的时候主要看异常信息 caused by 信息    

注:使用分片规则,MyCat启动后会在conf目录中创建一个ruledata下有使用规则信息分配的一个缓存文件,如果修改的分配规则,再次重启MyCat发现运用这个规则的缓存文件在就不会去重新生成,可能就得不到预计结果,所以,在重启前把这个目录对应的文件删除,让MyCat重新生成。

不能创建未在 schema.xml 中配置的逻辑表            

注意:如何部署

1、停机部署法:大致思路就是半夜停机升级,服务停了,跑数据迁移程序,进行数据迁移。
        写一个迁移程序,读db-old数据库,通过中间件写入新库db-new1和db-new2。
        校验迁移前后一致性,没问题就切该部分业务到新库
缺点:只能是访问量不高的,而且部署或有问题,没部署好,早上又要把数据切回去,晚上有的忙,身心累。

2、双写部署1
        上线一段代码:和表有关业务之前加一段代码,同时往老新库写。
                历史数据:在该次部署前,数据库表test_tb的有关数据,我们称之为历史数据。
                增量数据:在该次部署后,数据库表test_tb的新产生的数据,我们称之为增量数据。
        多加一条往消息队列中发消息的代码,只是写的sql。
        等到db-old中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。将迁移程序下线,写一段订阅程序订阅消息队列中的数据,订阅程序将订阅到到数据,通过中间件写入新库,新老库一致性验证,去除代码中的双写代码,将涉及到test_tb表的读写操作,指向新库。
3、双写部署2
        上面方式造成了严重的代码入侵。将非业务代码嵌入业务代码。
        替代办法是,记录日志。往消息队列里发的消息,都是写操作的消息。而binlog日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。
        打开binlog日志,迁移好历史数据后,写一个订阅程序,订阅binlog(mysql中有canal)。然后将订阅到的数据通过中间件,写入新库。

MyCat配置文件介绍

Mycat的配置文件都在conf目录里面,这里介绍几个常用的文件:

server.xml        Mycat的配置文件,设置账号、参数等
schema.xml        Mycat对应的物理数据库和数据库表的配置
rule.xml        Mycat分片(分库分表)规则

server.xml

配置 Mycat 服务信息,如: Mycat中的用户,用户可以访问的逻辑库/表,服务的端口号等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<mycat:server xmlns:mycat="http://io.mycat/">
<system>
<property name="nonePasswordLogin">0</property> <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
<property name="useHandshakeV10">1</property>
<property name="useSqlStat">0</property> <!-- 1为开启实时统计、0为关闭 -->
<property name="useGlobleTableCheck">0</property> <!-- 1为开启全加班一致性检测、0为关闭 -->
<property name="sequnceHandlerType">2</property>
<property name="subqueryRelationshipCheck">false</property>
<!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
<!--<property name="useCompression">1</property>--> <!--1为开启mysql压缩协议-->
<!--<property name="fakeMySQLVersion">5.6.20</property>--> <!--设置模拟的MySQL版本号-->
<!--<property name="processorBufferChunk">40960</property> -->
<!--<property name="processors">1</property>
<property name="processorExecutor">32</property> -->
<!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
<property name="processorBufferPoolType">0</property>
<!--默认是65535 64K 用于sql解析时最大文本长度 -->
<!--<property name="maxStringLiteralLength">65535</property>-->
<!--<property name="sequnceHandlerType">0</property>-->
<!--<property name="backSocketNoDelay">1</property>-->
<!--<property name="frontSocketNoDelay">1</property>-->
<!--<property name="processorExecutor">16</property>-->
<!--
<property name="serverPort">8066</property>
<property name="managerPort">9066</property>
<property name="idleTimeout">300000</property>
<property name="bindIp">0.0.0.0</property>
<property name="frontWriteQueueSize">4096</property>
<property name="processors">32</property> -->
<!--分布式事务开关,0为不过滤分布式事务,
1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),
2为不过滤分布式事务,但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>
<!--off heap for merge/order/group/limit 1开启 0关闭-->
<property name="useOffHeapForMerge">1</property>
<!--单位为m-->
<property name="memoryPageSize">64k</property>
<!--单位为k-->
<property name="spillsFileBufferSize">1k</property>
<property name="useStreamOutput">0</property>
<!--单位为m-->
<property name="systemReserveMemorySize">384m</property>
<!--是否采用zookeeper协调切换 -->
<property name="useZKSwitch">false</property>
<!-- XA Recovery Log日志路径 -->
<!--<property name="XARecoveryLogBaseDir">./</property>-->
<!-- XA Recovery Log日志名称 -->
<!--<property name="XARecoveryLogBaseName">tmlog</property>-->
<!--如果为 true的话 严格遵守隔离级别,不会在仅仅只有select语句的时候在事务中切换连接-->
<property name="strictTxIsolation">false</property>
<property name="useZKSwitch">true</property>
</system>
<!-- 全局SQL防火墙设置 -->
<!--白名单可以使用通配符%或着*-->
<!--例如<host host="127.0.0.*" user="root"/>-->
<!--例如<host host="127.0.*" user="root"/>-->
<!--例如<host host="127.*" user="root"/>-->
<!--例如<host host="1*7.*" user="root"/>-->
<!--这些配置情况下对于127.0.0.1都能以root账户登录-->
<!--
<firewall>
<whitehost>
<host host="1*7.0.0.*" user="root"/>
</whitehost>
<blacklist check="false">
</blacklist>
</firewall>
-->
<user name="root" defaultAccount="true"><!--user-->
<property name="password">123456</property>
<property name="schemas">TESTDB</property>
<!-- 表级 DML 权限设置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB</property>
<property name="readOnly">true</property>
</user>
</mycat:server>

rule.xml

用于定义分片规则的配置文件,主要是查看,很少修改。里边定义了很多分配规则比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<tableRule name="auto-sharding-long">
<rule>
<columns>id</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>
........
<tableRule name="crc32slot">
<rule>
<columns>id</columns>
<algorithm>crc32slot</algorithm>
</rule>
</tableRule>

auto-sharding-long:mycat 默认的分片规则:以 500 万为单位,实现分片规则。逻辑库 A 对应 dataNode - db1 和 db2. 1-500 万保存在 db1 中, 500 万零 1 到 1000 万保存在 db2 中,1000 万零 1 到 1500 万保存在 db1 中,依次类推。
crc32slot:在 CRUD 操作时,根据具体数据的 crc32 算法计算,数据应该保存在哪一个dataNode 中. 算法类似模运算。

server.xml

用于定义逻辑库和逻辑表的配置文件.在配置文件中可以定义读写分离,逻辑库,逻辑表,dataHost,dataNode 等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100">
<table name="travelrecord" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
<table name="hotnews" primaryKey="ID" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="mod-long" />
<table name="dual" primaryKey="ID" dataNode="dnx,dnoracle2" type="global" needAddLimit="false"/>
<table name="customer" primaryKey="ID" dataNode="dn1,dn2" rule="sharding-by-intfile">
<childTable name="orders" primaryKey="ID" joinKey="customer_id" parentKey="id">
<childTable name="order_items" joinKey="order_id" parentKey="id" />
</childTable>
<childTable name="customer_addr" primaryKey="ID" joinKey="customer_id" parentKey="id" />
</table>
</schema>
<dataNode name="dn1$0-743" dataHost="localhost1" database="db$0-743"/>
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="localhost:3306" user="root" password="123456">
<readHost host="hostS2" url="192.168.1.200:3306" user="root" password="xxx" />
</writeHost>
<writeHost host="hostS1" url="localhost:3316" user="root" password="123456" />
</dataHost>
</mycat:schema>

配置解释

schema:数据库设置,此数据库为逻辑数据库
name        逻辑数据库名,与server.xml中的schema对应
checkSQLschema        数据库前缀相关设置,是否检测SQL语法的schema信息,比如逻辑库名称是D,SQL:select * from D.table 当为true时:mycat发送到数据库的sql是select * from table;false时:发送的是select * from A.table

sqlMaxLimit select 时默认的limit,Mycat 在执行 SQL 的时候,如果 SQL 语法中没有 limit 子句.自动增加 limit 子句.避免查询全表

table标签:定义逻辑表,要求逻辑表和物理表的名字一致。
name        表名,物理数据库中表名
dataNode        表存储到哪些节点,多个节点用逗号分隔。节点为下文dataNode设置的name,多个也就分库了
primaryKey        主键字段名,自动生成主键时需要设置
autoIncrement          是否自增
rule        使用分片规则名,规则都在rule.xml。SQL 语句发送到 Mycat 中后,Mycat 如何计算,应该将当期的 SQL 发送到哪一个物理数
              据库管理系统或物理 database 中
ruleRequired    表是否绑定分片规则,如果配置为 true,但没有配置具体 rule 的话 ,程序会报错。
type   逻辑表的类型,目前逻辑表只有"全局表"和"普通表"两种类型。全局表:global。
       childTable 标签
       name 定义子表的表名。
       joinKey 插入子表的时候会使用这个列的值查找父表存储的数据节点。
parentKey 属性指定的值一般为与父表建立关联关系的列名。程序首先获取 joinkey 的值,再通过 parentKey 属性指定的列名产生查询语句,通过执行该语句得到父表存储在哪个分片上。从而确定子表存储的位置。

dataNode   分片信息,也就是分库相关配置
name        节点名,定义的逻辑名称,对应具体的物理数据库 database
datahost        物理数据库名,代表使用的物理数据库所在位置和配置信息,与datahost中name对应
database        物理数据库中数据库名

dataHost   物理数据库,真正存储数据的数据库
name        物理数据库名,与dataNode中dataHost对应
balance        均衡负载的方式
       balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的 writeHost 上。
       balance="1",全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡,简单的说,当双   主双从模式(M1->S1,M2->S2,并且 M1 与 M2 互为主备),正常情况下,M2,S1,S2 都参与 select 语句的负载均衡。
       balance="2",所有读操作都随机的在 writeHost、readhost 上分发。
       balance="3",所有读请求随机的分发到 wiriterHost 对应的 readhost 执行,writerHost 不负担读压力,注意 balance=3 只在 1.4 及其以后版本有,1.3 没有。

maxCon/minCon  最大最小连接数
writeType        负载均衡类型,目前的取值有 3 种:

1. writeType="0", 所有写操作发送到配置的第一个 writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后已切换后的为准,切换记录在配置文件中:dnindex.properties .
2. writeType="1",所有写操作都随机的发送到配置的 writeHost,1.5 以后废弃不推荐。

dbType        数据库类型
dbDriver     数据库驱动类型,,native,使用 mycat 提供的本地驱动
switchType         
       -1 表示不自动切换
       1 默认值,自动切换 可能有IO延迟问题
       2 基于 MySQL 主从同步的状态决定是否切换   心跳语句为 show slave status
       3 基于 MySQL galary cluster 的切换机制(适合集群) 心跳语句为 show status like 'wsrep%'
heartbeat        心跳检测语句,注意语句结尾的分号要加。
       子标签 writeHost:写数据的数据库定义标签. 实现读写分离操作,下面的readHost表示只读
host 数据库命名
url  数据库访问路径
user  数据库访问用户名
password  密码

HAProxy 配置文件详解

发表于 2018-02-20 | 分类于 负载均衡
haproxy 配置中分成五部分内容,分别如下:
global:参数是进程级的,通常是和操作系统相关。这些参数一般只设置一次,如果配置无误,就不需要再次进行修改;
defaults:配置默认参数,这些参数可以被用到frontend,backend,Listen组件;
frontend:接收请求的前端虚拟节点,Frontend可以更加规则直接指定具体使用后端的backend;
backend:后端服务集群的配置,是真实服务器,一个Backend对应一个或者多个实体服务器;
Listen Fronted和backend的组合体。

global   # 全局参数的设置
# log语法:log <address_1>[max_level_1] # 全局的日志配置,使用log关键字,指定使用127.0.0.1上的syslog服务中的local0日志设备,记录日志等级为info的日志
log 127.0.0.1 local0 info
# 设置运行haproxy的用户和组,也可使用uid,gid关键字替代之
user haproxy
group haproxy
# 以守护进程的方式运行
daemon
    #改变当前工作目录,可以提升 haproxy 的安全级别
chroot      /usr/share/haproxy  
    # 设置haproxy启动时的进程数,用于守护进程模式的 haproxy;默认为止启动 1 个进程。该值的设置应该和服务器的CPU核心数一致,即常见的2颗8核心CPU的服务器,即共有16核心,则可以将其值设置为:<=16 ,创建多个进程数,可以减少每个进程的任务队列,但是过多的进程数也可能会导致进程的崩溃。
nbproc 1
# 定义每个haproxy进程的最大连接数 ,由于每个连接包括一个客户端和一个服务器端,所以单个进程的TCP会话最大数目将是该值的两倍。
maxconn 4096
# 设置最大打开的文件描述符数,该值会自动计算,所以不建议进行设置
#ulimit -n 65536
# 定义haproxy的pid
#pidfile /var/run/haproxy.pid
    # 定义当前节点的名称,用于 HA 场景中多 haproxy 进程共享同一个 IP        地址时
    node                 haproxy1                           
    # 当前实例的描述信息
    description haproxy1  

defaults # 默认部分的定义
# mode语法:mode {http|tcp|health} 。http是七层模式,tcp是四层模式,health是健康检测,返回OK
                    # tcp: 实例运行于纯 tcp 模式,在客户端和服务器端之间将建立一个全双工的连接,且不会对 7 层报文做任何类型的检查,此为默认模式
                    # http:实例运行于 http 模式,客户端请求在转发至后端服务器之前将被深度分析,所有不与 RFC 模式兼容的请求都会被拒绝
                    # health:实例运行于 health 模式,其对入站请求仅响应"OK"信息并关闭连接,且不会记录任何日志信息 ,此模式将用于相应外部组件的监控状态检测请求
    mode http
# 使用127.0.0.1上的syslog服务的local3设备记录错误信息
log 127.0.0.1 local3 err
# 定义连接后端服务器的失败重连次数,连接失败次数超过此值后将会将对应后端服务器标记为不可用
retries 3
# 启用日志记录HTTP请求,默认haproxy日志记录是不记录HTTP请求的,只记录"时间[Jan 5 13:23:46] 日志服务器[127.0.0.1] 实例名已经pid[haproxy[25218]] 信息[Proxy http_80_in stopped.]",日志格式很简单。
option httplog
# 当使用了cookie时,haproxy将会将其请求的后端服务器的serverID插入到cookie中,以保证会话的SESSION持久性;而此时,如果后端的服务器宕掉了,但是客户端的cookie是不会刷新的,如果设置此参数,将会将客户的请求强制定向到另外一个后端server上,以保证服务的正常。
option redispatch
# 当服务器负载很高的时候,自动结束掉当前队列处理比较久的链接
option abortonclose
# 启用该项,日志中将不会记录空连接。所谓空连接就是在上游的负载均衡器或者监控系统为了探测该服务是否存活可用时,需要定期的连接或者获取某一固定的组件或页面,或者探测扫描端口是否在监听或开放等动作被称为空连接;官方文档中标注,如果该服务上游没有其他的负载均衡器的话,建议不要使用该参数,因为互联网上的恶意扫描或其他动作就不会被记录下来
option dontlognull
# 每处理完一个request时,haproxy都会去检查http头中的Connection的值,如果该值不是close,haproxy将会将其删除,如果该值为空将会添加为:Connection: close。使每个客户端和服务器端在完成一次传输后都会主动关闭TCP连接。与该参数类似的另外一个参数是"option forceclose",该参数的作用是强制关闭对外的服务通道,因为有的服务器端收到Connection: close时,也不会自动关闭TCP连接,如果客户端也不关闭,连接就会一直处于打开,直到超时。
    #每次请求完毕后主动关闭http通道
#option                 http-server-close                           
    option httpclose
    # 前端的最大并发连接数(默认为 2000)
maxconn         2000                                                                         
    # 连接超时,设置成功连接到一台服务器的最长等待时间,默认单位是毫秒
timeout connect 5000ms
# 客户端超时 设置连接客户端发送数据时的成功连接最长等待时间,默认单位是毫秒
timeout client 50000ms
# 服务器超时 设置服务器端回应客户度数据发送的最长等待时间,默认单位是毫秒
    timeout server 50000ms

listen admin_status # 定义一个名为admin_status的部分,状态信息统计页面
# 定义监听的套接字
bind 0.0.0.0:1080
# 定义为HTTP模式
mode http
# 继承global中log的定义
log global
# stats是haproxy的一个统计页面的套接字,该参数设置统计页面的刷新间隔为30s
stats refresh 30s
# 设置统计页面的uri为/admin?stats
stats uri /admin?stats
# 设置统计页面认证时的提示内容
stats realm Private lands
# 设置统计页面认证的用户和密码,如果要设置多个,另起一行写入即可
stats auth admin:password
# 隐藏统计页面上的haproxy版本信息
stats hide-version
    # 启用日志记录 HTTP 请求
    option httplog

frontend http_80_in # 定义一个名为http_80_in的前端部分
# http_80_in定义前端部分监听的套接字
bind 0.0.0.0:80
# 定义为HTTP模式
mode http
# 继承global中log的定义
log global
# 启用X-Forwarded-For,在requests头部插入客户端IP发送给后端的server,使后端server获取到客户端的真实IP
option forwardfor

    # 当请求是以这些结尾的交给url_static处理策略
    acl    url_static    url_reg /*.(css|jpg|png|jpeg|js|gif)$
    #或者path_end      /*.(css|jpg|png|jpeg|js|gif)$
    # 当请求的url末尾是以.jsp结尾的,交给jsp_web策略
    acl   jsp_web    path_end .jsp

    #如果url_static策略满足,交给static_server
    use_backend static_server if url_static
    use_backend jsp_server if jsp_web
    #其他请求交给my_webserver
default_backend       my_webserver

#jsp_server的后端部分
backend jsp_server  
# 设置为http模式
mode http
# 负载均衡调度算法可用于"defaults"、"listen"和"backend"中,默认为轮询方式,有hash,roundrobin 轮询, source 保存session值,支持static-rr,leastconn,first,uri等参数
balance source
# 允许向cookie插入SERVERID,每台服务器的SERVERID可在下面使用cookie关键字定义
cookie SERVERID
# 开启对后端服务器的健康检测,通过GET /test/index.php来判断后端服务器的健康情况
option httpchk GET /test/index.php
    #抵不过一后端server
server jsp_server_1 192.169.1.111:80 cookie 1 check inter 2000 rise 3 fall 3 weight 1
server jsp_server_2 192.169.1.112:80 cookie 2 check inter 2000 rise 3 fall 3 weight 1
# server语法:server <name> <address>[:[port]] [param*]    只能用于 listen 和 backend 区段。
    #  <name>为此服务器指定的内部名称,其将会出现在日志及警告信息中;后端服务器的IP地址,支持端口映射:[port];
    #  [param*]为此 server 设定的一系列参数,均为可选项,参数比较多,下面仅说明几个常用的参数:
    #  weight:权重,默认为 1,最大值为 256,0 表示不参与负载均衡
    #  backup:设定为备用服务器,仅在负载均衡场景中的其他 server 均不可以启用此server
    #  check:启动对此 server 执行监控状态检查
    #  inter:设定监控状态检查的时间间隔,单位为毫秒,默认为 2000,也可以使用 fastinter 和 downinter 来根据服务器端专题优化此事件延迟
    #  rise:设置 server 从离线状态转换至正常状态需要检查的次数(不设置的情况下,默认值为 2)
    #  fall:设置 server 从正常状态转换至离线状态需要检查的次数(不设置的情况下,默认值为 3)
    # cookie:为指定 server 设定 cookie 值,此处指定的值将会在请求入站时被检查,第一次为此值挑选的 server 将会被后续的请求所选中,其目的在于实现持久连接的功能
    # maxconn:指定此服务器接受的最大并发连接数,如果发往此服务器的连接数目高于此处指定的值,其将被放置于请求队列,以等待其他连接被释放

#定义static_server
backend static_server
mode http
server static_server_1 127.0.0.1:80 cookie 3 check inter 2000 rise 3 fall 3

#定义my_webserver,名字需要与frontend里面配置项default_backend 值相一致
backend my_webserver        
    # 负载均衡算法
balance     roundrobin                              
server  web01 192.169.1.33:80  check inter 2000 fall 3 weight 30              #定义的多个后端
server  web02 192.169.1.34:80  check inter 2000 fall 3 weight 30              #定义的多个后端
server  web03 192.169.1.35:80  check inter 2000 fall 3 weight 30              #定义的多个后端

注意:多节点部署时 node 、 description 的值要做相应调整

HAProxy+Keepalived+MyCat+MySQL高可用集群

发表于 2018-02-20 | 分类于 负载均衡

1、集群环境分配

由于硬件有限,安装了6台Linux虚拟机,做如下分配:
MySQL master1:192.168.1.103
MySQL slave1:192.168.1.104
MySQL master2/MyCat1:192.168.1.105
MySQL slave2/MyCat2:192.168.1.106
HAProxy1:192.168.1.107
HAProxy2:192.168.1.108
        VIP:192.168.1.8
使用软件:
        haproxy-1.7.1.tar.gz
        keepalived-1.2.18.tar.gz
        Mycat-server-1.6.6.1-release-20181031195535-linux.tar.gz
MySQL在最初的时候就安装好,每台机器克隆后都有MySQL,需要说明的是,把需要使用MySQL的uuid修改,因为都是克隆的,所以这个值是一样的[root@localhost ~]# cat /usr/local/mysql-5.6.31/data/auto.cnf 修改文件里边,随机改一下就即可。
其他文件,分别对应上传到服务器。

2、配置MySQL主从和主主

在103和105配置主库,分别执行如下操作,其中105的auto_increment_offset=2
 vim /etc/my.cnf 修改配置
 [client]
 port = 3306 #客户端端口
 default-character-set = utf8mb4  #客户端默认字符集
 [mysqld]
 default_storage_engine = InnoDB #默认数据库引擎
 port = 3306
 character_set_server=utf8
 log_bin=master_bin #开启二进制日志
 server_id=1 #设置server_id
 #步进值,一般有n台主MySQL就填n
 auto_increment_increment=2  
 #起始值,一般填第n台主MySQL。
 auto_increment_offset=1  
 # 做为从库时,数据库的修改也会写到bin-log里
 log-slave-updates  
 # 表示自动删除5天以前的binlog,可选
 expire_logs_days=5
 #忽略mysql库
 #binlog-ignore=mysql  
 #忽略information_schema库
 #binlog-ignore=information_schema  
 #要同步的数据库,默认所有库
 #replicate-do-db=demo

 103/104/105/106的server_id分别是1/3/2/4

 在104和106配置从库,分别执行如下操作:
 vim /etc/my.cnf 修改配置
 [client]
 port = 3306 #客户端端口
 default-character-set = utf8mb4  #客户端默认字符集
 [mysqld]
 default_storage_engine = InnoDB #默认数据库引擎
 port = 3306
 character_set_server=utf8
 server_id=3 #设置server_id

 分别启动两台master,在两台master中的MySQL中创建用户供Slave使用
 mysql> grant all privileges on *.* to 'slave'@'192.168.1.%' identified by 'slave' with grant option;
 mysql> flush privileges;

查看master状态,记录文件名

SHOW MASTER STATUS;

分别启动Slave,配置Slave,登录MySQL,先停止stop slave;再执行Slave配置,执行代码如下:

103:CHANGE MASTER TO MASTER_HOST='192.168.1.105',MASTER_USER='slave',MASTER_PASSWORD='slave',MASTER_LOG_FILE='master_bin.000001';
104:CHANGE MASTER TO MASTER_HOST='192.168.1.103',MASTER_USER='slave',MASTER_PASSWORD='slave',MASTER_LOG_FILE='master_bin.000001';
105:CHANGE MASTER TO MASTER_HOST='192.168.1.103',MASTER_USER='slave',MASTER_PASSWORD='slave',MASTER_LOG_FILE='master_bin.000001';  因为两个master互为主从,所以在两边都要配置对方的master
106:CHANGE MASTER TO MASTER_HOST='192.168.1.105',MASTER_USER='slave',MASTER_PASSWORD='slave',MASTER_LOG_FILE='master_bin.000001';

再次启动start slave

通过命令 show slave status\G; 查看每个节点信息,主从都是一致的。没问题。数据测试也ok。

测试双主从:开启日志查询set global general_log = on;,动态监控日志信息tail -f /usr/local/mysql-5.6.31/data/localhost.log

都不宕机的情况下,不管是从哪一台master修改数据,其他节点都能显示,同步一致性。
当master1停止MySQL情况下,master2修改了数据,master再次启动也会把master2修改的同步到master1和Slave1。反之亦然没问题。

3、配置MyCat

分别在105、106解压MyCat文件到/usr/local/mycat下

直接运行测试一下:

[root@localhost mycat]# bin/mycat start

没问题,配置MyCat逻辑表,进行测试,两个都一致。这里配置只是为了测试用,实际中按照实际需求配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--逻辑库表创建-->
<schema name="demo" checkSQLschema="false" sqlMaxLimit="100">
<table name="user" dataNode="demo1"/>
</schema>
<schema name="demo1" checkSQLschema="true" sqlMaxLimit="100">
<table name="user" dataNode="demo111" />
</schema>


<!--逻辑节点,对应物理库-->
<dataNode name="demo1" dataHost="host103" database="demo" />
<dataNode name="demo2" dataHost="host105" database="demo" />
<dataNode name="demo111" dataHost="host111" database="demo" />

<!--物理库配置-->
<dataHost name="host103" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<heartbeat>show slave status</heartbeat>
<writeHost host="hostM1" url="192.168.1.103:3306" user="root" password="123456"/>
<writeHost host="hostS1" url="192.168.1.104:3306" user="root" password="123456"/>
</dataHost>
<dataHost name="host105" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<heartbeat>show slave status</heartbeat>
<writeHost host="hostM2" url="192.168.1.105:3306" user="root" password="123456"/>
<writeHost host="hostS2" url="192.168.1.106:3306" user="root" password="123456"/>
</dataHost>
<dataHost name="host111" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<heartbeat>show slave status</heartbeat>
<writeHost host="hosstM1" url="192.168.1.103:3306" user="root" password="123456">
<readHost host="hostsS1" url="192.168.1.104:3306" user="root" password="123456"/>
</writeHost>
<writeHost host="hostsM2" url="192.168.1.105:3306" user="root" password="123456">
<readHost host="hostsS2" url="192.168.1.106:3306" user="root" password="123456"/>
</writeHost>
</dataHost>
</mycat:schema>

server.xml中增加逻辑库访问。在远程分别访问两台MyCat并测试。

 ①、每个节点都不宕机的情况下测试都没任何问题。
 ②、如果master1宕机,停止MySQL,在插入数据,日志可以看到往master2插入,salve2同步,再次恢复master1,数据变化将同步回master1
 ③、读数据,通常都分配到Slave和master2上,只有出现io延迟才会在master1读取。
两台MyCat搭配MySQL都没问题,下一步:

4、安装xinetd

xinetd是新一代的网络守护进程服务程序,又叫超级internet服务器。经常用来托管轻量级服务。
这里安装xinetd的目的是检测MyCat的服务状态,为HAProxy提供MyCat服务状态的依据。
A、安装如下:
        yum install -y xinetd
        检查/etc/xinetd.conf 的末尾是否有 includedir /etc/xinetd.d ,没有就加上
        检查 /etc/xinetd.d 目录是否存在,不存在则创建mkdir /etc/xinetd.d/

B、增加 Mycat 存活状态检测服务配置:vim /etc/xinetd.d/mycat_status
        service mycat_status
        {
                flags = REUSE
                ## 使用该标记的 socket_type 为 stream,需要设置 wait 为 no
                socket_type = stream ## 封包处理方式,Stream 为 TCP 数据包
                port = 48700 ## 服务监听端口
                wait = no ## 表示不需等待,即服务将以多线程的方式运行
                user = root ## 执行此服务进程的用户
                server =/usr/local/bin/mycat_status ## 需要启动的服务脚本
                log_on_failure += USERID ## 登录失败记录的内容
                disable = no ## 要启动服务,将此参数设置为 no
        }

C、添加 /usr/local/bin/mycat_status 服务脚本 vim /usr/local/bin/mycat_status
        #!/bin/bash
        Mycat=`/usr/local/mycat/bin/mycat status | grep 'not running' | wc -l`
        if [ "$Mycat" = "0" ]; then
                /bin/echo -e "HTTP/1.1 200 OK\r\n"
                else
                /bin/echo -e "HTTP/1.1 503 Service Unavailable\r\n"
        fi  
D、脚本赋予可执行权限
        chmod 755 /usr/local/bin/mycat_status

E、在 /etc/services 中加入 mycat_status 服务
        vim /etc/services 在末尾加入:
        mycat_status 48700/tcp # mycat_status
        保存后,重启 xinetd 服务 service xinetd restart

F、验证 mycat_status 服务是否成功启动
        netstat -antup|grep 48700

G、测试脚本
        /usr/local/bin/mycat_status

5、安装HAProxy

在107和108中安装HAProxy,解压文件到/usr/local下。
①、编译安装:
        依赖yum install -y gcc gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel
        编译:make TARGET=linux2628 ARCH=x86_64 USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 PREFIX=/usr/local/haproxy
                TARGET 是指定内核版本,高于 2.6.28 的建议设置为 linux2628,低于的是多少写多少。内核版本查看命令# uname -r, ARCH 指定系统架构,openssl pcre zlib 这三个包需要安装不然不支持
        安装:创建安装目录mkdir /usr/local/haproxy
        执行make install PREFIX=/usr/local/haproxy安装
②、配置:
        创建配置文件目录
                mkdir -p /usr/local/haproxy/conf
                mkdir -p /etc/haproxy/
        添加配置文件并创建软连接
                vi /usr/local/haproxy/conf/haproxy.cfg
                        # Simple configuration for an HTTP proxy listening on port 80 on all
                        # interfaces and forwarding requests to a single backend "servers" with a
                        # single server "server1" listening on 127.0.0.1:8000
                        global
                                daemon
                                maxconn 256
                                defaults
                                mode http
                                timeout connect 5000ms
                                timeout client 50000ms
                                timeout server 50000ms
                                frontend http-in
                                bind *:80
                        default_backend servers
                        backend servers
                        server server1 127.0.0.1:8000 maxconn 32
                        # The same configuration defined with a single listen block. Shorter but
                        # less expressive, especially in HTTP mode. global
                        daemon
                        maxconn 256
                        defaults
                        mode http
                        timeout connect 5000ms
                        timeout client 50000ms
                        timeout server 50000ms
                        listen http-in
                        bind *:80
                        server server1 127.0.0.1:8000 maxconn 32
                创建软连接:ln -s /usr/local/haproxy/conf/haproxy.cfg /etc/haproxy/haproxy.cfg

        ③、拷贝错误页面并创建软连接
                cp -r /usr/local/haproxy-1.7.1/examples/errorfiles /usr/local/haproxy/
                ln -s /usr/local/haproxy/errorfiles /etc/haproxy/errorfiles

        ④、添加 HAProxy 命令脚本软连接
                ln -s /usr/local/haproxy/sbin/haproxy /usr/sbin
        设置自启动(可选)
                cp /usr/local/haproxy-1.7.1/examples/haproxy.init /etc/rc.d/init.d/haproxy
                chmod +x /etc/rc.d/init.d/haproxy

                chkconfig --add haproxy
                chkconfig haproxy on
        ⑤、配置MyCat负载均衡集群
                修改配置;vi /usr/local/haproxy/conf/haproxy.cfg
                107:        
                        global
                                log 127.0.0.1 local0 info
                                chroot /usr/share/haproxy
                                group haproxy
                                user haproxy
                                daemon
                                nbproc 1
                                maxconn 4096
                                node haproxy1
                                description haproxy1
                        defaults
                                log global
                                mode http
                                option httplog
                                retries 3
                                option redispatch
                                maxconn 2000
                                timeout connect 5000ms
                                timeout client 50000ms
                                timeout server 50000ms
                        listen admin_stats
                                bind :48800
                                stats uri /admin-status
                                stats auth admin:admin
                                mode http
                                option httplog
                        listen mycat_servers
                                bind :3307
                                mode tcp
                                option tcplog
                                option tcpka
                                option httpchk OPTIONS * HTTP/1.1\r\nHost:\ www
                                balance roundrobin
                                server mycat_01 192.168.1.105:8066 check port 48700 inter 2000ms rise 2 fall 3 weight 10
                                server mycat_02 192.168.1.106:8066 check port 48700 inter 2000ms rise 2 fall 3 weight 10                                

                108:
                        global
                                log 127.0.0.1 local0 info
                                chroot /usr/share/haproxy
                                group haproxy
                                user haproxy
                                daemon
                                nbproc 1
                                maxconn 4096
                                node haproxy2
                                description haproxy2
                        defaults
                                log global
                                mode http
                                option httplog
                                retries 3
                                option redispatch
                                maxconn 2000
                                timeout connect 5000ms
                                timeout client 50000ms
                                timeout server 50000ms
                        listen admin_stats
                                bind :48800
                                stats uri /admin-status
                                stats auth admin:admin
                                mode http
                                option httplog
                        listen mycat_servers
                                bind :3307
                                mode tcp
                                option tcplog
                                option tcpka
                                option httpchk OPTIONS * HTTP/1.1\r\nHost:\ www
                                balance roundrobin
                                server mycat_01 192.168.1.105:8066 check port 48700 inter 2000ms rise 2 fall 3 weight 10
                                server mycat_02 192.168.1.106:8066 check port 48700 inter 2000ms rise 2 fall 3 weight 10

        ⑥、为 HAProxy 创建 Linux 系统用户                
                groupadd haproxy
                useradd -g haproxy haproxy

        ⑦、创建 chroot 运行的路径mkdir /usr/share/haproxy

        ⑧、开启 rsyslog 的 haproxy 日志记录功能
                默认情况下 haproxy 是不记录日志的,如果需要记录日志,还需要配置系统的 syslog,在 linux 系统中是 rsyslog 服务。syslog 服务器可以用作一个网络中的日志监控中心,rsyslog是一个开源工具,被广泛用于 Linux 系统以通过 TCP/UDP 协议转发或接收日志消息。安装配置 rsyslog 服务。
                        yum install -y rsyslog  
                修改配置:
                        vi /etc/rsyslog.conf                                
                                $ModLoad imudp # 模块名,支持 UDP 协议
                                $UDPServerRun 514  #允许 514 端口接收使用 UDP 和 TCP 协议转发过来的日志,默认端口
                        确认 #### GLOBAL DIRECTIVES #### 段中是否有 $IncludeConfig /etc/rsyslog.d/*.conf 没有则增加上此配置。rsyslog 服务会来此目录加载配置                                
                        创建 haproxy 的日志配置文件
                        vi /etc/rsyslog.d/haproxy.conf
                                local0.* /var/log/haproxy.log
                                &~
                        注:如果不加上&~,除了在/var/log/haproxy.log中写日志之外,也会写入/var/log/massage中

                        保存后重启rsyslog服务service rsyslog restart
                等到 HAProxy 服务启动后,就能在/var/log/haproxy.log中就会有日志。                                
        ⑨、配置系统内核IP包转发规则,Linux默认不会转发tcp层的包
                vi /etc/sysctl.conf
                                net.ipv4.ip_forward = 1
                生效: sysctl -p                
        ⑩、启动HAProxy
                service haproxy start
                ps -ef | grep haproxy


                测试:查看 HAProxy 提供的 WEB 统计应用
                        http://192.168.1.107:48800/admin-status
                        http://192.168.1.108:48800/admin-status

20200420192551

6、安装Keepalived

 A、安装
         yum -y install keepalived
         keepalived相关文件:
         /etc/keepalived
         /etc/keepalived/keepalived.conf     #keepalived服务主配置文件
         /etc/rc.d/init.d/keepalived         #服务启动脚本(centos 7 之前的用init.d 脚本启动,之后的systemd启动)
         /etc/sysconfig/keepalived
         /usr/bin/genhash
         /usr/libexec/keepalived
         /usr/sbin/keepalived
 B、配置 /etc/keepalived/keepalived.conf  
 192.168.1.107:
         ! Configuration File for keepalived
         global_defs {
                 # keepalived 自带的邮件提醒需要开启 sendmail 服务。建议用独立的监控或第三方SMTP
                 router_id haproxy1 ## 标识本节点的字符串,通常为 hostname,需要修改/etc/hosts
         }
         # keepalived 会定时执行脚本并对脚本执行的结果进行分析,动态调整 vrrp_instance的优先级。
         # 如果脚本执行结果为 0,并且 weight 配置的值大于 0,则优先级相应的增加。
         # 如果脚本执行结果非 0,并且 weight 配置的值小于 0,则优先级相应的减少。
         # 其他情况,维持原本配置的优先级,即配置文件中 priority 对应的值。
         vrrp_script chk_haproxy {
                 script "/etc/keepalived/haproxy_check.sh"  # 检测 haproxy 状态的脚本路径
                 interval 2  # 检测时间间隔
                 weight 2    # 如果条件成立,权重+2
         }
         # 定义虚拟路由, VI_1 为虚拟路由的标示符,自己定义名称
         vrrp_instance VI_1 {
                 state BACKUP         # 默认主设备(priority 值大的)和备用设备(priority 值小的)都设置为 BACKUP,通常由priority 来控制同时启动情况下的默认主备,否则先启动的为主设备
                 interface eth4         # 绑定虚拟 IP 的网络接口,与本机 IP 地址所在的网络接口相同
                 virtual_router_id 35 # 虚拟路由的 ID 号,两个节点设置必须一样,可选 IP 最后一段使用,相同的 VRID 为一个组,他将决定多播的 MAC 地址
                 priority 100         # 节点优先级,值范围 0-254, MASTER 要比 BACKUP 高
                 nopreempt                 # 主设备(priority 值大的)配置一定要加上 nopreempt,否则非抢占也不起作用
                 advert_int 1         # 组播信息发送间隔,两个节点设置必须一样,默认 1s
                 # 设置验证信息,两个节点必须一致
                 authentication {
                         auth_type PASS
                         auth_pass 1111  
                 }
                 # 将 track_script 块加入 instance 配置块
                 track_script {
                         chk_haproxy   # 检查 HAProxy 是否正常运行
                 }
                 # 虚拟 IP 池, 两个节点设置必须一样
                 virtual_ipaddress {
                         192.168.1.10  # 虚拟 ip,可以定义多个,每行一个
                 }
         }

192.168.1.108:
         ! Configuration File for keepalived
         global_defs {
                 # keepalived 自带的邮件提醒需要开启 sendmail 服务。建议用独立的监控或第三方SMTP
                 router_id haproxy2 ## 标识本节点的字符串,通常为 hostname,需要修改/etc/hosts
         }
         # keepalived 会定时执行脚本并对脚本执行的结果进行分析,动态调整 vrrp_instance的优先级。
         # 如果脚本执行结果为 0,并且 weight 配置的值大于 0,则优先级相应的增加。
         # 如果脚本执行结果非 0,并且 weight 配置的值小于 0,则优先级相应的减少。
         # 其他情况,维持原本配置的优先级,即配置文件中 priority 对应的值。
         vrrp_script chk_haproxy {
                 script "/etc/keepalived/haproxy_check.sh"  # 检测 haproxy 状态的脚本路径
                 interval 2  # 检测时间间隔
                 weight 2    # 如果条件成立,权重+2
         }
         # 定义虚拟路由, VI_1 为虚拟路由的标示符,自己定义名称
         vrrp_instance VI_1 {
                 state BACKUP         # 默认主设备(priority 值大的)和备用设备(priority 值小的)都设置为 BACKUP,通常由priority 来控制同时启动情况下的默认主备,否则先启动的为主设备
                 interface eth4         # 绑定虚拟 IP 的网络接口,与本机 IP 地址所在的网络接口相同
                 virtual_router_id 35 # 虚拟路由的 ID 号,两个节点设置必须一样,可选 IP 最后一段使用,相同的 VRID 为一个组,他将决定多播的 MAC 地址
                 priority 99         # 节点优先级,值范围 0-254, MASTER 要比 BACKUP 高
                 advert_int 1         # 组播信息发送间隔,两个节点设置必须一样,默认 1s
                 # 设置验证信息,两个节点必须一致
                 authentication {
                         auth_type PASS
                         auth_pass 1111  
                 }
                 # 将 track_script 块加入 instance 配置块
                 track_script {
                         chk_haproxy   # 检查 HAProxy 是否正常运行
                 }
                 # 虚拟 IP 池, 两个节点设置必须一样
                 virtual_ipaddress {
                         192.168.1.10  # 虚拟 ip,可以定义多个,每行一个
                 }
         }

如果非抢占模式不生效, 在 Keepalived 的故障节点恢复后会再次导抢占vip,从而因 vip 切换而闪断带来的风险。
按以上配置,配置了 Keepalived 非抢占模式, 配置及注意点如下:
        (1) 主设备、 从设备中的 state 都设置为 BACKUP
        (2) 主设备、从设备中都不要配置 mcast_src_ip (本机 IP 地址)
        (3) 默认主设备(priority 值大的 Keepalived 节点) 配置一定要加上 nopreempt,否则非抢占不起作用
        (4) 防火墙配置允许组播(主、备两台设备上都需要配置, keepalived 使用 224.0.0.18作为 Master 和 Backup 健康检查的通信 IP)


 ③、配置检测 haproxy 状态的脚本
 /etc/keepalived/haproxy_check.sh
 创建一个日志,如果发生haproxy异常需要切换,记录到日志中。
 mkdir -p /usr/share/haproxy/log
 脚本如下:如果 haproxy 停止运行,尝试启动,如果无法启动则杀死本机的 keepalived 进程,keepalied 自动将虚拟 ip 绑定到 BACKUP 机器上。
         #!/bin/bash
         START_HAPROXY="/etc/rc.d/init.d/haproxy start"
         STOP_HAPROXY="/etc/rc.d/init.d/haproxy stop"
         LOG_FILE="/usr/share/haproxy/log/haproxy-check.log"
         HAPS=`ps -C haproxy --no-header |wc -l`
         date "+%Y-%m-%d %H:%M:%S" >> $LOG_FILE
         echo "check haproxy status" >> $LOG_FILE
         if [ $HAPS -eq 0 ];then
                 echo $START_HAPROXY >> $LOG_FILE
                 $START_HAPROXY >> $LOG_FILE 2>&1
                 sleep 3
                 if [ `ps -C haproxy --no-header |wc -l` -eq 0 ];then
                         echo "start haproxy failed, killall keepalived" >> $LOG_FILE
                         killall keepalived
                 fi
         fi
 保存赋权:
 chmod +x /etc/keepalived/haproxy_check.sh

 ④、启动 Keepalived
 停止: service keepalived stop
 启动: service keepalived start
 重启: service keepalived restart
 看状态: service keepalived status

7、测试Keepalived+HAProxy

两台服务器启动Keepalived+HAProxy,测试:

20200420192826

此时,keepalived,VIP是在108上,如果停止108的haproxy,如下效果:

20200420192848

停止108的haproxy,keepalived检测haproxy异常,并且重启haproxy,所以haproxy继续运行且日志中记录重启haproxy记录,这样就实现keepalived对haproxy高可用。

如果直接停止108keepalived,VIP飘到107:

20200421123303

如果把107的/usr/local/haproxy/conf/haproxy.cfg配置文件修改错误,让haproxy不能重启,停止haproxy后测试效果如下:

20200421123317

可以看到,VIP漂移到108,而且107也访问不了haproxy,并且107keepalived状态:keepalived dead but subsys locked。
日志中显示启动haproxy报错信息,和杀死keepalived进程信息。

20200421123334

8、测试HAProxy+Keepalived+MyCat+MySQL高可用集群

全部机器启动并启动相关服务测试,通过VIP访问到mysql数据库,对数据的操作,没有问题。
上一页1…161718…25下一页
初晨

初晨

永远不要说你知道本质,更别说真相了。

249 日志
46 分类
109 标签
近期文章
  • WebSocket、Socket、TCP、HTTP区别
  • Springboot项目的接口防刷
  • 深入理解Volatile关键字及其实现原理
  • 使用vscode搭建个人笔记环境
  • HBase介绍安装与操作
© 2018 — 2020 Copyright
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4