一、前言
当今最风行的缓存中间件当属 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 挂掉时,该业务将不能进行,集体认为也绝对正当)
四、总结
在不同的利用场景下应用不同的实现计划:
- 在
不须要强一致性
的场景下,首选第一种计划,其实现简略、效率高
,不须要引入分布式锁 - 而
须要强一致性
的场景下,无奈只能抉择第二种计划,但分布式锁的引入也减少了保护难度