实战回顾
2018年11月28日 有两个客户在两个渠道购买了同一产品每人各买2笔
系统应在29日做成交处理, 成交结束后, 更新一张记录表, 记录表根据产品代码和渠道代码作为Unique.
成交使用已客户为维度的多线程成交.
// 方法名为虚拟捏造, 并非实际使用方法名
成交方法 chengjiao() 为独立事务;
chengjiao() 方法内使用多线程的嵌套事务 NESTED doChengjiao()
伪代码
// 独立事务
chengjiao() {
// 根据客户查出交易
List<Trade> lists = getTradeList();
new Thread ... (list);
}
// 嵌套事务
doChengjiao();
假如数据为 渠道 001 产品 002 渠道 002 产品 002
那更新的记录两条线程都要取更新表里面更新 001&002记录 和 002&002;
但是问题出在线程的执行顺序;
两个客户每个人在不同渠道买了一笔, 一共四笔交易记录;
线程A先去更新了 001 & 002 这条记录
线程B先去更新了 002 & 002 这条记录
之后
线程B又去更新 001 & 002 这条记录; (问题在这已经出现)
线程A去更新 002 & 002 这条记录;
后续的线程B在更新的时候, 在等待这条记录之前的UPDATE事务提交或回滚, 而在占用这条记录的线程A想要提交需要等待002 & 002 这条记录提交或回滚, 而002 & 002这条记录正好被B线程占用, 由此造成了互相等待, 将更新表锁住.后续交易无法进行.后经人为干预(数小时后发现), 杀掉其中一条会话, 导致该会话数据回滚, 而另一个会话因为数据库等待时间过长, 数据也没有进行提交, 最后导致4笔交易全部回滚. 如果问题出在这也就没什么. 问题是每天这几笔交易都恰巧同时执行.就一直卡死. 最后在12月3号, 4笔交易成交了. 4个工作日.问题影响… 客户是拒绝的… 不过好在客户大度, 没有计较. … …
至此将问题从生产日志取下, 分析, 复现, 解决,重新上线 共计 2周+, 期间对spring事务感悟颇深. 遂总结此文章. 整理, 学习.
Sring 事务管理
首先来看事务的四个特性:
- 原子性
事务的执行将事务内所做的操作看做一个整体, 要么全部执行, 要么全部不执行.
- 隔离性 (可能导致死锁)
简单来说, 两个事务在同时进行更新时,一个事务在更新, 另一个事务需要操作时,不可能看到这条记录之前的值, 需要等到之前的事务要么执行(事务提交),要么不执行(事务回滚). 才可以继续对该记录进行操作. 这也是事务的其中一个隔离级别, 也是默认最优隔离级别 READCOMMITED 读已提交;
- 一致性
对于同处在一个事务中的数据而言. 需要保持所有的相关数据保持一致状态, 当事务执行完以后也要保持相关全部数据的正确性
- 持久性
简单来说, 事物的提交之后的数据保存到数据库中, 进行持久化处理;
事务的4个隔离级别
隔离级别 | 脏读 | 不可重读 | 幻读 |
---|---|---|---|
读操作未提交 | 可能 | 可能 | 可能 |
读操作已提交 | 不可能 | 可能 | 可能 |
可重读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
事务的7个传播机制
- REQUIRED: 如果存在一个事务,支持当前事务。如果没有事务则开启
- SUPPORTS: 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行
- NOT_SUPPORTED: 总是非事务地执行,并挂起任何存在的事务(不使用事务)
- NESTED: 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, REQUIRED 属性执行
- REQUIRES_NEW: 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起(自己一个事务,独立事务)
- NEVER: 总是非事务地执行,如果存在一个活动事务,则抛出异常(必须由非事务的方法调用)
- MANDATORY: 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常(必须由带有事务的方法来调用)
画重点
spring的事务管理中, 让我们容易出现问题的几个传播约定
- REQUIRED
- NESTED
1是默认传播机制, 2是嵌套传播机制;
REQUIRED 如果你没有, 我就自己管自己, 如果有, 就用你的;
NESTED 如果有, 我就听你的, 如果没有, 我就按照默认的走;
举例说明:
fun1() 方法1 是一个带事务的方法, 我们将使用fun1()来调用, fun2(), 此时的方法2 fun2()我们在配置事务的时候
- 配置了一个 REQUIRED , 那么此时的fun2()支持fun1()的事务, 与fun1() 事务相同, 你是什么事务, 我就是什么事务.
- 配置了一个NESTED, 那么这个时候的fun2() 则是存在fun1()的事务之中, 而不是另起一个事务的存在. 他的提交与回滚, 与 fun1() 共存, fun1() 提交, 我就提交, fun1()回滚, 我就回滚;
当fun1() 方法1 不是一个带事务的方法 , 此时 REQUIRED 与 NESTED 意义相同; 都将自身新启事务. 独立提交或回滚;