一、缓存的应用
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少)
举例:电商利用,商品分类,商品列表等适宜缓存并加一个生效工夫(依据数据更新频率来定),后盾如果公布一个商品,买家须要 5 分钟能力看到新的商品个别还是能够承受的
二、高并发下缓存生效问题
- 缓存穿透
查问一个不存在的数据,因为缓存是不命中的,将去查询数据库,然而数据库也无此记录,咱们没有将这个此查问的 null 写入缓存,这将导致这个不存在的数据每次申请都到存储层查问,失去了缓存的意义。
危险:利用不存在的数据进行攻打,数据库刹时压力增大,最终导致解体
解决:null 后果缓存,并退出短暂过期工夫 - 缓存雪崩
指在咱们设置缓存时 key 采纳了雷同的过期工夫,导致缓存在某一时刻同时生效,申请全副转发到 DB,DB 刹时压力过重雪崩。
解决:原有生效工夫的根底上减少一个随机值,比方 1 - 5 分钟随机,这样没有一个缓存的过期工夫的反复率就会升高,就很难引发个体生效事件。 - 缓存击穿
对于设置过期工夫的 key,如果这些 key 可能会在某些工夫点被高并发地拜访,是一种十分 ” 热点 ” 的数据。
如果这个 key 在大量申请同时进来前正好生效,那么所有对这个 key 的数据查问都落到 DB,咱们称为缓存击穿。
解决:加锁,大量并发只让一个去查,其他人期待,查到后开释锁,其他人获取到锁,先查缓存,就会有数据,不必去 DB
三、本地锁
只有是同一把锁,就能锁住须要这把锁的所有线程,
1. synchronized(this),JUC(lock)
SpringBoot 所有的组件在容器中都是单例的,一个服务一个容器,一个容器一个实例,
每个 this 是不同的锁,
四、分布式锁
基本原理: 能够去一个中央占坑,占到就执行逻辑,否则期待,直到开释锁,” 占坑 ” 能够去 redis,能够去数据库,能够去任何大家都能拜访到的中央,期待能够自旋的形式。
问题
- setnx 占好了位,业务异样或者程序在页面过程中宕机,没有执行删除锁逻辑,就造成了死锁,能够通过设置锁的主动过期,即便没有删除,会主动删除,设置过期和加锁要原子操作
-
如果业务工夫长,锁本人过期了,咱们间接删除,有可能把他人正在持有的锁删除了,占锁的时候指定 uuid,每个人匹配本人的锁才删除。删除之前判断是否是本人的
String lockValue = stringRedisTemplate.opsForValue().get("lock"); if (uuid.equals(lockValue)) {stringRedisTemplate.delete("lock"); }
但这样还是有问题,从 redis 取出值,返回的路上,redis 过期了,别的线程加锁胜利,这时判断为 true,删除就删除了别的线程的锁,所以获取,判断,删除要是原子操作,通过 lua 脚本来实现。
Redisson
- 阻塞式期待,默认加锁都是 30s 工夫
- 锁的主动续期,如果业务超长,运行期间主动给锁续上新的 30s, 不必放心业务工夫长,锁主动过期被删掉
- 加锁业务只有运行实现,就不会给以后锁续期,即便不手动解锁,锁默认在 30 秒当前主动删除
-
读写锁
保障肯定能读到最新数据,批改期间,写锁是一个排他锁(互斥锁、独享锁), 读锁是一个共享锁
写锁没开释读就必须期待
读 + 读:相当于无锁
写 + 读:期待写锁开释
写 + 写:阻塞形式
读 + 写:有读锁,写也须要期待
只有有写的存在,都必须期待五、缓存一致性
缓存中的数据如何与数据库保持一致(缓存数据一致性)
- 双写模式
批改了数据库再查下,保留至缓存。两个线程批改同一条数据库,因为卡顿起因,导致写缓存 2 在最前,写缓存 1 在前面就呈现不统一。
脏数据问题:这时暂时性脏数据问题,然而在数据稳固,缓存过期后,又能失去最新的正确数据(最终一致性) - 生效模式
批改数据库,删除缓存
- 双写模式
无论双写还是生效模式,都会导致缓存不统一问题,即多个实例同时更新会出事,怎么办?
- 如果是用户维度数据(订单数据、用户数据),这种并发几率小,不必思考这个问题,缓存数据加上过期工夫,每隔一段时间触发读被动更新即可
- 如果是菜单,商品介绍等根底数据,也能够去应用 canal 订阅 binlog 的形式。
- 缓存数据 + 过期工夫也足够解决大部分业务对于缓存的要求
- 通过加锁保障并发读写,写写的时候按程序排好队,读读无所谓,所以适宜读写锁(业务不关心脏数据,容许长期脏数据可疏忽)
总结
- 咱们能放入缓存的数据本就不应该是实时性的,一致性要求超高的,所以缓存数据加上过期工夫,保障每天拿到以后的最新数据即可
- 咱们不应该适度设计,减少零碎的复杂性
- 遇到实时性、一致性要求高到的数据,就应该查数据库,即便慢点