共享锁、排他锁
InnoDB 实现了两种类型的锁机制:共享锁(S)和排他锁(X)。共享锁允许一个事务读数据,不允许修改数据,如果其他事务要再对该行加锁,只能加共享锁;排他锁是修改数据时加的锁,可以读取和修改数据,一旦一个事务对该行数据加锁,其他事务将不能再对该数据加任务锁。
共享锁(S)
排它锁(X)
共享锁(S)
允许
不允许
排它锁(X)
不允许
不允许
x轴:事务A拥有的锁
y轴:事务B申请的锁
注:一个事务在某一行数据上加上排它锁后,其他事务不能再在这行数据加任何锁,无法进行 update 与 delete,但是普通的 select 是可以的,因为普通的 select 不加任何锁,当 select .... from ... for update 时才会加上排它锁。
并发事务带来的问题
1.数据丢失(两个事务同时修改一行数据,其中一个事务异常回滚,导致更新失败)
解决办法:未提交读(Read Uncommitted):在事务 B 读取数据时,事务 A 读取数据加了共享锁,修改数据时加了排它锁。这种隔离级别,会导致脏读、不可重复读以及幻读。
未提交读会导致脏读、不可重复读、幻读的原因:尽管 A 持有排它锁,避免了两个事务同时修改的情况,但是普通的 select 不持有锁,还是可以读到数据的,所以事务 A 多次修改数据,事务 B 中的查询还是可以看到的。
2.脏读(一个事务的执行期间,另一个事务读到了没有提交数据)
解决办法:已提交读(Read Committed):在事务 B 读取数据时增加了共享锁,一旦读取,立即释放锁,事务 A 读取修改数据时增加了行级排他锁,直到事务结束才释放锁。
也就是说,事务 B 在读取数据时,事务 A 只能读取数据,不能修改。当事务 B 读取到数据后,事务 A 才能修改。
这种隔离级别,可以避免脏读,但依然存在不可重复读以及幻读的问题。
已提交读会导致不可重复读、幻读的原因:事务 B 没有提交就将锁释放,导致事务 A 可以对数据进行任意修改,修改后,事务 B 再次获取共享锁查询,查询到跟之前读取不一致的数据。
3.不可重复读(一个事务的执行期间,另一个事务连续两次读取到的值不一样)
解决办法:可重复读(Repeatable Read):在事务 B 读取数据时增加了共享锁,事务结束,才释放锁,事务 A 读取修改数据时增加了行级排他锁,直到事务结束才释放锁。
也就是说,事务 B 在没有结束事务时,事务 A 只能读取数据,不能修改。当事务 B 结束事务,事务 A 才能修改。
这种隔离级别,可以避免脏读、不可重复读,但依然存在幻读的问题。
可重复读导致幻读的原因:可重复读是加的行级锁,而幻读是满足查询条件的前提下数据量发生变化,即发生 insert 或 delete操作。
4.幻读(一个事务的执行期间,另一个事务连续两次读取到的数量不同)
解决办法:可序列化(Serializable):在事务 A 读取数据时增加了共享锁,事务结束,才释放锁,事务 B 读取修改数据时增加了表级排他锁,直到事务结束才释放锁。
可序列化解决了脏读、不可重复读、幻读等问题,但隔离级别越来越高的同时,并发性会越来越低。
小结
可以看出,控制数据库并发带来的问题依赖的是锁粒度的扩大来解决的。
但是隔离级别越大,并发性就越差,如果业务场景允许的情况下,适当减小隔离级别是处理并发的一种手段。