数据库事务和锁机制
1. 事务及其特点
事务是操作数据库的一个程序执行单元,往往包含一组sql
语句。
- 原子性
事务所包含的这个组sql
,它的执行是原子性的,要么全成功,要么全失败,不存在中间状态,如果有失败就需要回滚成功的记录
- 隔离性
各个事务之间是相互隔离,互不影响的
- 持久性
事务对数据造成的修改会持久化到数据库中
- 一致性
从一个一致性状态转移到另一个一致性状态。事务前后,数据的状态发生了改变,但是这个是改变一致的,原子性,隔离性,持久性都是为了保证一致性
2. 事务的隔离级别及其带来的问题
事务隔离级别 | 带来的问题 |
---|---|
读未提交 | 脏读 |
读提交 | 不可重复读 |
可重复读 | 幻读 |
串行化 |
事务隔离级别从上到下,隔离级别越来越高,所带来的的性能开销也越来越大,mysql Innodb
默认的事务隔离级别是可重复读,并且通过MMVC
避免了读数据情况下的幻读问题。
2.1 脏读
事务A读取了事务B未提交的数据。事务B对数据进行了更改随后事务A读取了此数据,之后事务B又撤销了对数据的更改,此时又不会通知事务A,因此事务A拿到的是脏数据,称之为脏读。
时间顺序 | 事务A(收款) | 事务B(付款) |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询账户,余额为2000 | |
4 | 付款1000,余额为1000 | |
5 | 查询账户余额为1000 | |
6 | 扣款失败,回滚,余额还是2000 | |
7 | 收款1000(1000+1000=2000,余额为2000) | |
8 | 提交事务 | |
9 | 提交事务,余额2000 | |
备注 | 正确逻辑余额应该是3000,这里产生了脏读,导致余额出错 |
2.2 不可重复读
事务A在读取数据时,前后两次读取由于某些操作耗时较长,在前后两次读取的中间,其他事务对数据进行了更改,导致事务A前后两次读取的到的数据不一致(不重复),称之为不可重复读,不可重复读往往发生在数据的update
时,可采用行级锁来避免此种情况
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询年龄为23岁 | |
4 | 其他操作 | |
5 | 更改年龄为3岁 | |
6 | 提交事务 | |
7 | 查询年龄为3岁 | |
备注 | 正确逻辑两次查询的结果应该是一致的 |
2.3 幻读
事务A在前后两次读取数据总量的过程中事务B增加或删除了记录,导致事务前后两次获取到的记录总数不一致,就像产生了幻觉一样,称之为幻读,幻读一般发生在对insert
或delete
的时候,可锁表来避免此种情况
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询数据总量为200条 | |
4 | 其他操作 | |
5 | 插入300条 | |
6 | 提交事务 | |
7 | 查询数据总量为500条 | |
备注 | 正确的逻辑两次的查询结果应该是一致的 |
3. 悲观锁和乐观锁
3.1 悲观锁
对数据是否会被外界修改持保守态度,因此在数据处理过程中,锁定数据,通常依靠数据库提供的锁机制。读数据时加锁,防止其他事务修改数据;写数据时加锁,防止其他事务读数据库。悲观锁最大程度上提供了对操作数据的独占性,但也降低了程序的并发性能,特别对于长事务,这种开销是巨大的。
3.2 乐观锁
对数据库加锁采用比悲观锁更宽松的加锁机制。乐观锁大多是基于数据版本version
来实现的。记录额外增加几个version
字段,取数据时将version
一并取出,更新数据时version
加1,之后更新数据将之前取出的版本号和数据库现有版本号比较,只有当现有版本号大于数据库版本号的时候才能更新数据。
4. mysql的读写锁
mysql Innodb
引擎会对insert、update、delete
语句涉及到的数据自动的加上排他锁,select
默认不会加任何锁类型,但是可以手动加锁
4.1 读锁(共享锁)
多个事务共享一把锁,但是只能读,不能写
|
|
4.2 写锁(排他锁)
一个事务获取到了排他锁,其他事务就不能再获取该行数据的其他锁了
|
|
5. MVCC 多版本并发控制
MVCC
(multi-Version Concurrency control
)多版本并发控制是一种基于乐观锁实现的并发控制机制,包括mysql
在内的多种数据库都实现了这一机制,它解决的是读写锁带来的多个读操作或长时间的读操作导致饿死写操作的情况,事务中的读操作,保留的一份快照,使得多次读操作取到的值是相同的;而写操作不会覆盖原数据,操作的是一份新的数据版本,直到最终才会提交。