实战:工作中对并发问题的处理
大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm 中,欢迎Star和获取原文。
1. 问题背景
问题发生在快递分拣的流程中,我尽可能将业务背景简化,让大家只关注并发问题本身。
分拣业务针对每个快递包裹都会生成一个任务,我们称它为 task。task 中有两个字段需要关注,一个是分拣中发生的 异常(exp_type),另一个是分拣任务的 状态(status)。另外,需要关注 分拣状态上报接口,通过它来记录分拣过程中的异常和状态变更。
一般情况下,分拣机在分拣异常发生时会及时调用接口上报,在分拣完成时调用接口来标记为完成状态,两次接口调用的时间间隔较长,不会发生并发问题。
但是有一种特殊的分拣机,它不会在异常发生时及时上报,而是在分拣完成时将分拣过程中发生的异常和分拣结果一起上报,那么此时分拣状态上报接口在同一时间内就会有两次调用,这时便发生了预期外的并发问题。
我们先看下分拣状态上报接口的执行流程:
-
先查询到该分拣任务 task,默认情况下 exp_type 和 status 均为默认值0
-
分拣异常修改 task 中的 exp_type,分拣完成修改 status 字段信息
-
修改完成将 task 写入
并发问题发生的图示如下:
数据库初始值为 1, 0, 0
,分拣异常和分拣完成几乎同时上报,它们都读取到该值。分拣异常动作将 exp_type 修改为9,写入数据库,此时数据库值为 1, 9, 0
;分拣完成动作将 status 修改为1,写入数据库,使得数据库最终值为 1, 0, 1
,它将异常字段的值覆盖掉了。正常情况下,最终值应该为 1, 9, 1
,分拣完成动作应该读取到分拣异常完成后的值 1, 9, 0
后再进行修改才对。
2. 解决方案
发生这个问题的原因很容易就能发现:两个事务同时执行 读取-修改-写入 序列,其中一个写操作在没有合并另一个写操作变更的情况下,直接覆盖了另一个写操作的结果,所以导致了数据的丢失。
这种问题是比较典型的 丢失更新 问题,可以通过对数据库读操作加锁或者改变数据库的隔离级别为可串行化使事务串行执行的方式进行避免。下面我会将大家在讨论避免丢失更新问题时提出的方案进行介绍,并尽可能的用代码来表现它们。
2.1 数据库读操作加锁和可串行化隔离级别
我们可以考虑:如果对每条Task数据修改的事务都是在当前事务完成之后才允许后续事务进行修改,使事务串行执行,那么我们就能够避免这种情况。比较直接的实现是通过显式加锁来实现,如下
select exp_type, status
from task
where id = 1
for update;
先查询该行数据的事务会获取到该行数据的 排他锁,后续针对该数据的所有读写请求都会被阻塞,直到先前事务执行完将锁释放。
这样通过加锁的方式实现了事务的串行执行。但是,在为SQL添加加锁语句时,需要确定是不是为该行数据加锁而不是锁住了整个表,如果是后者,那么可能会造成系统性能严重下降,而且还需要关注有哪些业务场景使用到了该SQL,是否存在长时间执行的只读事务使用,如果存在的话可能会出现因加锁导致延迟和系统性能下降,所以需要谨慎的评估。
此外,可串行化的数据库隔离级别也能保证事务的串行执行,不过它针对的是所有事务。一般情况下为了保证性能,我们不会采用这种方案(默认使用MySQL可重复读隔离级别)。
MySQL的InnoDB引擎实现可串行化隔离级别采用的是2PL机制:在第一阶段事务执行时获取锁,第二阶段事务执行完成释放锁。
2.2 针对业务只修改必要字段
如果异常状态请求仅修改 exp_type 字段,分拣完成仅修改 status 字段的话,那么我们可以梳理一下业务逻辑,仅将必要修改的字段写入数据库,这样就不会发生丢失更新的异常,如下代码所示:
// 处理异常状态请求,封装修改数据的对象
Task task = new Task();
tast.setId(id);
task.setExpType(expType);// 更改数据
taskService.updateById(task);
在执行修改数据前,创建一个新的修改对象,并只为其必要修改字段赋值。但是还需要考虑的是:如果这个业务流程处理已经很复杂了,很可能不清楚该为哪些字段赋值而导致再发生新的异常,所以采用这种方法需要对业务足够熟悉,并且在修改完后进行充分的测试。
2.3 分布式锁
分布式锁的方法与方法一类似,都是通过加锁的方式来保证同时只有一个事务执行,区别是方法一的锁加在了数据库层,而分布式锁是借助Redis来实现。
这种实现方式的好处是锁的粒度小,发生锁争抢仅限于单个包裹,无需像数据库加锁一样去考虑锁的粒度和对相关业务的影响。伪代码如下所示:
// 分布式锁KEY
String distributedKey = String.format(DISTRIBUTED_KEY_PREFIX, packageNo);
try {// 分布式锁阻塞同一包裹号的修改lock(distributedKey);// 处理业务逻辑handler();
} finally {// 执行完解锁redissonDistributedLocker.unlock(distributedKey);
}
需要注意,lock()
加锁方法要保证加锁失败或发生其他异常情况不影响业务逻辑的执行,并设定好锁持有时间和等待锁的阻塞时间,此外解锁方法务必添加到 finally 代码块中保证锁的释放。
2.4 CAS
CAS是乐观的解决方案,它一般通过在数据库中增加时间戳列来记录上次数据更改的时间,当新的事务执行时,需要比对读取时该行数据的时间戳和数据库中保存的时间戳是否一致,以此来判断事务执行期间是否有其他事务修改过该行数据,只有在没有发生改变的情况下才允许更新,否则需要重试这个事务。样例SQL如下所示:
update task
set exp_type = #{expType}, status = #{status}, ts = #{currentTs}
where id = #{id} and ts = #{readTs}
它的原理不难理解,但是实现起来可能会存在困难,因为需要考虑在执行失败后该如何重试,重试的方式和重试的次数需要根据业务去判断。
巨人的肩膀
- 《数据密集型应用系统设计》第七章 事务
相关文章:

实战:工作中对并发问题的处理
大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm 中…...

腾讯云Cloud Studio:基于Claude快速完成Excel工资自动核算
目录 1 什么是Cloud Studio?2 注册与代码管理2.1 账号注册2.2 Git关联 3 实战:Excel工资自动核算3.1 创建项目与配置3.2 “念咒师”Claude GPT3.3 代码编写与运行 1 什么是Cloud Studio? Cloud Studio是腾讯云为开发者提供的一个基于浏览器的…...
Spring Boot OAuth2 快速入门示例
系统要求 Spring Authorization Server 需要JDK1.8及以上版本。 项目搭建 使用在线项目初始化器 https://start.spring.io/ 生成项目[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ljKbMI4H-1690726855433)(images/screenshot_1690602511482.png)…...
MethodInterceptor
目录 1 MethodInterceptor 1.1 HandleSync 1.2 HandleException 1.3 /// This will be called via Reflection MethodInterceptor HandleSync private void HandleSync(IReadOnlyList<MethodFilterAttribute> filterAttributes, IReadOnlyList<ExceptionFilte…...
PID模块化__以stm32直流电机速度为例
文章目录 前言一、相关PID源码.c.h 二、如何使用1.创建变量2.初始化3.运算4.修改pid参数 总结 前言 本篇使用到的基于这个STM32CubeMX 直流电机PID速度控制、HAL库、cubemx、PID、速度控制、增量式 由于上次使用的pid没有模块化,当多出使用pid的时候就会很麻烦 所以…...

Java ~ Collection/Executor ~ DelayQueue【总结】
前言 文章 相关系列:《Java ~ Collection【目录】》(持续更新)相关系列:《Java ~ Executor【目录】》(持续更新)相关系列:《Java ~ Collection/Executor ~ DelayQueue【源码】》(学…...
前端高级面试题-安全相关
1 XSS 跨⽹站指令码(英语: Cross-site scripting ,通常简称为: XSS )是⼀种⽹站应⽤程式的安全漏洞攻击,是代码注⼊的⼀种。 它允许恶意使⽤者将程式码注⼊到⽹⻚上,其他使⽤者在观看⽹⻚时就会…...

【前缀和】560.和为 K 的子数组
Halo,这里是Ppeua。平时主要更新C,数据结构算法,Linux与ROS…感兴趣就关注我bua! 和为K的子数组 题目:示例:题解:解法一:解法二: 题目: 示例: 题解: 解法一: 暴力解法:我们很容易想到通过两个for循环去遍…...

【Docker】安全及日志管理
安全及日志管理 Docker 安全及日志管理一:Docker 容器与虚拟机的区别1. 隔离与共享2. 性能与损耗 二:Docker 存在的安全问题1.Docker 自身漏洞2.Docker 源码问题 三:Docker 架构缺陷与安全机制1. 容器之间的局域网攻击2. DDoS 攻击耗尽资源3.…...

基于x-scan扫描线的3D模型渲染算法
基于x-scan算法实现的z-buffer染色。c#语言,.net core framework 3.1运行。 模型是读取3D Max的obj模型。 x-scan算法实现: public List<Vertex3> xscan() {List<Vertex3> results new List<Vertex3>();SurfaceFormula formula g…...

LeetCode36.Valid-Sudoku<有效的数独>
题目: 思路: 这题并不难,它类似于N皇后问题。在N皇后问题中,行,列,对角线,写对角线,都不能出现连续的皇后。 本题类似,不过他是行,列,还有一个B…...

Linux中的pause函数
2023年7月29日,周六上午 函数原型 在Linux中,pause()函数用于使当前进程暂停执行,直到接收到一个信号。 #include <unistd.h>int pause(void);pause()函数不接受任何参数。 通常,pause()函数用于编写简单的信号处理程序&…...

CommonCollections6链分析
前面和CC1一样 优点是不限制jdk版本和cc的版本 先开一个ChainedTransformer 然后创LazyMap 我们顺便执行一下避免上面写错 能弹计算器 没问题 后面就是CC6不同的地方了 我们需要一个TiedMapEntry 因为需要一个类调用了get方法 在TiedMapEntry的getValue()方法中调用了get()…...

优化基于tcp,socket的ftp文件传输程序
原始程序: template_ftp_server_old.py: import socket import json import struct import os import time import pymysql.cursorssoc socket.socket(socket.AF_INET, socket.SOCK_STREAM) HOST 192.168.31.111 PORT 4101 soc.bind((HOST,PORT)) p…...

MySQL 数据库 【增删查改(二)】
目录 一、表的设计 1、一对一 2、一对多 3、多对多 二、新增 三、查询 1、聚合查询 (1)聚合函数: (2) group by 子句 (3)having 2、联合查询 (1)内连接 (2)外连接 (3)自链接 (4)…...

力扣 -- 978. 最长湍流子数组
一、题目 二、解题步骤 下面是用动态规划的思想解决这道题的过程,相信各位小伙伴都能看懂并且掌握这道经典的动规题目滴。 三、参考代码 class Solution { public:int maxTurbulenceSize(vector<int>& nums) {int nnums.size();vector<int> f(n);…...

甘特图 Dhtmlx Gantt
介绍 在一些任务计划、日程进度等场景中我们会使用到甘特图,Dhtmlx Gantt 对于甘特图的实现支持很友好,文档API介绍全面,虽然增强版的收费,但免费版的足以够用。 官网:https://docs.dhtmlx.com/gantt/ 安装dhtml gannt…...

iOS 应用上架流程详解
iOS 应用上架流程详解 欢迎来到我的博客,今天我将为大家分享 iOS 应用上架的详细流程。在这个数字化时代,移动应用已经成为了人们生活中不可或缺的一部分,而 iOS 平台的 App Store 则是开发者们发布应用的主要渠道之一。因此,了解…...

Python入门【LEGB规则、面向对象简介、面向过程和面向对象思想、面向对象是什么? 对象的进化 、类的定义、对象完整内存结构 】(十三)
👏作者简介:大家好,我是爱敲代码的小王,CSDN博客博主,Python小白 📕系列专栏:python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 📧如果文章知识点有错误…...

【消息中间件】原生PHP对接Uni H5、APP、微信小程序实时通讯消息服务
文章目录 视频演示效果前言一、分析二、全局注入MQTT连接1.引入库2.写入全局连接代码 二、PHP环境建立总结 视频演示效果 【uniapp】实现买定离手小游戏 前言 Mqtt不同环境问题太多,新手可以看下 《【MQTT】Esp32数据上传采集:最新mqtt插件(支…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
C++ 基础特性深度解析
目录 引言 一、命名空间(namespace) C 中的命名空间 与 C 语言的对比 二、缺省参数 C 中的缺省参数 与 C 语言的对比 三、引用(reference) C 中的引用 与 C 语言的对比 四、inline(内联函数…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...

视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...

免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
MySQL 8.0 事务全面讲解
以下是一个结合两次回答的 MySQL 8.0 事务全面讲解,涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容,并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念(ACID) 事务是…...

代码规范和架构【立芯理论一】(2025.06.08)
1、代码规范的目标 代码简洁精炼、美观,可持续性好高效率高复用,可移植性好高内聚,低耦合没有冗余规范性,代码有规可循,可以看出自己当时的思考过程特殊排版,特殊语法,特殊指令,必须…...