一、前言
当今最风行的缓存中间件当属redis了,因为redis是基于内存操作
,性能优越
,所以被宽泛应用。
应用缓存的个别步骤如下:
- 先查问缓存,如果
缓存命中
,间接返回数据 - 如果
缓存不命中
,则查询数据库返回数据,并将查问到的数据放入缓存中
然而当咱们想要更新数据时,这时可能会呈现缓存与数据库中的数据不统一问题,这外面有各种更新缓存的操作,比方先更新缓存、再更新数据库
或先更新数据库、再更新缓存
,这里不一一列举了,可查阅站内大神写的 https://segmentfault.com/a/11...
二、提早双删计划
在咱们外部个别是通过先更新数据,再删除缓存,再提早删除
的计划来更新缓存的,这样能够使缓存与数据库达到最终一致性
。伪代码如下
tx.begin(); // 开启事务boolean result = updateDB(data);if (result) { boolean cacheResult = deleteCache(dataId); // 删除缓存 if (!cacheResult) { tx.rollback(); // 回滚事务 return; }} else { tx.rollback(); // 回滚事务 return;}tx.commit(); // 提交事务// 将dataId放入提早队列,通过异步地形式再次删除该缓存// 异步删除缓存失败能够进行重试,如果失败次数达到n,则发送告警信息delayQueue.offer(dataId);
这种计划优缺点很显著,长处就是实现简略
,毛病就是只能让缓存和数据库达到最终一致性,依然可能呈现一小段时间的不统一
。
三、分布式锁计划
那如果真的有某些场景想要达到强一致性
,这里咱们外部抉择的是应用分布式锁
(为了不引入其余组件,应用redis来实现分布式锁)。
那代码如何来实现缓存与数据库的强一致性,伪代码如下:
3.1 查问数据
Object result = getCache(dataId); // 查问缓存if (result == null) { // 缓存未命中 RLock lock = getRLock(); // 获取分布式锁 lock.lock(); try { result = getCache(dataId); // 再次查问缓存,如果命中缓存,间接返回 if (result != null) return result; result = queryDB(dataId); // 查询数据库 putCache(dataId, result); // 将查问后果置入缓存中 } finally { lock.unlock(); // 开释锁 }}return result;
3.2 更新数据
// 获取分布式锁RLock lock = getRLock();lock.lock();try { tx.begin(); // 开启事务 boolean result = updateDB(data); // 更新数据库 if (result) { // 如果更新数据库胜利 boolean cacheResult = deleteCache(dataId); // 删除缓存 if (!cacheResult) { // 删除缓存失败 tx.rollback(); // 回滚事务 return; } } else { // 更新数据库失败 tx.rollback(); // 回滚事务 return; } tx.commit(); // 提交事务} finally { lock.unlock(); // 开释锁}
下面的伪代码还有许多能够优化的中央,这里只是把外围局部贴出来,仅供参考。
3.3、存在的问题
一旦引入分布式锁,也将引入新的问题
- 如果是单点redis,无奈保障高可用
- 如果是redis哨兵或集群模式,
极其状况
下会存在锁失落
(在主从切换时,master还没来的及将锁信息同步到slave时,master挂掉,slave切换为master,此时锁失落
)的状况,如何取舍?(集体偏差于应用独自的单点Redis来做分布式锁,因为在曾经须要强一致性的前提下,当该用作分布式锁的redis挂掉时,该业务将不能进行,集体认为也绝对正当)
四、总结
在不同的利用场景下应用不同的实现计划:
- 在
不须要强一致性
的场景下,首选第一种计划,其实现简略、效率高
,不须要引入分布式锁 - 而
须要强一致性
的场景下,无奈只能抉择第二种计划,但分布式锁的引入也减少了保护难度