一、前言
当今最流行的缓存中间件当属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挂掉时,该业务将不能进行,个人认为也相对合理)
- 在
不需要强一致性
的场景下,首选第一种方案,其实现简单、效率高
,不需要引入分布式锁 - 而
需要强一致性
的场景下,无奈只能选择第二种方案,但分布式锁的引入也增加了维护难度