使用缓存,可以 提升应用程序性能、
提高读取吞吐量(IOPS)、
消除数据库热点、
可预测的性能、
减少后端负载、
降低数据库成本
Redis 相关概念
1、缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
问题:缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
-
自身业务代码或者数据出现问题。
-
一些恶意攻击、 爬虫等造成大量空命中。
#### 解决方案
-
缓存空对象:当没有命中缓存,从数据库中查询数据为空后则缓存空对象,注意为了避免redis内存缓存空对象的浪费,需要为该空对象设置过期时间(过期时间能一定程度上解决频繁地用不存在的数据的Key来进行请求)。
-
布隆过滤器:、某个值存在时,这个值可能不存在;当不存在时,那就肯定不存在,布隆过滤器解决的问题是:如何准确快速的判断某个数据是否在大数据量集合中
2、缓存失效(击穿)
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。
解决方案
- 数据设置不同的缓存时间()
- 根据不同的缓存使用次数延长其过期时间(具体的实现呢?)
3、缓存雪崩
缓存雪崩:就是redis缓存直接挂掉了,请求穿过缓存直接到达数据库,最终导致数据库宕机,服务不可用
解决方案
-
保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
-
依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。
4、数据一致性
-
[1.方式一:先更新数据库,再更新缓存场景]
并发访问会出现数据不一致的问题
-
[2.方式二:先更新缓存,再更新数据库场景]
同方式一,并发访问出现数据不一致
-
[3.方式三:先删除缓存,再更新数据库的场景]
同方式一,并发访问出现数据不一致
-
[4.方式四:先更新数据库,在删除缓存场景]
并发访问可能会短暂出现数据不一致情况,但最终都会一致。推荐
-
[5.方式五:最佳实现,数据异步同步]
canal:基于数据库增量日志解析,提供增量数据订阅和消费
mysql会将操作记录在Binary log日志中,通过canal去监听数据库日志二进制文件,解析log日志,同步到redis中进行增删改操作。
canal的工作原理:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议;MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );canal 解析 binary log 对象(原始为 byte 流)。
5、缓存过期淘汰策略
1. Redis缓存淘汰策略工作流程
- 首先,客户端会发起需要更多内存的申请;
- 其次,Redis检查内存使用情况,如果实际使用内存已经超出maxmemory,Redis就会根据用户配置的淘汰策略选出无用的key;
- 最后,确认选中数据没有问题,成功执行淘汰任务。
2. Redis3.0版本支持淘汰策略有6种
- no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
7、redis应用场景
(1)热点数据的缓存
这个应用场景我们比较常见的使用方式,为了降低对数据库的访问,会将对应数据添加到缓存中,提供并发访问的能力,从而提高系统吞吐量。
(2)限时业务的运用
-
验证码,二维码生存周期
手机号(唯一标识) 生成的验证码、二维码信息保存在redis中指定过期时间(如果用户输入后 redis中的验证码过期 需要重新输入),在一定时间内如果redis有信息,用户频繁获取则将该信息直接返回并不调用真实获取验证码,二维码的接口。
-
接口api防刷,订单重复提交问题
ip+api接口 作为key存储并设置过期时间,value为请求次数 如果请求次数到达阈值则禁止请求。
订单重复提交类似
(3)计数器相关问题
文章的点赞数、页面的浏览数、网站的访客数、视频的播放数这些数据增长量很快,一旦数据规模上来后,对 mysql 读写都有很大的压力,这时就要考虑 memcache、redis 进行存储或 cache,同时定时同步到DB层。
(4)排行榜相关问题
对于千万级别的数据、大量并发情况下,基于redis可靠的读写请求以及其zset数据结构 可以考虑使用redis来实现相关排行榜功能。新增数据后并在zset中添加数据(需要考虑排行榜的维度作为score)。
(5)分布式锁
redis是一个分布式存储系统,同时其setNx命令是阻塞的(key存在则设置不成功) 可以很好的用来进行分布式的锁操作处理,从而实现 秒杀、模拟抢单、抢红包等关键资源的并发场景下的有序访问。
(6)延时操作
- 订单超过 30 分钟未支付,则自动取消。
- 外卖商家超时未接单,则自动取消。
- 医生抢单电话点诊,超过 30 分钟未打电话,则自动退款
针对如上场景 :我们可以使用 zset(sortedset)这个命令,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 …命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。也可以通过 zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费。
(7) 队列
redis支持list数据结果 使用LPUSH 和RPUSH、LPOP和RPOP可以很轻松的实现栈、队列等数据结构
(8) 分布式应用session(redis实现)
redis是分布式存储且支持读写很快,所有用户登录后的相关用户信息可以保存到redis中便于在后续分布式应用中进行使用。
8、redis高级用法
创建redis连接
@BeforeEach
public void createMasterSlaveClient(){
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
//timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
jedisPool = new JedisPool(config, "IP", port,3000, "password");
}
Pipeline管道的使用
-
首先Redis的管道(pipeline)并不是Redis服务端提供的功能,而是Redis客户端为了减少网络交互而提供的一种功能。
-
pipeline主要就是将多个请求合并,进行一次提交给Redis服务器,Redis服务器将所有请求处理完成之后,再一次性返回给客户端。
-
pipeline执行的操作,和mget,mset,hmget这样的操作不同,pipeline的操作是不具备原子性的。还有在集群模式下因为数据是被分散在不同的slot里面的,因此在进行批量操作的时候,不能保证操作的数据都在同一台服务器的slot上,所以集群模式下是禁止执行像mget、mset、pipeline等批量操作的,如果非要使用批量操作,需要自己维护key与slot的关系。
-
pipeline也不能保证批量操作中有命令执行失败了而中断,也不能让下一个指令依赖上一个指令, 如果非要这样的复杂逻辑,建议使用lua脚本来完成操作。
@Test
public void testPipline(){
Jedis client = jedisPool.getResource();
Pipeline pipeline = client.pipelined();
Map<String,Response> responseMap = new HashMap<>();
//字符串操作
responseMap.put("name", pipeline.set("name","张三"));
responseMap.put("age",pipeline.set("age","28"));
//list列表操作
pipeline.lpush("worker","张三","李四","王五","赵六");
//map集合操作
Map<String,String> bookMap = new HashMap<>();
bookMap.put("web","web技术书籍");
bookMap.put("java","java技术数据");
bookMap.put("h5","h5页面学习");
responseMap.put("bookInfo",pipeline.hset("bookInfo", bookMap));
//set操作
responseMap.put("score",pipeline.sadd("score","1","2","3","4"));
//sort Set集合操作
responseMap.put("rankList",pipeline.zadd("rankList",100,"张三"));
responseMap.put("rankList1",pipeline.zadd("rankList",99,"李四"));