您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
实战:工作中对并发问题的处理
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
实战:工作中对并发问题的处理
wy****
2023-08-09
IP归属:北京
7120浏览
### 1\. 问题背景 问题发生在快递分拣的流程中,我尽可能将业务背景简化,让大家只关注并发问题本身。 分拣业务针对每个快递包裹都会生成一个任务,我们称它为 task。task 中有两个字段需要关注,一个是分拣中发生的 **异常(exp\_type)**,另一个是分拣任务的 **状态(status)**。另外,需要关注 **分拣状态上报接口**,通过它来记录分拣过程中的异常和状态变更。 ![Task概览.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-07-31-09-01c318BbgZD7IKB7lG.png) 一般情况下,分拣机在分拣异常发生时会及时调用接口上报,在分拣完成时调用接口来标记为完成状态,两次接口调用的时间间隔较长,不会发生并发问题。 但是有一种特殊的分拣机,它不会在异常发生时及时上报,而是在分拣完成时将分拣过程中发生的异常和分拣结果一起上报,那么此时分拣状态上报接口在同一时间内就会有两次调用,这时便发生了预期外的并发问题。 我们先看下分拣状态上报接口的执行流程: 1. 先查询到该分拣任务 task,默认情况下 exp\_type 和 status 均为默认值0 2. 分拣异常修改 task 中的 exp\_type,分拣完成修改 status 字段信息 3. 修改完成将 task 写入 并发问题发生的图示如下: ![并发问题流程图.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-07-31-09-01Qa10iWqEmrDHnEw.png) 数据库初始值为 `1, 0, 0`,分拣异常和分拣完成几乎同时上报,它们都读取到该值。分拣异常动作将 exp\_type 修改为9,写入数据库,此时数据库值为 `1, 9, 0`;分拣完成动作将 status 修改为1,写入数据库,使得数据库最终值为 `1, 0, 1`,它**将异常字段的值覆盖掉了**。正常情况下,最终值应该为 `1, 9, 1`,分拣完成动作应该读取到分拣异常完成后的值 `1, 9, 0` 后再进行修改才对。 ### 2\. 解决方案 发生这个问题的原因很容易就能发现:两个事务同时执行 **读取-修改-写入** 序列,其中一个写操作在没有合并另一个写操作变更的情况下,直接覆盖了另一个写操作的结果,所以导致了数据的丢失。 这种问题是比较典型的 **丢失更新** 问题,可以通过对数据库读操作加锁或者改变数据库的隔离级别为可串行化使事务串行执行的方式进行避免。下面我会将大家在讨论避免丢失更新问题时提出的方案进行介绍,并尽可能的用代码来表现它们。 #### 2.1 数据库读操作加锁和可串行化隔离级别 我们可以考虑:如果对每条Task数据修改的事务都是在当前事务完成之后才允许后续事务进行修改,使事务串行执行,那么我们就能够避免这种情况。比较直接的实现是通过显式加锁来实现,如下 ```sql select exp_type, status from task where id = 1 for update; ``` 先查询该行数据的事务会获取到该行数据的 **排他锁**,后续针对该数据的所有读写请求都会被阻塞,直到先前事务执行完将锁释放。 这样通过加锁的方式实现了事务的串行执行。但是,在为SQL添加加锁语句时,需要确定是不是为该行数据加锁而不是锁住了整个表,如果是后者,那么可能会造成系统性能严重下降,而且还需要关注有哪些业务场景使用到了该SQL,是否存在长时间执行的只读事务使用,如果存在的话可能会出现因加锁导致延迟和系统性能下降,所以需要谨慎的评估。 此外,可串行化的数据库隔离级别也能保证事务的串行执行,不过它针对的是所有事务。一般情况下为了保证性能,我们不会采用这种方案(默认使用MySQL可重复读隔离级别)。 > MySQL的InnoDB引擎实现可串行化隔离级别采用的是2PL机制:在第一阶段事务执行时获取锁,第二阶段事务执行完成释放锁。 #### 2.2 针对业务只修改必要字段 如果异常状态请求仅修改 exp\_type 字段,分拣完成仅修改 status 字段的话,那么我们可以梳理一下业务逻辑,仅将必要修改的字段写入数据库,这样就不会发生丢失更新的异常,如下代码所示: ```java // 处理异常状态请求,封装修改数据的对象 Task task = new Task(); tast.setId(id); task.setExpType(expType); // 更改数据 taskService.updateById(task); ``` 在执行修改数据前,创建一个新的修改对象,并只为其必要修改字段赋值。但是还需要考虑的是:如果这个业务流程处理已经很复杂了,很可能不清楚该为哪些字段赋值而导致再发生新的异常,所以采用这种方法需要对业务足够熟悉,并且在修改完后进行充分的测试。 #### 2.3 分布式锁 分布式锁的方法与方法一类似,都是通过加锁的方式来保证同时只有一个事务执行,区别是方法一的锁加在了数据库层,而分布式锁是借助Redis来实现。 这种实现方式的好处是锁的粒度小,发生锁争抢仅限于单个包裹,无需像数据库加锁一样去考虑锁的粒度和对相关业务的影响。伪代码如下所示: ```java // 分布式锁KEY String distributedKey = String.format(DISTRIBUTED_KEY_PREFIX, packageNo); try { // 分布式锁阻塞同一包裹号的修改 lock(distributedKey); // 处理业务逻辑 handler(); } finally { // 执行完解锁 redissonDistributedLocker.unlock(distributedKey); } ``` 需要注意,`lock()` 加锁方法要保证加锁失败或发生其他异常情况不影响业务逻辑的执行,并设定好锁持有时间和等待锁的阻塞时间,此外解锁方法务必添加到 **finally** 代码块中保证锁的释放。 #### 2.4 CAS CAS是乐观的解决方案,它一般通过在数据库中增加时间戳列来记录上次数据更改的时间,当新的事务执行时,需要比对读取时该行数据的时间戳和数据库中保存的时间戳是否一致,以此来判断事务执行期间是否有其他事务修改过该行数据,只有在没有发生改变的情况下才允许更新,否则需要重试这个事务。样例SQL如下所示: ```sql update task set exp_type = #{expType}, status = #{status}, ts = #{currentTs} where id = #{id} and ts = #{readTs} ``` 它的原理不难理解,但是实现起来可能会存在困难,因为需要考虑在执行失败后该如何重试,重试的方式和重试的次数需要根据业务去判断。 --- ### 巨人的肩膀 * 《数据密集型应用系统设计》第七章 事务
上一篇:MySQL 执行计划详解
下一篇:分布式事务的华丽进化
wy****
文章数
22
阅读量
2476
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
分布式服务高可用实现:复制
1. 为什么需要复制我们可以考虑如下问题:当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?希望在单台服务器出现故障时仍能继续工作,这该如何实现?当服务的用户遍布全球,并希望他们访问服务时不会有较大的延迟,怎么才能统一用户的交互体验?这些问题其实都能通过 “复制” 来解决:复制,即在不同的节点上保存相同的副本,提供数据冗余。如果一些节点不可用,剩余的节点仍然可以提供数据服务
01
高性能MySQL实战(三):性能优化
这篇主要介绍对慢 SQL 优化的一些手段,而在讲解具体的优化措施之前,我想先对 EXPLAIN 进行介绍,它是我们在分析查询时必要的操作,理解了它输出结果的内容更有利于我们优化 SQL。为了方便大家的阅读,在下文中规定类似 key1 的表示二级索引,key_part1 表示联合索引的第一部分,unique_key1 则表示唯一二级索引,primary_key 表示主键索引。高性能MySQL实战(一
01
从2PC和容错共识算法讨论zookeeper中的Create请求
最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。1. 2PC(两阶段提交协议)两阶段提交 (two-phase commit) 协议是一种用于实现 跨多个节点的原子事务(分布
wy****
文章数
22
阅读量
2476
作者其他文章
01
高性能MySQL实战(一):表结构
01
分布式服务高可用实现:复制
01
高性能MySQL实战(三):性能优化
01
从2PC和容错共识算法讨论zookeeper中的Create请求
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号