锁类型
- 全局锁
全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本。
以前有一种做法,是通过FTWRL确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。
mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
但是MySAM不支持可重复读,只能通过FTWRL命令 - 表级锁
MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables … read/write,与FTWRL类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。读与读不互斥,读写互斥 -
行锁 S锁 共享锁(读锁) select … lock in share mode;
X锁 排他锁(写锁) select … for update;
IS IX 意向锁
若对某一行加X锁,那么首先对对应数据库、表加IX锁,那么后面的想对表再加X/S,表上有IX锁,说明本表或行上有X锁在,就不要遍历每一行了IS IX S X IS 兼容 兼容 兼容 不兼容 IX 兼容 兼容 不兼容 不兼容 S 兼容 不兼容 兼容 不兼容 X 不兼容 不兼容 不兼容 不兼容
行锁算法
Record Lock 单个行记录的锁(READ_COMMITTED级别默认)
Gap Lock 间隙锁,锁一个区间,不包含边界
Next-Key Lock 锁一个区间,前开后闭(REPEATABLE_READ级别默认,加行锁默认都是next key,只不过会优化成间隙锁和行锁)
加锁原则:
- 加锁的基本单位是:Next-Key Lock,在索引上加锁,加锁被访问到的数据
- 加完Next-Key Lock后,唯一索引上等值查询退化为行锁,非唯一索引退化为间隙锁
在主键上查询 select * from table where id = 1 for update;会聚集索引上加Record Lock
在辅助索引上查询 select * from table where test_index = 1 for update;会在主键/唯一索引上加Record Lock,辅助索引上加Next-Key Lock,
若test_index数据有{0,1,3},区间为不包含边界的上下两个值(0,3),所有在两个值时间再做插入{2}会阻塞
若两个事务查询 select * from table where test_index = 2 for update;,因为{2}不存在,两个事务均会获得Next-Key Lock,区间(1,3),而后同时插入{2}会发生死锁
在辅助索引上查询 select id from table where test_index = 1 for update; 和 select id from table where test_index = 1 lock in share mode;会不一样,由于只查询了id,所以不经过聚集索引,共享锁只会锁住辅助索引,排它锁会顺便锁住主键
因此 update t set d = d + 1 where id = 1;在排他锁时会阻塞,共享锁不会阻塞
深入理解与例子:加锁几个规则与优化 MySQL实战45讲/21讲
快照读 & 当前读
- 快照读(snapshot read)
简单的select操作(不包括 select … lock in share mode, select … for update)
快照读通过mvvc和undo log实现,读取事务开始时的数据 - 当前读(current read)
select ... lock in share mode select ... for update insert update delete
在上面的语句中会读取最新数据,并加上 next-key锁, 因此在应用层面没有使用当前读可能会造成丢失更新,如
//若findById 没有使用当前读,没有加上锁,那么后面的依赖历史数据的更新(user.getCount() + 1)会丢失
User user = userDao.findById(id);
user.setCount(user.getCount() + 1);
userDao.save(user);
//解决:1.用共享锁或排他锁,使之变成当前读 2.update user set count = count + 1;
锁使用场景
1、多对多关系需要相互关联的时候
需求:新增用户时需要关注某一类商品,用户、商品时多对多的关系
问题:当新增商品和新增用户并发时,新增用户时查询不到正在新增的商品
解决:"商品类别"加索引,新增某类商品时加上共享锁,此时会加上间隙锁
2、基于更新时间戳增量处理数据
需求:定时器每次处理某个时间段内的数据
问题:新增数据和扫描数据并发时,定时器获取到的数据会漏掉正在新增的数据
解决: 时间字段加索引,扫描数据时加上共享锁,也就是某个时间段内的间隙锁
死锁发生例子
-
两个事务先获取S锁,再进行写操作,X锁
T1 T2 获取a表S锁 获取a表S锁 修改数据 X锁(阻塞) 修改数据 X锁(死锁抛异常,rollback) 修改成功 -
某个事务需要同时得到两个锁
T1 T2 获取行a X锁 获取行b X锁 获取行b X锁(阻塞) 获取行a X锁 (死锁抛异常,rollback) 获取两个锁 查询成功 -
X 和 范围S锁
死锁检测抛异常,默认将undo量大的回滚
|T1|T2
|:---:|:---:|
|select * from company_file where id = 2 for update; |
| | select * from company_file where id < 20 lock in share mode;(阻塞,占了(*,2)的锁)
|insert into company_file select 0; (死锁抛异常,rollback) |
| | 查询成功
|T1|T2
|:---:|:---:|
|select * from company_file where id = 2 for update; |
| | select * from company_file where id < 20 lock in share mode;(阻塞,占了(*,2)的锁)
|insert into company_file select 19; (正常) |
|insert into company_file select 0; (死锁,但正常提交,检测到了死锁,undo量大,将T2回滚) |
| | 死锁抛异常,rollback
|T1|T2
|:---:|:---:|
|select * from company_file where id = 2 for update; |
|insert into company_file select 0; (正常) |
| | select * from company_file where id < 20 lock in share mode;(阻塞,占了(*,0)的锁)
|insert into company_file select 1; (正常) |
|insert into company_file select -1; (死锁,但正常提交,优先死锁之前有操作成功的) |
| | 死锁抛异常,rollback
死锁检测,wait-for graph,将等待的事务画图,若T1等T2,则画T1->T2,若同时T2->T1,成环,则回滚undo量最小的事务
锁升级
innodb 根据页进行加锁,资源(内存)开销小,不会进行锁升级
若表有3 000 000数据页,每页100条数据
若在数据上加锁,每个锁10字节,内存3G(sqlServer,升级为表锁)
页上加锁,每个30字节,90M