共计 3953 个字符,预计需要花费 10 分钟才能阅读完成。
一、序言
在分布式并发零碎中,数据库与缓存数据一致性是一项富裕挑战性的技术难点。本文将探讨数据库与缓存数据一致性问题,并提供通用的解决方案。
假如有欠缺的工业级分布式事务解决方案,那么数据库与缓存数据一致性便迎刃而解,实际上,目前分布式事务不成熟。
二、不同的声音
在数据库与缓存数据统一解决形式中,有各种声音。
- 先操作数据库后缓存还是先缓存后数据库
- 缓存是更新还是删除
1、操作的先后顺序
在并发零碎中,数据库与缓存 双写
场景下,为了谋求更大的并发量,操作数据库与缓存不言而喻不会同步进行。前者操作胜利后者以异步的形式进行。
关系型数据库作为成熟的工业级数据存储计划,有欠缺的事务处理机制,数据一旦落盘,不思考硬件故障,能够负责任的说数据不会失落。
所谓缓存,无非是存储在内存中的数据,服务一旦重启,缓存数据全副失落。既然称之为缓存,那么时刻做好了缓存数据失落的筹备。只管 Redis 有长久化机制,是否可能保障百分之百长久化?Redis 将数据异步长久化到磁盘有不可,缓存是缓存,数据库是数据库,两个不同的货色。把缓存当数据库应用是一件极其危险的事件。
从数据安全的角度来讲,先操作数据库,而后以异步的形式操作缓存,响应用户申请。
2、解决缓存的态度
缓存是更新还是删除,对应 懒汉式
和饱汉式
,从解决线程平安实际来讲,删除缓存操作绝对难度低一些。如果在删除缓存的前提下满足了查问性能,那么优先选择删除缓存。
更新缓存只管可能进步查问效率,而后带来的线程并发脏数据处理起来较麻烦,序言引入 MQ 等其它消息中间件,因而非必要不举荐。
三、线程并发剖析
了解线程并发所带来问题的要害是先了解 零碎中断
,操作系统在任务调度时,中断随时都在产生,这是线程数据不统一产生的本源。以 4 和 8 线程 CPU 为例,同一时刻最多解决 8 个线程,然而操作系统治理的线程远远超过 8 个,因而线程们以一种看似 并行
的形式进行。
(一)查问数据
1、非并发环境
在非并发环境中,应用如下形式查问数据并无不妥:先查问缓存,如果缓存数据不存在,查询数据库,更新缓存,返回后果。
public BuOrder getOrder(Long orderId) {
String key = ORDER_KEY_PREFIX + orderId;
BuOrder buOrder = RedisUtils.getObject(key, BuOrder.class);
if (buOrder != null) {return buOrder;}
BuOrder order = getById(orderId);
RedisUtils.setObject(key, order, 5, TimeUnit.MINUTES);
return order;
}
如果在高并发环境中有一个重大缺点:当缓存生效时,大量查问申请涌入,霎时全部打到 DB 上,轻则数据库连贯资源耗尽,用户端响应500 谬误
,重则数据库压力过大服务宕机。
2、并发环境
因而在并发环境中,须要对上述代码进行批改,应用 分布式锁
。大量申请涌入时,取得锁的线程有机会拜访数据库查问数据,其余线程阻塞。当查问完数据并更新缓存,而后开释锁。期待的线程从新查看缓存,发现可能获取到数据,间接将缓存数据响应。
这里提到分布式锁,那么应用 表锁
还是 行锁
呢?应用分布式行锁进步并发量;应用二次查看机制,确保期待取得锁的线程可能疾速返回后果
@Override
public BuOrder getOrder(Long orderId) {
/* 如果缓存不存在,则增加分布式锁更新缓存 */
String key = ORDER_KEY_PREFIX + orderId;
BuOrder order = RedisUtils.getObject(key, BuOrder.class);
if (order != null) {return order;}
String orderLock = ORDER_LOCK + orderId;
RLock lock = redissonClient.getLock(orderLock);
if (lock.tryLock()) {order = RedisUtils.getObject(key, BuOrder.class);
if (order != null) {LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
return order;
}
BuOrder buOrder = getById(orderId);
RedisUtils.setObject(key, buOrder, 5, TimeUnit.MINUTES);
LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
}
return RedisUtils.getObject(key, BuOrder.class);
}
(二)更新数据
1、非并发环境
非并发环境中,如下代码只管可能会产生数据不统一问题(数据被笼罩)。只管应用数据库层面 乐观锁
可能解决数据被笼罩问题,然而有效更新流量依旧会流向数据库。
public Boolean editOrder(BuOrder order) {
/* 更新数据库 */
updateById(order);
/* 删除缓存 */
RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
return true;
}
2、并发环境
下面剖析中应用数据库 乐观锁
可能解决并发更新中数据被笼罩的问题,然而当同一行记录被批改后,版本号产生扭转,后续并发流向数据库的申请为有效流量。减小数据库压力的首要策略是将有效流量拦挡在数据库之前。
应用分布式锁可能保障并发流量有序拜访数据库,思考到数据库层面曾经应用了乐观锁,第二个及当前取得锁的线程操作数据库为有效流量。
线程在取得锁时采纳 超时退出
的策略,期待取得锁的线程超时疾速退出,疾速响应用户申请,重试更新数据操作。
public Boolean editOrder(BuOrder order) {String orderLock = ORDER_LOCK + order.getOrderId();
RLock lock = redissonClient.getLock(orderLock);
try {
/* 超时未获取到锁,疾速失败,用户端重试 */
if (lock.tryLock(1, TimeUnit.SECONDS)) {
/* 更新数据库 */
updateById(order);
/* 删除缓存 */
RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
/* 开释锁 */
LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
return true;
}
} catch (InterruptedException e) {e.printStackTrace();
}
return false;
}
(三)依赖环境
上述代码应用了封装锁的工具类。
<dependency>
<groupId>xin.altitude.cms</groupId>
<artifactId>ucode-cms-common</artifactId>
<version>1.4.3.2</version>
</dependency>
LockOptional
依据锁的状态执行后续操作。
四、先数据库后缓存
(一)数据一致性
1、问题形容
接下来探讨先更新数据库,后删除缓存是否存在并发问题。
(1)缓存刚好生效(2)申请 A 查询数据库,得一个旧值(3)申请 B 将新值写入数据库(4)申请 B 删除缓存(5)申请 A 将查到的旧值写入缓存
上述并发问题呈现的要害是第 5 步比第 3、4 步后产生,由操作系统中断不确定因素可知,此种状况却有产生的可能。
2、解决形式
从理论状况来看,将数据写入 Redis 远比将数据写入数据库耗时要短,只管产生的概率较低,但仍会产生。
(1)减少缓存过期工夫
减少缓存过期工夫容许肯定工夫范畴内脏数据存在,直到下一次并发更新呈现,可能会呈现脏数据。脏数据会周期性存在。
(2)更新和查问共用一把行锁
更新和查问共用一把行分布式锁,上述问题不复存在。当读申请获取到锁时,写申请处于阻塞状态(超时会疾速失败返回),可能保障步骤 5 在步骤 3 之前进行。
(3)提早删除缓存
应用 RabbitMQ 提早删除缓存,去除步骤 5 的影响。应用异步的形式进行,简直不影响性能。
(二)非凡状况
数据库有事务机制保障操作胜利与否;Redis 单条指令具备原子性,而后组合起来却不具备原子特色,具体来说是数据库操作胜利,而后利用异样挂掉,导致 Redis 缓存未删除。Redis 服务网络连接超时呈现此问题。
如果设置有缓存过期工夫,那么在缓存尚未过期前,脏数据始终存在。如果未设置过期工夫,那么直到下一次批改数据前,脏数据始终存在。(数据库数据曾经产生扭转,缓存尚未更新)
解决形式
在操作数据库前,向 RabbitMQ 写入一条提早删除缓存的音讯,而后执行数据库操作,执行缓存删除操作。不论代码层面缓存是否删除胜利,MQ 删除缓存作为保底操作。
五、小结
上述形式提供的数据库与缓存数据一致性解决形式,属于耦合版,当然还有订阅 binlog 日志的解耦版。解耦版因为减少了订阅 binlog 组件,对系统稳定性提出更高的要求。
数据库与缓存一致性问题看似是解决数据问题,本质上解决并发问题:在尽可能保障更多并发量的前提下,在保障数据库安全的前提下,保障数据库与缓存数据统一。
喜爱本文点个♥️赞♥️反对一下,如有须要,可通过微信
dream4s
与我分割。相干源码在 GitHub,视频解说在 B 站,本文珍藏在博客天地。