一、锁的作用
在多线程环境下执行i++,如果不加锁,大概率是得不到期望的结果。多线程并行对共享数据进行操作,会有线程安全的问题。对于数据库而言,多个客户端通过多个连接对数据进行操作,同样存在这个问题。分布式缓存Redis是单线程接受客户端请求,对于单个请求而言没有线程安全的问题。MySQL是支持多连接并行处理请求的,那么就面临线程安全的问题。多线程环境下,解决线程安全的问题,通常是用锁,悲观锁或者乐观锁,对于MySQL而言,使用乐观锁在应用层实现,通过版本号控制;悲观锁由数据库本身实现,下文介绍的是数据库本身设计的悲观锁。
二、锁分类
1.表级锁
元数据锁
分为读写锁,读锁与读锁不互斥,其他组合均互斥。对表执行DML的时候需要获取元数据读锁,对表执行DDL的时候需要获取写锁,在修改表结构的时候,是独占这张表的,不允许其他的DDL执行,也不允许DML执行,以保证数据的完整性。所以在执行DDL的时候要谨慎,选择业务低峰期,以避免执行DDL导致大量线上事务阻塞。
数据表锁
使用表锁的情况大致有两种:第一,在不支持行级锁的引擎中,比如MyISAM,只能用表锁。第二,在InnoDB中,能够利用索引的情况下会使用行锁,但如果使用表锁能带来更高的收益,那么就会选择表锁。比如,没有索引或者索引的区分度太低;全表更新;复杂事务,涉及多表级联,这些都有可能放弃行锁而选择表锁。表锁同样分为读锁和写锁,读锁与读锁之间不互斥,其余与写锁的组合都互斥。
2.行级锁
行级锁是InnoDB引擎支持的,通过对索引加锁,实现细粒度的控制,提高事务的并行度。不同的事务操作不同的行,对不同的行加锁,事务之间可以并行。
加锁过程
行级锁是加在索引上的,如果是主键索引,则直接锁定匹配的记录行。如果是二级索引,则先锁定匹配的二级索引记录,再根据id锁定对应的主键索引。具体的加锁规则根据隔离级别和索引类型而不同。
主键索引在RC级别和RR级别下,锁定对应的主键索引记录。
唯一索引在RC级别和RR级别下,锁定对应的唯一索引记录和对应的主键索引记录。
非唯一索引在RC级别下,锁定对应的二级索引记录和对应的主键索引记录。
非唯一索引在RR级别下,锁定对应的二级索引和对应的主键索引,并且会锁定二级索引记录之间的间隙,防止数据插入。
无索引在RC级别下,锁所有记录。
无索引在RR级别下,锁所有记录,并且加上记录间的间隙锁。
排他锁
排他锁是写锁,独占。
共享锁
共享锁是读锁,共享锁之间能并存,与排他锁互斥。
意向排他锁
意向锁之间是不互斥的。意向锁的存在是为了使得行锁与表锁能够高效的协调并存。假设有一个事务锁定了一行记录,另一个事务由于需要更新大量的记录,mysql判定需要全表扫描,于是去拿表级写锁,如果这个表有记录被行锁锁定,那么它不应该被表级写锁锁定。这就需要扫描,确认所有的行锁未被锁定,这样做显然太低效,于是有了意向锁。在获取排他锁之前先获取意向排他锁,在获取表锁之前只需要检查意向排他锁是否被锁定。如果被锁定,说明有行锁被锁定,这样就不需要对所有的行锁进行检查。
意向共享锁
意向共享锁的作用类似意向排他锁,在获取共享锁前需要先获取意向共享锁。
间隙锁
间隙锁主要是为了解决RR级别下的幻读问题。假设事务一更新数据,通过二级索引name=“张三”,定位到两条记录,那么锁定对应的二级索引和主键索引。另一个事务插入一条name="张三"的数据,提交事务二。事务一根据name="张三"去查询时,发现一条未被更新的数据,出现了幻读。解决这个问题的方案是在二级索引上锁定匹配到的记录的同时,锁定记录的区间,使得新的数据不能被插入。
三、死锁分析
在使用行锁过程中可能发生死锁。事务一持有行锁A,等待行锁B,事务二持有行锁B,等待行锁A,这就引发了死锁。如果不同的事务走了两个不同的二级索引,并且涉及相同的一批数据,拿锁的顺序又不相同,则有可能引发死锁。曾经就遇到过一个案例,定时任务多线程更新数据时出现了主键id冲突,原因就是多加了一个索引,两个更新请求又走了不同的索引。一般而言,最好是通过唯一标识单个的去更新数据。
本文围绕MySQL的锁展开。介绍了锁在多线程环境下解决线程安全问题的作用,阐述了表级锁(元数据锁、数据表锁)和行级锁(排他锁、共享锁等)的分类及特点,还说明了行级锁的加锁过程。最后分析了使用行锁时可能出现的死锁情况及案例。
1567

被折叠的 条评论
为什么被折叠?



