ACID
原子性 锁机制和日志实现 一组操作要么一起完成,要么都不完成。即使出现故障也能保证状态的一致性。
隔离性 锁机制实现
一致性 锁机制实现
持久性 日志实现 事务对数据库的影响应该是永久的,即使出现错误也可以回复
之所以有这些特性纯粹是出于现实需要,比如转账的场景,需要对于多个表进行多次操作,又必须保证这些操作同时完成
与恢复相关的数据结构(存储到磁盘中)
提交事务列表(记录事务的执行状态)
执行事务列表(记录事务的执行状态)
日志(记录事务的执行信息)
日志应该记录的信息
tid tid对应的前向列表(记录修改块的旧值和地址)tid对应的后向列表(记录修改块的地址和新值)
规则
- 在事务提交之前,必须把修改写入日志中
- 在修改数据库之前,必须把修改记录的旧值写入日志中
恢复策略
redo 操作 使用日志中的新值
undo 操作 使用日志中的旧值
上述操作都具有幂等性,即一个事务重做多次或撤销多次的效果相同
更新策略
- 修改直接改数据库
事务开始时获取tid加入活动列表,
每次修改数据库前把写入旧值到日志,
之后直接修改数据库,
commit时先加入提交列表,再弹出活动列表
发生故障时,如果没提交,就undo
如果已经提交但活动列表中未弹出, 直接弹出
如果已经提交,说明不需要恢复
- 检查点:每隔一段时间,检查一部分事务,使得恢复时不需要检查所有事务,只需要检查最新的检查点起的事务
- 提交时修改数据库(并发性更高,排他锁的时机可以推迟)
数据库更新时把修改的旧值和新值加入到日志中
commit时先把tid加入到提交列表中
再把所有修改写入数据库中
tid在活动列表中删除
发生故障时,如果未提交,说明事务执行失败,直接从执行列表中删除即可。如果已提交但未从执行列表中删除,重做事务,从执行列表中删除。如果正常提交,说明不需要恢复。
- 间隙并发修改数据库
使用一个后台线程在磁盘空闲时将修改写入数据库
执行修改时新值和旧值都写入日志,同时后台线程会间隙的写入磁盘
在提交时再把没写完的修改写入磁盘
故障发生时,与策略2的区别仅在于未提交时需要undo
已提交未写完需要redo
正常提交不需要恢复
悲观锁
一个基本的思路就是加锁,当前事务执行时锁住被影响的数据,等事务执行完毕后在解锁,这也就是所谓的悲观锁策略。
根据锁的数据范围,锁分为表锁和行锁,并不是所有的数据库引擎都支持行锁,但MySQL默认的InnoDB引擎是支持的,并且不需要显示的指定使用行锁(但可以显示地指定使用表锁)。
值得一提的是,锁住的数据范围和sql语句是相关的,并且是和索引相关的,如果where子句条件中的列没有相关的索引,那就只能使用表锁。(因为行锁的实现实际是给索引项加锁)
同样,根据操作的不同,锁的排斥级别也不同,可以分为读锁(共享锁)和写锁(排他锁),读锁认为这个事务只对这一部分数据进行读操作不会更新数据,因此可以共享,由其他事务读取;写锁代表认为这个事务会对这部分数据更新,因此需要在执行阶段就锁住并禁止其他事务访问。
更新操作会默认加上写锁。查询操作则不会。
数据库判断数据范围不同的锁是否存在冲突是根据另一个叫意向锁的东西,比如已经有一行读锁,又要对整个表进行申请写锁时就会失败,因为整个表已经有了读的意向锁。这属于数据库内部机制,不用进行操作。
乐观锁
另一个思路就是执行时不加锁,在提交时比较当前数据和执行开始时的数据是否一致:如果一致,说明没有其他事务进行修改,提交完成;如果不一致,那么影响的行数为0,回滚并重试。
乐观锁的实现机制不需要数据库提供(但是提交的影响行数需要获取),而是由应用程序的逻辑控制。通常是在表中增加一个版本号的属性,当执行更新操作时比较版本号是否是预期的版本号(事务执行开始时先获取版本号),然后每次更新时版本号加1,如果版本号不是预期版本号,影响行数为0,事务执行失败。然后回滚,重试。
事务执行开始时先获取版本号,就相当于获取了锁,只是这是一种协同锁,需要所有事务都遵循“没人修改我再改”的规则才能保证有效。
死锁
死锁的问题本质还是资源竞争
如果使用悲观锁,并且所有要访问的数据的锁不在所有操作之前获取,都有可能发生死锁:比如一个事务在执行一部分的数据更新后,要获取另一个事务锁住但未提交的数据,同样另一个事务接下来也要访问这个事务已锁住的数据,这样两个事务都会阻塞无法进行下去,形成死锁。
乐观锁对资源竞争的处理相对简单,因为没有实际加锁,总是能获得新的版本号,如果不能获取预期的版本号,就重新进行事务。
悲观锁就必须要保证在获取所有锁和释放所有锁在事务的开始和结束时一起进行的,最简单的做法就是事务开始时使用表锁锁住要访问的所有表,当然,这会对性能有所影响。如果使用行级锁,即使不需要查询也要给使用查询语句给相关数据加锁,再执行对数据的操作。
MVCC
数据库内部实现的乐观锁机制,用于实现不同的隔离级别(最高到可重复读,可串行化需要别的机制保证)。实际上每一行记录都有隐藏的当前版本号和删除版本号,每次事务的读取都是在记录当时的一个快照上进行的。
这个机制只保证了有限的隔离性(比如可重复读仍然存在幻读的问题),并不能完全保证事务的数据一致性,原子性等,因此还需要加锁的机制进行控制
总结
悲观锁(使用数据库提供的锁机制) |_____表锁(隐式,可显示指定),行锁(需要索引) |_____读锁/写锁(更新时隐式排他锁,可显示指定) 乐观锁(不加锁,比较标识,失败则重试)