注:如果要使用事务操作,数据表的存储引擎必须是InnoDb事务用于保证数据的一致性,它由一组相关的dml语句组成,该组的dml语句要么全部成功,要么全部失败。如转账就要用事务来处理,用以保证数据一致性。注:什么是 DML?DML = Data Manipulation Language,数据操作语言。它只干一件事:对表里已经存在的数据,进行增、删、改。常见就这 3 种:INSERT:插入数据UPDATE:修改数据DELETE:删除数据
下面我们使用一件事来帮助理解一下事务的基本内涵。利用转账这个例子最容易理解,下面表是几个账户的基本信息:现在假设A给B转100元,数据库里要做两件事:一把A的余额扣 100,
UPDATE account SET money = money - 100 WHERE name = 'A';
UPDATE account SET money = money + 100 WHERE name = 'B';
这两句都是 DML。没有事务会怎样?第一句执行成功:A 少了 100,然后第二句突然断电 / 报错:B 没多 100。结果钱凭空消失,数据不一致。
事务的核心特性就是行业里常说的 ACID,四个字母分别对应:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。原子性:事务是一个不可分割的最小执行单元,里面的所有操作,要么全部执行成功并提交,要么全部失败并回滚,绝对不会停在 “部分成功、部分失败” 的中间状态。A 扣钱、B 加钱,两步必须同时成功/同时失败,绝对不能出现「A 扣了钱,B 没收到」的情况一致性:事务执行前后,数据库的所有数据约束(业务规则、完整性约束)必须保持一致,不能出现非法状态。转账前后,A+B 的总金额永远不变(比如 A 有 1000、B 有 500,总 1500;转完 A900、B600,总还是 1500)隔离性:多个并发事务同时执行时,互相之间不能干扰,每个事务都感觉不到其他事务的存在,不会读取到其他事务的中间未提交状态,也不会被其他事务的操作破坏。A 给 B 转账的同时,C 给 D 转账,两个事务完全互不干扰,不会互相篡改对方的数据持久性:事务一旦提交成功,对数据库的修改就永久生效,就算服务器断电、崩溃、重启,数据也不会丢失,能恢复到提交后的状态。转账提交成功后,就算数据库宕机、重启,A 少 100、B 多 100 的结果也会永久保留,不会丢失场景一,资金转账(银行 / 微信 / 支付宝 / 平台余额划转),上面提到了,不再具体解释。
场景二,电商下单支付(最经典业务场景),业务流程:1.创建订单记录,2.扣减商品库存,3.冻结 / 扣除用户余额,4.生成支付流水记录。如果不采用事务,可能会导致1.订单生成了,库存没扣 → 商品超卖。2.库存扣了,订单创建失败 → 凭空少库存、产生无效库存亏损等问题。
场景三, 售后退款 / 退货,业务流程:1.修改订单为退款状态;2。退还用户余额 / 原路退款;3.恢复商品库存;4.生成退款流水。如果不采用事务,可能会导致钱退了库存没恢复,或库存恢复了钱没退,账务库存混乱。
场景四,积分 / 会员权益变更,业务流程:签到加积分、消费扣积分、积分兑换礼品:扣用户积分 + 生成兑换记录 + 扣减礼品库存。如果不采用事务,可能会出现积分扣了没兑换记录,或兑换成功没扣积分,积分账目错乱。
场景五,充值 / 缴费(手机话费、水电煤、会员充值),业务流程:扣用户钱包余额 → 服务商账户入账 → 生成缴费订单。
场景六,批量结算 / 月度对账 / 批量数据处理,业务场景:月底批量给商家结算佣金、批量导入账单、批量更新一批业务数据。如果不采用事务,可能导致批量更新到一半报错,一部分改了、一部分没改,数据乱七八糟,对账对不上。
场景七,分账 / 平台佣金结算,业务场景:订单成交后:平台抽佣金、商家入账、分销员分佣,多账户同时变动。
四、事务操作的关键语句
1. start transaction -- 开始一个事务2. savepoint 保存点名 -- 设置保存点3. rollback to 保存点名 -- 回退事务4. rollback -- 回退全部事务5. commit -- 提交事务,所有的操作生效,不能回退
借助上面的图来理解一下事务中的回滚操作,我们用下面表中的业务描述举一个举例,方便理解-- 1. 创建一个测试表CREATE TABLE t28( id INT, `name` VARCHAR(32));-- 2. 开始事务START TRANSACTION;-- 3. 设置保存点SAVEPOINT a;-- 执行dml操作INSERT INTO t28 VALUES(100, 'tom');SELECT * FROM t28;SAVEPOINT b;-- 执行dml操作INSERT INTO t28 VALUES(200, 'jack');
好的,现在我们开始了一个事务,执行了2次dml操作,设置了2个保存点,可以得到下面的图我们现存执行回退操作,让项目回退到b点,看效果如何?- 如果直接写一个ROLLBACK; 就表示回退到事务开始位置
但是一旦执行了commit操作,就会结束事务,会自动的删除该事务所定义的所有保存点。如果我在开始事务过程中依次设置了a-->b-->c保存点,我们可以先回退到c点,再回退到b点,但如果我们最先回退到a点,就无法再次回退至b点和c点。- 多个连接开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个连接在获取数据时的准确性。如果不考虑隔离性,可能会引发如下问题:一是脏读,二是不可重复读,三是幻读。
- 脏读:一个事务读取了另一个事务未提交的数据;如果另一个事务回滚后,当前事务读取到的数据就是无效 “脏数据”。
- 不可重复读:同一个事务内,两次读取同一行数据,结果不一致(因为中间被其他事务修改或删除并提交了)。
- 幻读:同一个事务内,两次执行相同的
SELECT查询,返回的结果行数不一样(因为中间被其他事务插入 / 删除了数据并提交)。
注意区分:不可重复读是「同一行数据变了」,幻读是「数据行数变了」说明:V 可能出现 x 不会出现。这个表中可重复读是有可能会出现幻读的情况的,这里要留意一下。
- 1. 读未提交:最低隔离级别,允许读取未提交数据,存在脏读、不可重复读、幻读问题
- 2. 读已提交:Oracle 默认隔离级别,解决脏读,存在不可重复读、幻读问题
- 3. 可重复读:MySQL 默认隔离级别,解决脏读、不可重复读,通过间隙锁解决大部分幻读问题
- 4. 可串行化:最高隔离级别,通过加锁强制事务串行执行,完全避免脏读、不可重复读、幻读,但性能较低
-- 查看会话级SELECT @@tx_isolation;-- 查看全局级SELECT @@global.tx_isolation;-- SHOW 命令SHOW VARIABLES LIKE 'tx_isolation';
1. MySQL 8.0+(当前主流版本,优先用这个)-- 查看【当前会话】的隔离级别(最常用,仅对当前连接生效)SELECT @@transaction_isolation;-- 查看【全局】的隔离级别(所有新连接的默认级别)SELECT @@global.transaction_isolation;-- 用 SHOW VARIABLES 直观查看SHOW VARIABLES LIKE 'transaction_isolation'; -- 会话级SHOW GLOBAL VARIABLES LIKE 'transaction_isolation'; -- 全局级
针对当前这个控制台/会话,设置隔离级别为读未提交 (READ UNCOMMITTED)-- 设置 当前会话 隔离级别为 读未提交SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SESSION 关键字:代表仅当前这个控制台生效。- 关闭这个窗口,重新打开MySQL 控制台,隔离级别会恢复默认(可重复读),不会影响其他连接、全局配置,非常安全,这是测试脏读、不可重复读、幻读的专用级别,生产环境绝对不要用
- 控制台 1:设置为
READ-UNCOMMITTED - 控制台 2:保持默认级别,分别执行事务,就能看到脏读现象
补充:全局设置(不推荐,仅了解),如果误操作想改回全局默认(MySQL 默认:可重复读):
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
现在我们在其中一个控制台中开启一个事务,建立一个表,分别从两个控制台上查看这个表,对比结果现在,我们在左侧控制台向表中添加一条数据,并不进行提交,但是从右侧的控制台中却能看到,这就是脏读我现通过左侧控制台修改和并添加一条数据,提交一下。再由右侧控制台查看一下看到效果后,你想是否有什么不对!左右两侧的控制台分别是两个人连接的同一个数据库进行操作的。左侧的人一直在对数据进行修改和调整,而侧的哥们儿始终没有进行操作,但是却一直在看到数据变动,这就慌了啊!好的,现在我们将右侧隔离级别改成读已提交(READ COMMITTED),左右两端同时开启一个事务,在左侧添加一条数据,从两个控制台查看现在我们从左侧控制台再修改一条数据,将id=200的money变为1800,并提交。从右侧查看- 锁是数据库为了解决并发冲突设计的「并发控制工具」,核心作用只有一个:保证多个事务同时操作数据时,数据不会被改乱。加锁不加锁与隔离级别是相互配合的关系,而不是绑定关系
我们再回到这个表,此时,我们用读未提交和读已提交进行了举例,可重复读我们就不再展开说,现在脏读、不可重复读与幻读,都有了概念,就只剩下加锁和不加锁之间的区别了。我们将右侧隔离级别改成可串行化(Serializable),两端都开启一个事务,在左侧添加一条数据,同时修改一条数据,如果我们不进行commit提交,从右侧进行查询时,光标就会一直闪动没有结果。这就是加锁的效果,如果左侧事务不提交,右侧就得不到查询结果。