✅P166_缓存-分布式锁-缓存一致性解决

gong_yz大约 4 分钟谷粒商城

缓存一致性解决


初始方案

    /**
     * 使用RedissonLock解决缓存击穿
     *
     * @return
     */
    private Map<Long, List<Catelog2Vo>> getCategoryJsonWithRedissonLock() {
        // 锁的粒度,越细越快
        RLock rLock = redissonClient.getReadWriteLock("categoryJsonLock").readLock();
        Map<Long, List<Catelog2Vo>> categoryMap;
        try{
            rLock.lock();
            categoryMap = getCategoryJsonFromDB();
        } finally {
            rLock.unlock();
        }
        return categoryMap;
    }

双写模式

数据库改过的值,到我们最终看到的值,中间有一个比较大的延迟时间,无论怎么延迟,最终都会看到数据库最新修改的值。


失效模式

1号机器写数据,删缓存

2号机器,它想把1号数据改成2,但是它操作比较慢,花的时间比较长,

1号机器刚删除完缓存,

3号机器就进来了,它去读缓存,结果发现没数据,然后就去读数据库,此时因为2号请求还没改完数据库,所以3号请求读到了1号机器写入的数据,

然后,3号机器要更新缓存了,如果它执行的快还好,顶多刚更新,然后碰上2号机器删缓存,相当于什么也没干,缓存没更新

如果它执行的慢,让2号机器把数据写完,再把缓存删了,那就没有人能阻拦3号机器更新缓存了,最终会把1号机器的数据更新到缓存,导致缓存不一致。


解决方案

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

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

总结

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

Canal

是阿里开源的一个中间件,可以模拟成数据库的从服务器。

比如说MySQL有一个数据库,如果我们装了Canal,Canal就会将自己伪装成MySQL的从服务器,

从服务器的特点就是,MySQL里面只要有什么变化,它都会同步过来

正好利用Canal的这个特性,只要我们的业务代码更新了数据库,我们的MySQL数据库肯定得开启Binlog(二进制日志),这个日志里面有MySQL每一次的更新变化,Canal就假装成MySQL的从库,把MySQL每一次的更新变化都拿过来,相当于MySQL只要有更新,Canal就知道了。

比如说它看到MySQL分类数据更新了,那它就去把Redis里边所有跟分类有关的数据更新了就行

用这种的好处就是,我们在编码期间改数据库就行,不需要管缓存的任何操作,Canel在后台只要看数据改了就自动都更新了。

缺点就是,又加了一个中间件,又得开发一些自定义的功能。


最终方案

  1. 选择失效模式
  2. 缓存的所有数据都有过期时间,即使有脏数据,下一次查询数据库也能触发主动更新
  3. 读写数据的时候,加上分布式的读写锁,
    • 如果经常写还经常读,肯定会对系统的性能产生极大的影响
    • 如果偶尔写一次还经常读,那对系统性能一点也不影响