数据库与缓存数据一致性解决方案

标签: 数据库 缓存 数据 | 发表时间:2022-03-15 02:24 | 作者:Java知识图谱
出处:https://juejin.cn/backend

一、序言

在分布式并发系统中,数据库与缓存数据一致性是一项富有挑战性的技术难点。本文将讨论数据库与缓存数据一致性问题,并提供通用的解决方案。

假设有完善的工业级分布式事务解决方案,那么数据库与缓存数据一致性便迎刃而解,实际上,目前分布式事务不成熟。

二、不同的声音

在数据库与缓存数据一致解决方式中,有各种声音。

  • 先操作数据库后缓存还是先缓存后数据库
  • 缓存是更新还是删除
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站,本文收藏在 博客天地


相关 [数据库 缓存 数据] 推荐:

Spring AOP + Redis缓存数据库查询

- - 编程语言 - ITeye博客
我们希望能够将数据库查询结果缓存到Redis中,这样在第二次做同样的查询时便可以直接从redis取结果,从而减少数据库读写次数. 必须要做到与业务逻辑代码完全分离. 从缓存中读出的数据必须与数据库中的数据一致. 如何为一个数据库查询结果生成一个唯一的标识. Key),能唯一确定一个查询结果,同一个查询结果,一定能映射到同一个.

数据库与缓存数据一致性解决方案

- - 掘金 后端
在分布式并发系统中,数据库与缓存数据一致性是一项富有挑战性的技术难点. 本文将讨论数据库与缓存数据一致性问题,并提供通用的解决方案. 假设有完善的工业级分布式事务解决方案,那么数据库与缓存数据一致性便迎刃而解,实际上,目前分布式事务不成熟. 在数据库与缓存数据一致解决方式中,有各种声音. 先操作数据库后缓存还是先缓存后数据库.

MySQL 数据库性能优化之缓存参数优化

- flychen50 - Sky.Jian 朝阳的天空
在平时被问及最多的问题就是关于 MySQL 数据库性能优化方面的问题,所以最近打算写一个MySQL数据库性能优化方面的系列文章,希望对初中级 MySQL DBA 以及其他对 MySQL 性能优化感兴趣的朋友们有所帮助. 这是 MySQL数据库性能优化专题 系列的第一篇文章:MySQL 数据库性能优化之缓存参数优化.

Memcache缓存与Mongodb数据库的优势和应用

- - C++博客-牵着老婆满街逛
转载自: http://www.jzxue.com/shujuku/shujukuzonghe/201005/19-3807.html. 先说说自己对 Memcache和Mongodb的一些看法,主要是抛砖引玉了,希望看到大家的意见和补充. Memcache的优势我觉得总结下来主要体现在:. 可以由10台拥有4G内存的机器,构成一个40G的内存池,如果觉得还不够大可以增加机器,这样一个大的内存池,完全可以把大部分热点业务数据保存进去,由内存来阻挡大部分对数据库读的请求,对数据库释放可观的压力.

如何保证缓存与数据库的双写一致性?

- -
如何保证缓存与数据库的双写一致性. 你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题. 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去.

数据库sharding

- - 数据库 - ITeye博客
当团队决定自行实现sharding的时候,DAO层可能是嵌入sharding逻辑的首选位置,因为在这个层面上,每一个DAO的方法都明确地知道需要访问的数据表以及查询参数,借助这些信息可以直接定位到目标shard上,而不必像框架那样需要对SQL进行解析然后再依据配置的规则进行路由. 另一个优势是不会受ORM框架的制约.

数据库索引

- - CSDN博客推荐文章
索引是由用户创建的、能够被修改和删除的、实际存储于数据库中的物理存在;创建索引的目的是使用户能够从整体内容直接查找到某个特定部分的内容. 一般来说,索引能够提高查询,但是会增加额外的空间消耗,并且降低删除、插入和修改速度. 1.聚集索引:表数据按照索引的顺序来存储的. 2.非聚集索引:表数据存储顺序与索引顺序无关.

数据库事务

- - 数据库 - ITeye博客
事务传播发生在类似以下情形:. 假设methodB的配置是:. 如果methodA在事务里,那么methodB也在这个事务中运行. 如果methodA不在事务里,那么methodB重新建立一个事务运行. 如果methodA在事务里,那么methodB也在这个事务中运行. 如果methodA不在是事务里,那么methodB在非事务中运行.