缓存问题
- 缓存穿透
高并发访问数据库不存在的值,导致缓存失效
解决: 给无结果的key缓存 标志位表示无值(0,1 之类的),并添加短期失效时间
- 缓存雪崩
高并发访问时,正好大批量的key失效
解决:添加失效时间间隔值
– 缓存击穿
对于热点key突然失效时的高并发访问
解决:加锁
Redis 分布式锁
- 使用redis setnx 实现的一种分布式锁, 必须保证加锁解锁的原子性
场景:
- 使用redis 的 setnx 时候, 需要设置过期时间,但是过期时间设置时候闪断怎么办?
- 让设置值的命令和设置过期时间的命令在同一次操作中实现, setnx key value ex xx
- A线程删了B线程的锁怎么办?
- 回查当前redis种占用锁的线程id(set时候当value传入了),与当线程对比后再删
- 问题2中,如果代码执行到匹配正确然后要删锁时,锁过期了并且同时被其他线程抢到了,这时候又删到了别人的key
- 使用redis 脚本来执行删key操作, 脚本实际时比对当前value和传入的value是否一直,是才值删锁。
if redis.call("get",KEY[1]) == ARGV[1] then return redis.call("del",KEY[1]) else return 0 end
- 如果业务执行时间长,锁先过期了,如何设计这个锁续期呢
- 简单处理是将过期时间延长,必须执行手动删锁。
- 详细可以看redisson中的自动续期 (如果设置了过期时间,看门狗失效)
redisson
-
读写锁 (redis中有记录)
- 写 + 读: 等写结束才能读
- 写 + 写: 阻塞
- 读 + 写: 有读锁,写锁需要等
- 读 + 读: 无锁模式
-
countdown latch
- 门闩锁: 用于某个特定业务必须多个业务必须都操作完才能执行
-
信号量锁
- 信号量锁:用于控制某个业务执行的次数,如停车场车位,入库则减1,但校验时候需要判断有无信号量了。
- 也可用为限流工具
缓存一致性问题
-
双写
- 改完数据库,改缓存
- 问题: ABA问题(A线程写完时,B线程把旧数据又放入缓存了))
- 解决:加读写锁,或者业务允许 可以考虑过期时间 -> 最终一致性
-
失效模式
- 改完数据库,删除缓存
- 问题: ABA问题(在A线程删除缓存时,B线程把旧数据又放入缓存了)
- 解决:加读写锁,或者业务允许 可以考虑过期时间 -> 最终一致性
-
最终解决方案
- 常用并且一致性要求高的数据,可以直接使用数据库
- 如果使用canal订阅binlog,canal会伪装成从库,然后把数据更新到redis,业务代码就可以不处理更新redis逻辑了。 (又引入了中间件)
- 延时双删
spring-cache
- 默认用本地map当成缓存
- redis (CacheAutoConfiguration -> RedisCacheConfiguration -> RedisCacheManger -> 初始化所有缓存)
- 使用redis时,需要在配置中申明使用redis做缓存
- 开启缓存功能 @EnableCaching
- @Cacheable // 缓存当前结果,下次执行会先找缓存
- names: 存入的分区
- key: redis中的key,使用"‘xxx’" -> 默认不加’'会去解析成SEPL语言
- @CacheEvict // 删除缓存数据
- value: 分区名称
- key: redis的key
- allEntries: true时 删除分区所有数据
- @Caching // 多个缓存操作
- evict: @CacheEvict 集合
- ttl设置:只能配置文件中设置全局 (ms单位)
- 自定义序列化:默认是jdk序列化,不友好
- 注入一个自定义配置的 RedisCacheConfiguration
- 使用了自定义的,配置文件的就失效了,不过可以从容器中获取再注入