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

3次阅读

共计 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 站,本文珍藏在博客天地。


正文完
 0