一、缓存的使用

  • 即时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)
    举例:电商应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的

二、高并发下缓存失效问题

  1. 缓存穿透
    查询一个不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无此记录,我们没有将这个此查询的null写入缓存,这将导致这个不存在的数据每次请求都到存储层查询,失去了缓存的意义。
    风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
    解决:null结果缓存,并加入短暂过期时间
  2. 缓存雪崩
    指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
    解决:原有失效时间的基础上增加一个随机值,比如1-5分钟随机,这样没有一个缓存的过期时间的重复率就会降低,就很难引发集体失效事件。
  3. 缓存击穿
    对于设置过期时间的key,如果这些key可能会在某些时间点被高并发地访问,是一种非常"热点"的数据。
    如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到DB,我们称为缓存击穿。
    解决:加锁,大量并发只让一个去查,其他人等待,查到后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去DB

三、本地锁

只要是同一把锁,就能锁住需要这把锁的所有线程,

1. synchronized(this),JUC(lock)

SpringBoot 所有的组件在容器中都是单例的,一个服务一个容器,一个容器一个实例,

每个this是不同的锁,

四、分布式锁

基本原理: 可以去一个地方占坑,占到就执行逻辑,否则等待,直到释放锁,"占坑"可以去redis,可以去数据库,可以去任何大家都能访问到的地方,等待可以自旋的方式。
问题

  1. setnx 占好了位,业务异常或者程序在页面过程中宕机,没有执行删除锁逻辑,就造成了死锁,可以通过设置锁的自动过期,即使没有删除,会自动删除,设置过期和加锁要原子操作
  2. 如果业务时间长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了,占锁的时候指定uuid,每个人匹配自己的锁才删除。删除之前判断是否是自己的

     String lockValue = stringRedisTemplate.opsForValue().get("lock");
     if (uuid.equals(lockValue)) {
         stringRedisTemplate.delete("lock");
     }
    

但这样还是有问题, 从redis取出值,返回的路上,redis过期了,别的线程加锁成功,这时判断为true,删除就删除了别的线程的锁,所以获取,判断,删除要是原子操作,通过lua脚本来实现。

Redisson

  • 阻塞式等待,默认加锁都是30s时间
  • 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期被删掉
  • 加锁业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30秒以后自动删除
  1. 读写锁
    保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁),读锁是一个共享锁
    写锁没释放读就必须等待
    读 + 读:相当于无锁
    写 + 读:等待写锁释放
    写 + 写:阻塞方式
    读 + 写:有读锁,写也需要等待
    只要有写的存在,都必须等待

    五、缓存一致性

    缓存中的数据如何与数据库保持一致(缓存数据一致性)

    1. 双写模式
      修改了数据库再查下,保存至缓存。两个线程修改同一条数据库,由于卡顿原因,导致写缓存2在最前,写缓存1在后面就出现不一致。
      脏数据问题:这时暂时性脏数据问题,但是在数据稳定,缓存过期后,又能得到最新的正确数据(最终一致性)
    2. 失效模式
      修改数据库,删除缓存

无论双写还是失效模式,都会导致缓存不一致问题,即多个实例同时更新会出事,怎么办?

  1. 如果是用户维度数据(订单数据、用户数据),这种并发几率小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读主动更新即可
  2. 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  3. 缓存数据+过期时间也足够解决大部分业务对于缓存的要求
  4. 通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓,所以适合读写锁(业务不关心脏数据,允许临时脏数据可忽略)

总结

  • 我们能放入缓存的数据本就不应该是实时性的,一致性要求超高的,所以缓存数据加上过期时间,保证每天拿到当前的最新数据即可
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高到的数据,就应该查数据库,即使慢点

binbin
37 声望3 粉丝

炎黄子孙,女娲后人,共产主义接班人


« 上一篇
golang反射