在同一个事务中先使用平常的SELECT语句查询数据,随后再对刚才查询的数据执行更新或者插入的操作,在这种情况之下InnoDB并没有提供足够的安全保护,因而其他事务也可以对刚才查询的数据执行更新或者删除操作。为了给SELECT查询语句提供额外的安全保护,InnoDB支持两种类型的有锁查询:
SELECT ... FOR SHARE 假设会话A对执行读操作的数据行设置一个共享锁,其他会话B也可以读取相同的数据行,但是不能修改相同的数据行,直到会话A提交事务完成,会话B才能修改相同的数据行,如果会话A已经修改了其中的一些数据行,但是还没有提交事务,则会话B必须等待会话A提交事务完成,再读取最新的数据版本。 |
SELECT ... FOR UPDATE 检索操作遇到的所有索引记录以及索引实体,都设置了一个锁,类似于运行一个平常的更新语句,在这种情况下,其他事务将被阻塞对这些行记录的平常更新、对这些行记录执行SELECT…FOR SHARE、对这些行记录执行读操作(在某种事务隔离级别下),但是,一致性读将忽略这些行记录的锁。 |
以上这些语句在处理树形结构数据或者图结构数据的场景中非常有用,这些数据可能存储在单一的数据表或者多个关联的数据表中,使用有锁读的机制,可以保证在一个事务中对这些数据结构执行全局性的调整,例如,移动一些树形结构的分支。
由FOR SHARE或者FOR UPDATE语句查询设置的锁在事务提交或者事务回滚之后释放,有锁读只能在自动提交事务属性关闭的情况下有效,例如,设置autocommit等于0,或者使用START TRANSACTION语句开启一个新的事务。
在嵌套语句的场景中,每层语句都需要各自显式地使用有锁读,外层语句与子层语句之间不相互影响,举例如下所示:
如上所示,t1使用有锁读机制,t2不使用有锁读机制。
如上所示,t1使用有锁读机制,t2使用有锁读机制。
有锁读示例
假设,在child表中插入一条新的记录,需要确定该记录与上级表的关联关系在parent表是否存在(child表的上级表是parent表),如果使用一致性读先查询parent表并验证上下级的关联关系是否存在,再插入新记录到child表中,这是一种不安全的处理方式,因为,在查询与插入之间的时间段,可能存在其他事务将上下级的关联关系从parent表中删除。为了避免数据不安全的问题发生,则使用SELECT …FOR SHARE有锁读的方式,举例如下所示:
如上所示,有锁读返回上级的关联关系Jones,随后,可以安全地在child表插入新的下级关系并提交事务,在此期间,其他事务从parent表中获取排他锁,都需要等待前面的事务提交完成(child表与parent表的关联关系的数据最终达成一致)。
假设,存在表child_codes,该表的作用是提供整数计数器,输出一个唯一性的ID,用于标识插入到child表中每条唯一性的记录,在该场景中不能使用SELECT…FOR SHARE有锁读的方式,因为可能存在两个不同的事务同时从child_codes表中读到相同的值,从而在child表中插入记录的时候发生重复键的错误。为了解决该问题,需要使用SELECT…FOR UPDATE有锁读的方式,先以排他锁的方式读出记录,再以排他锁的方式更新记录,举例如下所示:
如上所示,第一条语句读出最新的可用版本,并在每行记录中设置排他锁。MySQL也提供函数支持类似的原子操作,举例如下所示:
有锁读并发性
如果一行记录被锁住,则使用SELECT ... FOR UPDATE语句或者SELECT ... FOR SHARE语句获取相同锁的时候,需要等待前面的事务释放对应的锁。MySQL也提供额外的选项支持不需要等待释放锁的场景,其描述如下所示:
NOWAIT 不需要等待其他事务释放锁,如果锁已被其他事务获取,则当前事务立刻返回错误 |
SKIP LOCKED 不需要等待其他事务释放锁,如果锁已被其他事务获取,则当前事务立刻返回没有被锁住的行记录 |
以上两种选项只支持行级锁,该操作是不安全的操作,需要根据实际的使用场景确定,举例如下所示:
如上所示,会话1增加三行记录到t表中,并锁住了i值等于2的行记录,会话2使用NOWAIT的选项获取锁,则立刻返回错误的提示,会话3使用SKIP LOCKED的选项获取锁,则立刻返回未被锁住的记录(i的值等于1、3)。