分布式缓存
参考:
zhuanlan.zhihu.com/p/408515044
zhuanlan.zhihu.com/p/347181685
1、基本定义
缓存作用
- 提高网站吞吐率,提高网站运行效率
- 缓存的存在是为了减轻数据库的压力
本地缓存和分布式缓存
- 本地缓存:存在应用服务器内存中的数据
- 分布式缓存:存储在当前应用服务器职位的数据库中
解决方案
- Redis等
- 存储的是较少修改的数据,比如行政规划
使用Maybatis自身结合redis实现分布式缓存
- mybatis 中应用级缓存(二级缓存)sqlsessionfactory级别缓存,所有会话共享
- 如何开启二级缓存。mapper.xml 文件 标签本地缓存。
- 本质是cache标签的实现,org.apache.ibatis.cache.impl.PerpetualCache。使用redistemplate实现增伤改查,但是redistemplate不是归工厂管理,不能直接注入;需要获得容器环境。
- 自定义Rediscache实现,
- 针对增删改查重写方法
- 查不需要清空redis数据,删改查必须要清空redis数据
- 如果项目中标查询之间没有任何关联采用这种缓存没有问题,但是如果涉及到关联查询,使用<cache-ref namspace=””>使用同一个命名空间
- MD5加密形成定长的key
2、存在的问题
缓存穿透(没有key)
问题: 传进来的key在Redis中是不存在的
方案:
1、把无效的Key存进Redis中
2、使用布隆过滤器。
缓存雪崩
问题:同一时间大规模的key失效;可能是采用了相同的过期时间
方案: redis key 随机; redis 集群,数据库集群/容灾
缓存击穿
问题: 热点key 失效
方案:热点的key可以设置永不过期的key
缓存一致性问题
背景:一旦涉及到写操作,操作数据/redis其中一个失误导致的问题,或者并发会导致数据一致性问题
3、一致性解决方案
刷新方案
1、先刷新redis,再更新数据库
- 如果后面刷新数据库失败。缓存失效后,导致后面读取数据库的都是旧值
- 如果发生高并发,并且两步都成功。两个线程写。比如线程 A 更新缓存(X = 1);线程 B 更新缓存(X = 2);线程 B 更新数据库(X = 2);线程 A 更新数据库(X = 1)。最终 X 的值在缓存中是2,在数据库中是 1,发生不一致。通过分布式锁来解决。但是问题是并不是每个更新的数据,都必须更新到redis中。如果强行一致性都导致性能浪费。
2、先更新数据库,再更新redis
- 如果后面更新redis失败。缓存失效前,导致一开始从redis读取的是旧值
- 如果发生高并发,并且两步都成功。同样推理可知,比如线程 A 更新数据库(X = 1);线程 B 更新数据库(X = 2);线程 B 更新缓存(X = 2);线程 A 更新缓存(X = 1)。最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。通过分布式锁来解决。但是问题是并不是每个更新的数据,都必须更新到redis中。如果强行一致性都导致性能浪费。
删除方案
1、先删除redis,再更新数据库【解决并发,延迟双删】
-
如果后面更新数据库失败。情况是一直读取的都是旧值,没有一致性问题,但是有更新的问题。
-
如果发生高并发,并且两步都成功。
场景:线程 A 要更新 X = 2(原值 X = 1),A更新,B读取。
步骤:
线程 A 先删除缓存 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1) 线程 A 将新值写入数据库(X = 2) 线程 B 将旧值写入缓存(X = 1) 最终:数据库X=2, 缓存X=1方案:延迟双删。在线程 A 删除缓存、更新完数据库之后,先「休眠一会」,再「删除」一次缓存。这样一来,下次就可以从数据库读取到最新值,写入缓存。但是没法估计时间。线程A延迟时间要大于线程 B 读取数据库 + 写入缓存的时间。
-
如果加入主从复制的场景。
2、先更新数据库,再删除redis。【解决第二步失败,消息队列】
-
如果后面删除redis失败。情况是缓存失效前,从redis读取的都是旧值,有一致性问题。
-
如果发生高并发,并且两步都成功。
场景:缓存中 X 不存在(数据库 X = 1),A读取,B更新。
线程 A 读取数据库,得到旧值(X = 1) 线程 B 更新数据库(X = 2) 线程 B 删除缓存 线程 A 将旧值写入缓存(X = 1) 最终:数据库X=2, 缓存X=1实际:缓存刚好已失效;读请求 + 写请求并发;更新数据库 + 删除缓存的时间,要比读数据库 + 写缓存时间短。因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。所以「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
但是解决了并发问题,需要解决第二步执行「失败」导致数据不一致的问题。解决方案是重新尝试。异步重试。实就是把重试请求写到「消息队列」中,然后由专门的消费者来重试,直到成功。或者更直接的做法,为了避免第二步执行失败,我们可以把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。(这里假设写入消息队列和操作缓存缓存完全没有问题)[下面实例1,2]
-
如果加入主从复制场景。A更新,B读取「先更新数据库,再删除缓存」
线程 A 更新主库 X = 2(原值 X = 1) 线程 A 删除缓存 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1) 从库「同步」完成(主从库 X = 2) 线程 B 将「旧值」写入缓存(X = 1) 最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。 解决方案:线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。线程A延迟时间要大于线程 B 读取数据库 + 写入缓存的时间。
总结,一旦用到缓存,就没法和数据库强一致性,这个涉及到性能问题。