伙伴云客服论坛»论坛 S区 S软件开发 查看内容

0 评论

0 收藏

分享

Java并发编程数据库与缓存数据一致性方案解析

目录

    一、序言二、不同的声音
      1、操作的先后顺序2、处置缓存的态度
    三、线程并发分析
      查询数据
        1、非并发环境2、并发环境
      更新数据
        1、非并发环境2、并发环境
      依赖环境
    四、先数据库后缓存
      数据一致性
        1、问题描绘2、处置方式
      特殊情况处置方式
    五、小结


一、序言

在分布式并发系统中,数据库与缓存数据一致性是一项富有挑战性的技术难点。本文将讨论数据库与缓存数据一致性问题,并提供通用的处置方案。
假设有完善的工业级分布式事务处置方案,那么数据库与缓存数据一致性便迎刃而解,实际上,目前分布式事务不成熟。

二、不同的声音

在数据库与缓存数据一致处置方式中,有各种声音。
    先操作数据库后缓存还是先缓存后数据库缓存是更新还是删除

1、操作的先后顺序

在并发系统中,数据库与缓存双写场景下,为了追求更大的并发量,操作数据库与缓存显而易见不会同步停止。前者操作胜利后者以异步的方式停止。
关系型数据库作为成熟的工业级数据存储方案,有完善的事务处置机制,数据一旦落盘,不考虑硬件故障,可以负责任的说数据不会丧失。
所谓缓存,无非是存储在内存中的数据,效劳一旦重启,缓存数据全部丧失。既然称之为缓存,那么时刻做好了缓存数据丧失的准备。虽然Redis有耐久化机制,是否可以保证百分之百耐久化?Redis将数据异步耐久化到磁盘有不可,缓存是缓存,数据库是数据库,两个不同的东西。把缓存当数据库使用是一件极其危险的事情。
从数据安全的角度来讲,先操作数据库,然后以异步的方式操作缓存,响应用户恳求。

2、处置缓存的态度

缓存是更新还是删除,对应懒汉式和饱汉式,从处置线程安全理论来讲,删除缓存操作相对难度低一些。假设在删除缓存的前提下满足了查询性能,那么优先选择删除缓存。
更新缓存虽然可以进步查询效率,然后带来的线程并发脏数据处置起来较费事,序言引入MQ等其它消息中间件,因而非必要不推荐。

三、线程并发分析

理解线程并发所带来问题的关键是先理解系统中断,操作系统在任务调度时,中断随时都在发生,这是线程数据不一致产生的根源。以4和8线程CPU为例,同一时刻最多处置8个线程,然而操作系统管理的线程远远超越8个,因而线程们以一种看似并行的方式停止。

查询数据



1、非并发环境

在非并发环境中,使用如下方式查询数据并无不妥:先查询缓存,假设缓存数据不存在,查询数据库,更新缓存,返回结果。
  1. public BuOrder getOrder(Long orderId) {
  2.     String key = ORDER_KEY_PREFIX + orderId;
  3.     BuOrder buOrder = RedisUtils.getObject(key, BuOrder.class);
  4.     if (buOrder != null) {
  5.         return buOrder;
  6.     }
  7.     BuOrder order = getById(orderId);
  8.     RedisUtils.setObject(key, order, 5, TimeUnit.MINUTES);
  9.     return order;
  10. }
复制代码
假设在高并发环境中有一个严重缺陷:当缓存失效时,大量查询恳求涌入,霎时全部打到DB上,轻则数据库连接资源耗尽,用户端响应500错误,重则数据库压力过大效劳宕机。


2、并发环境

因而在并发环境中,需要对上述代码停止修改,使用分布式锁。大量恳求涌入时,获得锁的线程有时机访问数据库查询数据,其余线程阻塞。当查询完数据并更新缓存,然后释放锁。等待的线程重新检查缓存,发现可以获取到数据,直接将缓存数据响应。
这里提到分布式锁,那么使用​​​​​​​表锁还是行锁呢?使用分布式行锁进步并发量;使用二次检查机制,确保等待获得锁的线程可以快速返回结果
  1. @Override
  2. public BuOrder getOrder(Long orderId) {
  3.     /* 假设缓存不存在,则添加分布式锁更新缓存 */
  4.     String key = ORDER_KEY_PREFIX + orderId;
  5.     BuOrder order = RedisUtils.getObject(key, BuOrder.class);
  6.     if (order != null) {
  7.         return order;
  8.     }
  9.     String orderLock = ORDER_LOCK + orderId;
  10.     RLock lock = redissonClient.getLock(orderLock);
  11.     if (lock.tryLock()) {
  12.         order = RedisUtils.getObject(key, BuOrder.class);
  13.         if (order != null) {
  14.             LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
  15.             return order;
  16.         }
  17.         BuOrder buOrder = getById(orderId);
  18.         RedisUtils.setObject(key, buOrder, 5, TimeUnit.MINUTES);
  19.         LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
  20.     }
  21.     return RedisUtils.getObject(key, BuOrder.class);
  22. }
复制代码
更新数据

1、非并发环境

非并发环境中,如下代码虽然可能会产生数据不一致问题(数据被覆盖)。虽然使用数据库层面乐观锁可以处置数据被覆盖问题,然而无效更新流量照旧会流向数据库。
  1. public Boolean editOrder(BuOrder order) {
  2.     /* 更新数据库 */
  3.     updateById(order);
  4.     /* 删除缓存 */
  5.     RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
  6.     return true;
  7. }
复制代码
2、并发环境

上面分析中使用数据库乐观锁可以处置并发更新中数据被覆盖的问题,然而当同一行记录被修改后,版本号发生改变,后续并发流向数据库的恳求为无效流量。减小数据库压力的首要战略是将无效流量拦截在数据库之前。
使用分布式锁可以保证并发流量有序访问数据库,考虑到数据库层面已经使用了乐观锁,第二个及以后获得锁的线程操作数据库为无效流量。
线程在获得锁时采用超时退出的战略,等待获得锁的线程超时快速退出,快速响应用户恳求,重试更新数据操作。
  1. public Boolean editOrder(BuOrder order) {
  2.     String orderLock = ORDER_LOCK + order.getOrderId();
  3.     RLock lock = redissonClient.getLock(orderLock);
  4.     try {
  5.         /* 超时未获取到锁,快速失败,用户端重试 */
  6.         if (lock.tryLock(1, TimeUnit.SECONDS)) {
  7.             /* 更新数据库 */
  8.             updateById(order);
  9.             /* 删除缓存 */
  10.             RedisUtils.deleteObject(OrderServiceImpl.ORDER_KEY_PREFIX + order.getOrderId());
  11.             /* 释放锁 */
  12.             LockOptional.ofNullable(lock).ifLocked(RLock::unlock);
  13.             return true;
  14.         }
  15.     } catch (InterruptedException e) {
  16.         e.printStackTrace();
  17.     }
  18.     return false;
  19. }
复制代码
依赖环境

上述代码使用了封装锁的工具类。
  1. <dependency>
  2.   <groupId>xin.altitude.cms</groupId>
  3.   <artifactId>ucode-cms-common</artifactId>
  4.   <version>1.4.3.2</version>
  5. </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组件,对系统稳定性提出更高的要求。
数据库与缓存一致性问题看似是处置数据问题,实质上处置并发问题:在尽可能保证更多并发量的前提下,在保证数据库安全的前提下,保证数据库与缓存数据一致。
以上就是数据库与缓存数据一致性方案解析的详细内容,更多关于数据库缓存数据一致性的资料请关注网站其它相关文章!

回复

举报 使用道具

相关帖子
全部回复
暂无回帖,快来参与回复吧
本版积分规则 高级模式
B Color Image Link Quote Code Smilies

桦艺
注册会员
主题 21
回复 23
粉丝 0
|网站地图
快速回复 返回顶部 返回列表