Redis 应用场景
- 利用Redis中字符串类型完成项目中手机验证码存储的实现。
- 利用Redis中字符串类型完成具有时效性业务功能(12306,淘宝等的订单超时时间)。
- 利用Redis分布式集群系统中的Session共享。
- 利用Redis的可排序set类型完成排行榜之类的功能。
- 利用Redis实现分布式缓存。
- 利用Redis存储认证之后的token信息(微信小程序,微信公众号)。
- 利用Redis解决分布式集群系统中分布式问题。
1. Redis 中分布式缓存实现
-
什么是缓存(Cache)
定义:就是计算机内存中一段数据。
-
内存中数据特点
读写快,断电立即丢失。
-
缓存解决了什么问题
提高网站吞吐量,提高网站运行效率。
核心解决的问题:缓存的存在是用来减轻数据库访问压力。
-
既然缓存能提高效率,项目里所有数据加入缓存不是更好?
使用缓存时一定是数据库中数据极少发生修改,更多用于查询这种情况。
-
本地缓存和分布式缓存的区别
本地缓存:存在应用服务器内存中数据称之为本地缓存(local cache)。
分布式缓存:存储在当前应用服务器内存之外的数据称之为分布式缓存(distribute cache)。
分布式系统:由多个不同服务集群共同对系统提供服务,这个系统称之为分布式系统。
-
利用Mybatis自身本地缓存结合Redis实现分布式缓存
Mybatis中应用级缓存(二级缓存)。SqlSessionFactory级别缓存,所有会话共享。
开启二级缓存:在mapper.xml中加入标签,即为本地缓存。
查看Cache标签的缓存实现,发现Mybatis底层默认使用的是 org.apache.ibatis.cache.impl.PerpetualCache。
-
自定义RedisCache实现
定义工具类:
package com.whk.util; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; // 用来获取springboot创建好的工厂 @Component public class ApplicationContextUtils implements ApplicationContextAware { // 保留下来工厂 private static ApplicationContext applicationContext; // 将创建好工厂以参数形式传递给这个类 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } // 提供在工厂中获取对象的方法 //RedisTemplate redisTemplate public static Object getBean(String beanName){ return applicationContext.getBean(beanName); } }
通过Mybatis默认cache源码得知,可以使用自定义Cache类,implements Cache接口,并对里面方法进行实现。
package com.whk.cache; import com.whk.util.ApplicationContextUtils; import org.apache.ibatis.cache.Cache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.util.DigestUtils; import java.util.concurrent.TimeUnit; // 自定义Redis缓存实现 public class RedisCache implements Cache { // 当前放入缓存的mapper的namespace private final String id; // 必须存在构造方法 public RedisCache(String id) { System.out.println("id:=====================> " + id); this.id = id; } // 返回cache唯一标识 @Override public String getId() { return this.id; } // 缓存放入值 redis RedisTemplate StringRedisTemplate @Override public void putObject(Object key, Object value) { System.out.println("key:" + key.toString()); System.out.println("value:" + value); // // 通过application工具类获取redisTemplate // RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); // redisTemplate.setKeySerializer(new StringRedisSerializer()); // redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 使用redishash类型作为缓存存储模型 key hashkey value getRedisTemplate().opsForHash().put(id.toString(),key.toString(),value); if(id.equals("com.whk.dao.UserDAO")){ //缓存超时 client 用户 client 员工 getRedisTemplate().expire(id.toString(),1, TimeUnit.HOURS); } if(id.equals("com.whk.dao.CityDAO")){ //缓存超时 client 用户 client 员工 getRedisTemplate().expire(id.toString(),30, TimeUnit.MINUTES); } //.....指定不同业务模块设置不同缓存超时时间 } // 获取中获取数据 @Override public Object getObject(Object key) { System.out.println("key:" + key.toString()); // //通过application工具类获取redisTemplate // RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); // redisTemplate.setKeySerializer(new StringRedisSerializer()); // redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 根据key 从redis的hash类型中获取数据 return getRedisTemplate().opsForHash().get(id.toString(), key.toString()); } // 注意:这个方法为mybatis保留方法 默认没有实现 后续版本可能会实现 @Override public Object removeObject(Object key) { System.out.println("根据指定key删除缓存"); return null; } @Override public void clear() { System.out.println("清空缓存~~~"); //清空namespace getRedisTemplate().delete(id.toString());//清空缓存 } //用来计算缓存数量 @Override public int getSize() { //获取hash中key value数量 return getRedisTemplate().opsForHash().size(id.toString()).intValue(); } // 封装redisTemplate private RedisTemplate getRedisTemplate(){ // 通过application工具类获取redisTemplate RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
使用RedisCache实现,在mapper.xml中
<cache type="xxxx.RedisCache"/>
。 -
产生的问题
如果项目中表查询之间没有任何关联查询,使用现在的这种缓存方式没有任何问题,但当在表连接查询过程中就一定存在问题(Mybatis执行增删改会自动清空缓存)。
如何解决关联关系时更新缓存信息的问题:
<cache-ref /> //用来将多个具有关联关系查询缓存放在一起处理
-
缓存优化策略
对放入redis中key进行优化:key的长度不能太长,尽可能将key设计简洁一些。
算法:MD5处理。
特点:- 一切文件字符串等经过MD5处理之后,都会生成32位16进制字符串。
- 不同内容文件经过MD5进行加密,加密结果一定不一致。
- 相同内容文件多次经过MD5加密,生成结果始终一致。
推荐在redis整合Mybatis过程中建议将key进行MD5优化处理。
// 封装一个对key进行md5处理方法 private String getKeyToMD5(String key){ return DigestUtils.md5DigestAsHex(key.getBytes()); }
-
相关概念
缓存穿透/缓存击穿:
定义:客户端查询了一个数据库中没有的数据记录导致缓存在这种情况下无法利用,称之为缓存穿透或缓存击穿。
Mybatis中Cache解决了缓存穿透:将数据库中没有查询到的结果也进行缓存。缓存雪崩:
定义:在系统运行的某一时刻,突然系统中缓存全部失效,恰好在这一时刻涌来大量客户端请求,导致所有模块缓存无法利用,大量请求涌向数据库导致极端情况,数据库阻塞或挂起。
缓存存储时业务系统非常大,模块多,业务数据不同,不同模块在放入缓存时,都会设置一个缓存超时时间。
解决方案:- 缓存永久存储(不推荐)。
- 针对于不同业务数据设置不同的缓存超时时间。
if(id.equals("com.whk.dao.UserDAO")){ //缓存超时 client 用户 client 员工 getRedisTemplate().expire(id.toString(),1, TimeUnit.HOURS); } if(id.equals("com.whk.dao.CityDAO")){ //缓存超时 client 用户 client 员工 getRedisTemplate().expire(id.toString(),30, TimeUnit.MINUTES); } //.....指定不同业务模块设置不同缓存超时时间
2. Redis 主从复制(并不常用)
2.1 主从复制
主从复制架构仅仅用来解决数据的冗余备份,从节点仅仅用来同步数据,只允许读操作,不允许写操作(可以进行修改,一般不建议),该架构不具备自动故障转移功能。
2.2 主从复制架构图
2.3 搭建主从复制(伪分布式)
[root@localhost ~]# mkdir master slave1 slave2
[root@localhost ~]# cp redis-4.0.10/redis.conf master/
[root@localhost ~]# cp redis-4.0.10/redis.conf slave1/
[root@localhost ~]# cp redis-4.0.10/redis.conf slave2/
[root@localhost ~]# vim master/redis.conf
[root@localhost ~]# vim slave1/redis.conf
[root@localhost ~]# vim slave2/redis.conf
修改配置为:
- master
port 7000
bind 0.0.0.0
- slave1
port 7001
bind 0.0.0.0
slaveof masterip masterport
- slave2
port 7002
bind 0.0.0.0
slaveof masterip masterport
启动并进行测试:
[root@localhost ~]# cd /usr/redis/bin
[root@localhost bin]# ./redis-server /root/master/redis.conf
[root@localhost bin]# ./redis-server /root/slave1/redis.conf
[root@localhost bin]# ./redis-server /root/slave2/redis.conf
3. Redis 哨兵机制
3.1 哨兵Sentinel机制
Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。简单的说哨兵就是带有自动故障转移功能的主从架构。
无法解决:
- 单节点并发压力问题。
- 单节点内存和磁盘物理上限。
3.2 哨兵架构原理
3.3 搭建哨兵架构
-
在主节点上创建哨兵配置。
在Master对应redis.conf同目录下新建sentinel.conf文件,名字绝对不能错。 -
配置哨兵,在sentinel.conf文件中填入内容:
sentinel monitor 被监控数据库名字(自己起名字) ip port 1
-
将根目录下的redis源码目录下的src目录中的redis-sentinel脚本复制到/usr/redis/bin目录下。
-
启动哨兵模式进行测试。
redis-sentinel /root/sentinel/sentinel.conf
说明:这个后面的数字2,是指当有两个及以上的sentinel服务检测到master宕机,才会去执行主从切换的功能。
3.4 Spring Boot 操作哨兵
在配置文件中做如下配置:
# redis sentinel 配置
# master书写是使用哨兵监听的那个名称
spring.redis.sentinel.master=自己起的名字
# 连接的不再是一个具体redis主机,书写的是多个哨兵节点
spring.redis.sentinel.nodes=192.168.202.206:26379
注意:在哨兵的配置文件中也需要加入bind 0.0.0.0
开启远程连接权限。
4. Redis 集群
4.1 集群
Redis在3.0后开始支持Cluster(集群)模式,目前Redis的集群支持节点的自动发现,支持slave-master选举和容错,支持在线分片(sharding shard)等特性。
4.2 集群架构图
4.3 集群细节
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
- 节点的fail是通过集群中超过半数的节点检测失效时才生效。
- 客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
- redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value。
CRC16算法特点:
- 对集群模式下的所有key进行CRC16计算,计算的结果始终在0-16383之间。
- 对客户端的key进行CRC16计算时同一个key多次经过CRC16计算结果始终一致。
- 对客户端的不同key进行CRC16计算,计算结果,不同key的结果会出现一致。
5. Redis 实现分布式 Session 管理
5.1 管理机制
Redis的session管理是利用spring提供的session管理解决方案,将一个应用session交给Redis存储,整个应用中所有session的请求都会去Redis中获取对应的session数据。
5.2 开发 Session 管理
-
引入依赖:
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
-
开发Session管理配置类:
@Configuration @EnableRedisHttpSession public class RedisSessionManager { }
注意:使用Redis的session管理,当session中数据发生变化时必须将session中变化的数据同步到Redis中。