MySQL事务、并发问题及事务隔离
一、什么是事务
1、事务概念:
事务是包含了一组有序的数据库操作命令的序列,它是数据库并发操作的最小控制单位。
2、事务特性(ACID):
- 原子性:事务包含的数据库操作命令要么都执行,要么都不执行。
- 一致性:当事务完成时,数据库处于稳定而一致的状态。即事务执行后,数据库数据要符合规定,而且所有数据查询的结果是一致的,数据没有被破坏。
- 隔离性:多个事务同时执行时,他们之间是互不干扰的。
- 持久性:一旦事务提交成功,它引发的变化也就永久保存了下来,硬件与应用程序发生错误也不能改变。
3、事务操作命令:
二、事务并发的问题
- 1、脏读:一个事务读到另一个事务未提交的数据(事务A和B并发执行,B事务执行更新后,A事务查询B事务没有提交的数据,B事务回滚,则A事务得到的数据不是数据库中的真实数据,也就是脏数据。)。
- 2、不可重复读:一个事务两次读取同一行,得到的数据却不同(事务A和B事务并发执行,A事务查询数据,然后B事务更新了该数据,A再次查询该数据时,发现该数据变化了)。
- 3、幻读:一个事务两次读取同一个范围内的记录,得到的结果集不一样(A和B事务并发执行,A事务查询数据,B事务插入或者删除数据,A事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了)。
- 4、覆盖更新:一个事务覆盖另一个事务已提交的更新数据(A事务更新数据,然后B事务也更新该数据并提交,A事务提交,B事务的更新被覆盖了)。
三、四种事务隔离级别
1、Serializable(串行化):一个事务在执行过程中不允许别的事务并发执行,后启动的事务只能等前一个事务执行完成之后,才能继续执行。
2、Repeatable Read(可重复读):一个事务在执行过程中看到的数据,总是和这个事务在启动时看到的数据是一致的。
3、Read Commited(读已提交):一个事务提交之后,它做的数据变更才能被其它事务看到。
4、Read Uncommitted(读未提交):一个事务还未提交时,它做的数据变更就能被其它事务看到。
- | 脏读 | 不可重复读 | 幻读 | 覆盖更新 |
---|---|---|---|---|
未提交读 | √ | √ | √ | √ |
已提交读 | × | √ | √ | √ |
可重复读 | × | × | √ | √ |
串行化 | × | × | × | × |
四、查看/修改隔离级别
- MYSQL默认隔离级别是:Repeatable Read
- 如何查看/修改隔离级别?
-- 查看全局隔离级别
SELECT @@global.tx_isolation;
-- 查看当前会话隔离级别
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;
-- 设置隔离级别
SET @@tx_isolation = 'REPEATABLE READ';
SET @@global.tx_isolation = 'REPEATABLE READ';
-- 查看当前的事务
select * from information_schema.innodb_trx;
-- 查看当前等锁的事务
select * from information_schema.innodb_lock_waits;
-- 删除事务
kill 进程号;
-- 未提交事务自动回滚时间
set innodb_lock_wait_timeout = 10;
如果报错"tx_isolation"变量不存在,可用“transaction_isolation”替换。
五、数据库是如何保证事务的隔离性的?
1,“串行化”隔离级别通过加读写锁实现,但是频繁的加锁,会大大降低了数据库性能。
2,“读已提交”、“可重复读” 都是基于MVCC实现的,相对于加锁简单粗暴的方式,它用更好的方式去处理读写冲突,能有效提高数据库并发性能。
多版本并发控制 MVCC(Multiversion Concurrency Control),用来实现一致性的非锁定读。通过维护一个数据的多个版本,减少读写操作的冲突。
MVCC是基于Undo Log(回滚日志)、隐藏字段、Read View(读视图)实现的。
当前读和快照读
当前读: 读取数据的最新版本,并对数据进行加锁。
例如:insert、update、delete、select for update、 select lock in share mode。
快照读: 读取数据的历史版本,不对数据加锁。
例如:select
读已提交:每次select,总是读取被锁定行的最新一份快照数据。
可重复读:每次select,总是读取事务开始时的快照数据版本。
六、“可重复读”为什么会出现“幻读”?
既然可重复读总是读取事务开始时的快照,为什么会出现“幻读”呢?
例如,有一个users表
id | name | age |
---|---|---|
1 | 张三 | 60 |
2 | 李四 | 70 |
1,在其它事务插入数据之后使用了加锁:select for update、 select lock in share mode。
事务A | 事务B |
---|---|
begin; | |
1,select * from users where age > 50; 两条数据(张三、李四) | |
2,insert into users(“name”, “age”) values(“王五”, 80) | |
3,select * from users where age > 50 for update; 三条数据(张三、李四、王五),出现了幻读 |
2,更新了范围内的数据,触发了当前读,如:
事务A | 事务B |
---|---|
begin; | |
1,select * from users where age > 50; 两条数据(张三、李四) | |
2,insert into users(“name”, “age”) values(“王五”, 80) | |
3,update users set age=age+1 where age > 50; | |
4,select * from users where age > 50; 三条数据(张三、李四、王五),出现了幻读 |
解决办法:
事务一开启之后就加锁(生成间隙锁),防止事务过程中其它事务插入:
select * from users where age > 50 lock in share mode;
七、如何解决“覆盖更新”?
问题如下,事务A的更新被覆盖,造成经济损失:
事务A | 事务B |
---|---|
begin; | begin; |
1,select money from account where user_id=1; | 1,select money from account where user_id=1; |
业务1处理…,扣款20,余额为80 | 业务2处理…,扣款10,余额为90 |
2,update account set money=80 where user_id=1; | |
commit; | 3,update account set money=90 where user_id=1; |
commit; |
1,直接利用sql自身的约束
update account set money=money-20 where user_id=1
2,使用乐观锁
乐观锁通过在每次更新数据时检查数据是否在读取后被修改来避免丢失更新问题。常用的方法是添加一个版本号或时间戳字段,每次更新时检查版本号是否一致,不一致则说明数据已被其他事务修改,更新失败。
3,使用排他锁
注意:这里有个巨坑,看到很多文章也都没说清楚,只说在查询时使用排它锁,防止其它事务查询,如在事务A中:
SELECT money FROM account WHERE user_id = 1 FOR UPDATE
然并卵,排他锁指的是其他事务不能再在其上加其他的锁。而select语句默认不会加任何锁类型,所以事务B还是可以通过select …from…查询到数据,因为普通查询没有任何锁机制。
所以要使用排它锁解决覆盖更新的问题,必须在所有涉及到查询account的表的相关事务中都加排它锁。