分布式问题03__分布式缓存

273 阅读6分钟

分布式缓存

参考:

zhuanlan.zhihu.com/p/408515044

zhuanlan.zhihu.com/p/347181685

1、基本定义

缓存作用

  • 提高网站吞吐率,提高网站运行效率
  • 缓存的存在是为了减轻数据库的压力

本地缓存和分布式缓存

  • 本地缓存:存在应用服务器内存中的数据
  • 分布式缓存:存储在当前应用服务器职位的数据库中

解决方案

  1. Redis等
  2. 存储的是较少修改的数据,比如行政规划

使用Maybatis自身结合redis实现分布式缓存

  1. mybatis 中应用级缓存(二级缓存)sqlsessionfactory级别缓存,所有会话共享
  2. 如何开启二级缓存。mapper.xml 文件 标签本地缓存。
  3. 本质是cache标签的实现,org.apache.ibatis.cache.impl.PerpetualCache。使用redistemplate实现增伤改查,但是redistemplate不是归工厂管理,不能直接注入;需要获得容器环境。
  4. 自定义Rediscache实现,
  • 针对增删改查重写方法
  • 查不需要清空redis数据,删改查必须要清空redis数据
  • 如果项目中标查询之间没有任何关联采用这种缓存没有问题,但是如果涉及到关联查询,使用<cache-ref  namspace=””>使用同一个命名空间
  1. 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]

image.png

image.png

 

  • 如果加入主从复制场景。A更新,B读取「先更新数据库,再删除缓存」

      线程 A 更新主库 X = 2(原值 X = 1)
    
      线程 A 删除缓存
    
      线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
    
      从库「同步」完成(主从库 X = 2)
    
      线程 B 将「旧值」写入缓存(X = 1)
    
      最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。
    
      解决方案:线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。线程A延迟时间要大于线程 B 读取数据库 + 写入缓存的时间。
    

总结,一旦用到缓存,就没法和数据库强一致性,这个涉及到性能问题。