0

0

Java并发编程的数据库与缓存数据一致性方案是什么?

PHPz

PHPz

发布时间:2023-04-24 08:28:06

|

1060人浏览过

|

来源于亿速云

转载

一、序言

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

二、不同的声音

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

  • 先操作数据库后缓存还是先缓存后数据库

  • 缓存是更新还是删除

1、操作的先后顺序

在并发系统中,数据库与缓存双写场景下,为了追求更大的并发量,操作数据库与缓存显而易见不会同步进行。前者操作成功后者以异步的方式进行。

立即学习Java免费学习笔记(深入)”;

关系型数据库作为成熟的工业级数据存储方案,有完善的事务处理机制,数据一旦落盘,不考虑硬件故障,可以负责任的说数据不会丢失。

所谓缓存,无非是存储在内存中的数据,服务一旦重启,缓存数据全部丢失。既然称之为缓存,那么时刻做好了缓存数据丢失的准备。尽管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、并发环境

上面分析中使用数据库乐观锁能够解决并发更新中数据被覆盖的问题,然而当同一行记录被修改后,版本号发生改变,后续并发流向数据库的请求为无效流量。减小数据库压力的首要策略是将无效流量拦截在数据库之前。

使用分布式锁能够保证并发流量有序访问数据库,考虑到数据库层面已经使用了乐观锁,第二个及以后获得锁的线程操作数据库为无效流量。

DESTOON B2B网站管理系统
DESTOON B2B网站管理系统

DESTOON B2B网站管理系统是一套完善的B2B(电子商务)行业门户解决方案。系统基于PHP+MySQL开发,采用B/S架构,模板与程序分离,源码开放。模型化的开发思路,可扩展或删除任何功能;创新的缓存技术与数据库设计,可负载千万级别数据容量及访问。

下载

线程在获得锁时采用超时退出的策略,等待获得锁的线程超时快速退出,快速响应用户请求,重试更新数据操作。

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;
}

依赖环境

上述代码使用了封装锁的工具类。


  xin.altitude.cms
  ucode-cms-common
  1.4.3.2

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删除缓存作为保底操作。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
rabbitmq和kafka有什么区别
rabbitmq和kafka有什么区别

rabbitmq和kafka的区别:1、语言与平台;2、消息传递模型;3、可靠性;4、性能与吞吐量;5、集群与负载均衡;6、消费模型;7、用途与场景;8、社区与生态系统;9、监控与管理;10、其他特性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

202

2024.02.23

Java 消息队列与异步架构实战
Java 消息队列与异步架构实战

本专题系统讲解 Java 在消息队列与异步系统架构中的核心应用,涵盖消息队列基本原理、Kafka 与 RabbitMQ 的使用场景对比、生产者与消费者模型、消息可靠性与顺序性保障、重复消费与幂等处理,以及在高并发系统中的异步解耦设计。通过实战案例,帮助学习者掌握 使用 Java 构建高吞吐、高可靠异步消息系统的完整思路。

8

2026.01.28

什么是分布式
什么是分布式

分布式是一种计算和数据处理的方式,将计算任务或数据分散到多个计算机或节点中进行处理。本专题为大家提供分布式相关的文章、下载、课程内容,供大家免费下载体验。

328

2023.08.11

分布式和微服务的区别
分布式和微服务的区别

分布式和微服务的区别在定义和概念、设计思想、粒度和复杂性、服务边界和自治性、技术栈和部署方式等。本专题为大家提供分布式和微服务相关的文章、下载、课程内容,供大家免费下载体验。

235

2023.10.07

什么是中间件
什么是中间件

中间件是一种软件组件,充当不兼容组件之间的桥梁,提供额外服务,例如集成异构系统、提供常用服务、提高应用程序性能,以及简化应用程序开发。想了解更多中间件的相关内容,可以阅读本专题下面的文章。

178

2024.05.11

Golang 中间件开发与微服务架构
Golang 中间件开发与微服务架构

本专题系统讲解 Golang 在微服务架构中的中间件开发,包括日志处理、限流与熔断、认证与授权、服务监控、API 网关设计等常见中间件功能的实现。通过实战项目,帮助开发者理解如何使用 Go 编写高效、可扩展的中间件组件,并在微服务环境中进行灵活部署与管理。

214

2025.12.18

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

502

2023.08.10

常用的数据库软件
常用的数据库软件

常用的数据库软件有MySQL、Oracle、SQL Server、PostgreSQL、MongoDB、Redis、Cassandra、Hadoop、Spark和Amazon DynamoDB。更多关于数据库软件的内容详情请看本专题下面的文章。php中文网欢迎大家前来学习。

978

2023.11.02

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

84

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 8万人学习

Django 教程
Django 教程

共28课时 | 3.6万人学习

Excel 教程
Excel 教程

共162课时 | 13.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号