
一般系统中都会添加缓存中间件缓存一些热点数据以提高系统的响应速度但是缓存Redis的加入会导致系统复杂性的增加例如如何保证数据库和缓存的数据保持一致性。而这种情况根据业务的不同又可以分为强一致性和弱一致性。强一致性定义及场景强一致性一般是指对于一些数据的修改系统要保证该修改用户立马可见即任何时刻用户读到的都是最新写入的数据。常见的场景有商品库存下单扣减库存后必须立即生效防止出现超卖账户余额、支付金额扣款、转账之后余额必须立即准确订单状态支付成功后订单状态需要用户立马可见权限类数据如用户被封禁后需要立即无法再进行操作例如对于商品库存的修改需要用户立马可见。实现方式对于强一致性的系统而言为了保证强一致性一般需要同时操作数据库和缓存。例如修改数据库之后立马修改缓存但是由于并发带来的问题导致先修改数据库还是先修改缓存会产生不一样的结果下面就对先操作数据库和先操作缓存进行分类讨论。在操作缓存时一般默认是直接删除缓存原因有以下两点对于缓存中的修改操作一般是要比删除操作要慢修改可能需要修改对应 key 中的某一个值删除时只需要删除整个 key 即可。一般系统只会对热点数据进行缓存即使缓存被删除后该数据不存在了也可以通过互斥控制的方式实现一次只有一个请求访问数据库并将数据放入缓存中。先操作缓存对于先操作缓存时可能出现以下这种情况。线程一先删除缓存在线程一删除缓存和修改数据库之间线程二进行了查询数据库并写入缓存的操作。这时就会导致数据库的数据是新值但是缓存由于线程二的加入导致缓存的内容是旧值。先操作数据库先操作数据库也会有一些问题存在。如下图线程一首先查询缓存发现没有数据就在查询数据库并写入缓存之间线程二出现线程二完成了修改数据库以及删除缓存的操作。这又会导致缓存中的是旧值而数据库中的为新值从而导致数据不一致的问题。但我认为这种情况是很少见的原因就是发生这种情况需要满足两个条件一是线程一查询缓存时缓存刚好失效也可能是这条数据从未被缓存过二是就在这时线程二需要修改数据。这种情况个人认为概率是非常低的。除非这是一个写多读少的系统但对于写多读少的系统我们一般不会对缓存进行操作。这个概率低同样的论证也适用于先操作缓存但两者的概率并不在一个量级关键区别在于并发窗口的大小先删缓存的失败条件是读线程的查库 写缓存落在写线程删缓存到更新数据库之间即可。数据库的更新操作本身较慢中间还可能穿插业务逻辑这个窗口相对较长而读请求本身就很频繁读请求落入这个窗口的概率并不低。先更新数据库的失败条件是读线程的查库 写缓存必须完整跨越写线程更新数据库 删除缓存这两个都很快的操作即查库要发生在更新数据库之前、写缓存要发生在删除缓存之后要求两个线程的操作严格交错窗口极窄。所以两种方案理论上都可能出现不一致但先删缓存出问题的概率明显更高这正是先操作数据库、再删除缓存成为行业通用方案的原因。使用方案通过上面的分析以及行业的经验。一般对于强一致性的系统通常都是通过先操作数据库、再删除缓存的方式进行的。同时也有其他一些方案我个人认为这些方案都有一定的缺点了解即可。延时双删延时双删是指在修改数据时先删除缓存 - 修改数据库 -延时一会删除缓存。由于先操作数据库再删除缓存出现问题的概率是很低的同时延时双删中这个延时的时长是多少这是一个非常吃经验的值。引入读写锁引入 Redisson 的读写锁。引入锁之后会导致系统的响应有一定程度的降低特别是并发的时候这需要结合业务综合考量。Redisson 读写锁代码示例读锁public Item getById(Integer id){ RReadWriteLock readWriteLock redissonClient.getReadWriteLock(ITEM_READ_WRITE_LOCK); //读之前加读锁读锁的作用就是等待该lockkey释放写锁以后再读 RLock readLock readWriteLock.readLock(); try { //开锁 readLock.lock(); System.out.println(readLock...); Item item (Item) redisTemplate.opsForValue().get(item:id); if(item ! null){ return item; } //查询业务数据 item new Item(id, 华为手机, 华为手机, 5999.00); //写入缓存 redisTemplate.opsForValue().set(item:id,item); //返回数据 return item; } finally { readLock.unlock(); } }写锁public void updateById(Integer id){ RReadWriteLock readWriteLock redissonClient.getReadWriteLock(ITEM_READ_WRITE_LOCK); //写之前加写锁写锁加锁成功读锁只能等待 RLock writeLock readWriteLock.writeLock(); try { //开锁 writeLock.lock(); System.out.println(writeLock...); //更新业务数据 Item item new Item(id, 华为手机, 华为手机, 5299.00); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } //删除缓存 redisTemplate.delete(item:id); } finally { writeLock.unlock(); } }弱一致性定义及常见场景弱一致性是指系统允许数据在修改后短时间内存在不一致即不保证修改对用户立马可见只要求经过一段时间后缓存与数据库的数据最终能够达成一致最终一致性。常见的场景有商品的基础展示信息如商品标题、描述、图片等非核心字段内容类数据如文章、评论、点赞数、浏览量等用户信息如昵称、头像等推荐类数据如首页推荐、排行榜等这类数据即使短时间内读到旧值也不会对业务造成实质性影响。使用方案弱一致性一般不需要同时操作数据库和缓存这种弱一致性处理方式其实很像是主从复制的思路 —— 最终一致性。一般有两种方案使用 MQ使用 canalCanal 是阿里巴巴开源的一个基于 MySQL binlog 的增量数据订阅与消费组件。它的工作原理是将自己伪装成 MySQL 的一个从库slave向主库发送 dump 请求主库随后将 binlog 推送给 CanalCanal 解析 binlog 后即可拿到数据库中数据的变更增、删、改事件再将其发送到 MQ 或直接交给业务方处理。这样业务系统就可以在不侵入业务代码的情况下感知数据库的变更再据此删除或更新对应的缓存从而实现数据库到缓存的最终一致性。