为什么要用缓存?
适用互联网高并发,高性能场景,解决mysql磁盘慢问题
在项目中缓存是如何使用的?
- 走springboot @CacheConfig
查询:先查缓存,有返回,没有查数据库,set到缓存返回。
修改:先删db,后删redis - 单独业务使用
db,缓存一致性问题,缓存竞争后面讨论解决方案
缓存使用有哪些分类,达成的效果是?
- 分页缓存(查询条件当key,时间控制业务可以接受方位)
- 数据库行数据缓存(只缓存有用不易改动的列)
- 业务缓存(通过特定业务特殊设置,特殊淘汰)
整个系统大部分查询全走缓存。冷热数据动态设置,淘汰,最大化利用缓存。
缓存使用不当会造成什么后果?
缓存雪崩
是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机
解决方案:
- 冷热数据区分,采用不同的实效时间,缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布。
- 设置热点数据永远不过期。
缓存穿透
是指缓存和数据库中都没有的数据,而用户不断发起请求,缓存没有起到压力缓冲的作用
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以- 防止攻击用户反复用同一个id暴力攻击
- 针对key做布隆过滤器
- 采用灰度发布的方式,先接入少量请求,再逐步增加系统的请求数量,直到全部请求都切换完成
- 提前缓存预热或定时预热
缓存击穿
是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据(sql又慢),引起数据库压力瞬间增大,造成过大压力。缓存失效时瞬时的并发打到数据库
解决方案:
- 设置热点数据永远不过期
- 针对key做加互斥锁 解决-第一次缓存大并发问题 单jvm级别加双重锁double-check, (使用分布式锁会限制并发能力,所以使用单jvm级别限制,特殊场景支付除外)
全量缓存
在处理超大规模并发的场景时,由于并发请求的数量非常大,即使少量的缓存穿透,也有可能打死数据库引发雪崩效应。
解决方案:
- 对于这种情况,我们可以缓存全量数据来彻底避免缓存穿透问题
canal订阅binlog异步更新缓存
缓存并发竞争
多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了
- 如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。 - 如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA-->valueB-->valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
- 利用队列,将set方法变成串行访问也可以
缓存与数据库双写不一致,数据一致性问题
Cache Aside Pattern 旁路缓存
- 读请求:先读缓存,如果没有命中,读数据库,再set回缓存
- 写请求:先删缓存,再删数据库,
为什么建议淘汰缓存,不修改缓存
在1和2两个并发写发生时,由于无法保证时序,此时不管先操作缓存还是先操作数据库,都可能出现:
- 请求1先操作数据库,请求2后操作数据库
- 请求2先set了缓存,请求1后set了缓存
导致,数据库与缓存之间的数据不一致。
Cache Aside Pattern问题
Cache Aside 在高并发场景下也会出现数据不一致。
读操作A,没有命中缓存,就会到数据库中取数据v1。
此时来了一个写操作B,将v2写入数据库,让缓存失效;
读操作A在把v1放入缓存,这样就会造成脏数据。因为缓存中是v1,数据库中是v2
解决方案:
- b线程:读缓存->未命中->上写锁>从db读数据到缓存->释放锁;a线程:上写锁->写db->删除缓存/改缓存->释放锁;
- 看业务方能接受多长时间的脏数据,然后缓存就设置多久的过期时间。
或者数据库更新成功后,用MQ去通知刷新缓存
canal订阅binlog,终极方案-还可以解决主从库同步问题
- 降级或补偿方案或兜底方案
redis基础
5.0.7 内存,单线程,c语言,io多路复用
持久方式:
rdb,aof,混合模式
一分钟内修改1W次
5分钟内修改10次
15分钟内修改1次
淘汰机制:
volatile-lru:从设置了过期时间的数据集中,选择最近最久未使用的数据释放;
allkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放;
volatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放;
allkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放;
volatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作;
noeviction:不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。
allkeys-lru,针对所有 Key,优先删除最近最少使用的 Key;
volatile-lru,针对带有过期时间的 Key,优先删除最近最少使用的 Key;
volatile-ttl,针对带有过期时间的 Key,优先删除即将过期的 Key(根据 TTL 的值);
allkeys-lfu(Redis 4.0 以上),针对所有 Key,优先删除最少使用的 Key;
volatile-lfu(Redis 4.0 以上),针对带有过期时间的 Key,优先删除最少使用的 Key。
范围有 allkeys 和 volatile两种,算法有 LRU、TTL 和 LFU 三种,建议volatile-lfu
redis有哪几种数据类型,在项目中分别在哪里使用了?
string
计数器,点赞数、收藏数、分享数,token
hash
圈子id uid:lastUpdateTime(最后发布时间) --点赞,发帖,回复频率控制
userId:name:张三;age:13 -- 用户信息存储,修改
list
用户粉丝列表, 用户点赞列表, 用户收藏列表, 用户关注列表
lrange命令,并发大的最新200评论分页常驻内存
消息队列
set
唯一无序 共同关注的书 | 互相关注 | 粉丝
是否互关去重就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能
zset
唯一有序score,群积分排名晋级管理员 | 书排行榜 | top-n原理跳表
stream
代替之前基于list的发布订阅
redis集群方案
一主一备,一主多从,哨兵模式,
类codis,redis cluster,旁路探活
并发海量建议旁路探活式的集群方式
分布式锁
单jvm级别
与synchronized关键字相比,Lock的使用更灵活,可以有加锁超时时间、公平性等优势
单机redis分布锁
set key value EX 60 NX 超时时间根据业务设计
setnx SET if Not eXists 1:获取锁 0:继续尝试获取(重试次数+休息时间)
ex过期时间防止锁一直被占用
三个请求A,B,C同时竞争锁,被请求A抢先获得,其他请求只能不断尝试获取锁(tryLock)
请求A由于业务比较复杂处理时间已经超时,所以请求B能够获取到锁
请求A终于完成了自己的业务,这个时候执行了DEL user_id,但是他自己的锁已经失效了,删除的是请求B锁。而请求B的业务此时并未处理完,所以此处就出现了问题!
通过value来判断这把锁是否属于自己
SET user_id 10086 EX 30 NX
// 处理业务中
//业务处理完毕
if( (GET user_id) == "XXX" ){
DEL user_id
}
if( (GET user_id) == "XXX" ){ //获取到自己锁后,进行取值判断且判断为真。此时,这把锁恰好失效。而另外一个请求恰好获得key值为user_id的锁。
此时程序执行了了DEL user_id,删除了别人加的锁
DEL user_id
}
保证查询和删除的原子性操作,需要引入lua脚本支持
eval()方法可以确保原子性?源于Redis的特性,因为Redis是单线程,在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行
加锁时 key 同,value 不同。
释放锁时,根据value判断,是不是我的锁,不能释放别人的锁。
及时释放锁,而不是利用自动超时
锁超时时间一定要结合业务情况权衡,过长,过短都不行。
程序异常之处,要捕获,并释放锁。如果需要回滚的,主动做回滚、补偿。保证整体的健壮性,一致性
key超时解决:弄个守护线程进行监听key的失效时间,然后在快要失效的时候为期续命
集群redis分布式锁
命令先是落到了主库。假设这时主库down了,而这条数据还没来得及同步到从库,
sentinel将从库中的一台选举为主库了。这时,我们的新主库中并没有mykey这条数据,若此时另外一个client执行 setnx mykey hisvalue , 也会成功,即也能得到锁。这就意味着,此时有两个client获得了锁
为了解决故障转移情况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,需要多个redis实例,加锁的时候,它会想多半节点发送 setex mykey myvalue 命令,只要过半节点成功了,那么就算加锁成功了。释放锁的时候需要想所有节点发送del命令。这是一种基于【大多数都同意】的一种机制
多级缓存实现
nginx+lua,jvm,ZooKeeper(监听修改变动),redis集群
总结:
- 锁的颗粒度,范围尽量要小。推荐无锁编程(解决热点冲突),热点账户分而治之
- 缓存数据允许丢失
- 遵守缓存使用开发规范
参考:
- https://www.cnblogs.com/rjzheng/p/9041659.html#!comments
- https://blog.csdn.net/z50L2O08e2u4afToR9A/article/details/81024553
- https://www.pianshen.com/article/1706860918/
- https://www.jianshu.com/p/d00348a9eb3b
- https://www.cnblogs.com/qdhxhz/p/11046905.html
- https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
- https://github.com/liyue2008/canal-to-redis-example