十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
加锁情况与死锁原因分析
创新互联建站拥有10余年成都网站建设工作经验,为各大企业提供成都做网站、成都网站设计服务,对于网页设计、PC网站建设(电脑版网站建设)、成都app开发、wap网站建设(手机版网站建设)、程序开发、网站优化(SEO优化)、微网站、主机域名等,凭借多年来在互联网的打拼,我们在互联网网站建设行业积累了很多网站制作、网站设计、网络营销经验,集策划、开发、设计、营销、管理等网站化运作于一体,具备承接各种规模类型的网站建设项目的能力。
为方便大家复现,完整表结构和数据如下:
CREATE TABLE `t3` (
`c1` int(11) NOT NULL AUTO_INCREMENT,
`c2` int(11) DEFAULT NULL,
PRIMARY KEY (`c1`),
UNIQUE KEY `c2` (`c2`)
) ENGINE=InnoDB
insert into t3 values(1,1),(15,15),(20,20);
在 session1 执行 commit 的瞬间,我们会看到 session2、session3 的其中一个报死锁。这个死锁是这样产生的:
1. session1 执行 delete 会在唯一索引 c2 的 c2 = 15 这一记录上加 X lock(也就是在MySQL 内部观测到的:X Lock but not gap);
2. session2 和 session3 在执行 insert 的时候,由于唯一约束检测发生唯一冲突,会加 S Next-Key Lock,即对 (1,15] 这个区间加锁包括间隙,并且被 seesion1 的 X Lock 阻塞,进入等待;
3. session1 在执行 commit 后,会释放 X Lock,session2 和 session3 都获得 S Next-Key Lock;
4. session2 和 session3 继续执行插入操作,这个时候 INSERT INTENTION LOCK(插入意向锁)出现了,并且由于插入意向锁会被 gap 锁阻塞,所以 session2 和 session3 互相等待,造成死锁。
死锁日志如下:
请点击输入图片描述
INSERT INTENTION LOCK
在之前的死锁分析第四点,如果不分析插入意向锁,也是会造成死锁的,因为插入最终还是要对记录加 X Lock 的,session2 和 session3 还是会互相阻塞互相等待。
但是插入意向锁是客观存在的,我们可以在官方手册中查到,不可忽略:
Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.
插入意向锁其实是一种特殊的 gap lock,但是它不会阻塞其他锁。假设存在值为 4 和 7 的索引记录,尝试插入值 5 和 6 的两个事务在获取插入行上的排它锁之前使用插入意向锁锁定间隙,即在(4,7)上加 gap lock,但是这两个事务不会互相冲突等待。
当插入一条记录时,会去检查当前插入位置的下一条记录上是否存在锁对象,如果下一条记录上存在锁对象,就需要判断该锁对象是否锁住了 gap。如果 gap 被锁住了,则插入意向锁与之冲突,进入等待状态(插入意向锁之间并不互斥)。总结一下这把锁的属性:
1. 它不会阻塞其他任何锁;
2. 它本身仅会被 gap lock 阻塞。
在学习 MySQL 过程中,一般只有在它被阻塞的时候才能观察到,所以这也是它常常被忽略的原因吧...
GAP LOCK
在此例中,另外一个重要的点就是 gap lock,通常情况下我们说到 gap lock 都只会联想到 REPEATABLE-READ 隔离级别利用其解决幻读。但实际上在 READ-COMMITTED 隔离级别,也会存在 gap lock ,只发生在:唯一约束检查到有唯一冲突的时候,会加 S Next-key Lock,即对记录以及与和上一条记录之间的间隙加共享锁。
通过下面这个例子就能验证:
请点击输入图片描述
这里 session1 插入数据遇到唯一冲突,虽然报错,但是对 (15,20] 加的 S Next-Key Lock 并不会马上释放,所以 session2 被阻塞。另外一种情况就是本文开始的例子,当 session2 插入遇到唯一冲突但是因为被 X Lock 阻塞,并不会立刻报错 “Duplicate key”,但是依然要等待获取 S Next-Key Lock 。
有个困惑很久的疑问:出现唯一冲突需要加 S Next-Key Lock 是事实,但是加锁的意义是什么?还是说是通过 S Next-Key Lock 来实现的唯一约束检查,但是这样意味着在插入没有遇到唯一冲突的时候,这个锁会立刻释放,这不符合二阶段锁原则。这点希望能与大家一起讨论得到好的解释。
如果是在 REPEATABLE-READ,除以上所说的唯一约束冲突外,gap lock 的存在是这样的:
普通索引(非唯一索引)的S/X Lock,都带 gap 属性,会锁住记录以及前1条记录到后1条记录的左闭右开区间,比如有[4,6,8]记录,delete 6,则会锁住[4,8)整个区间。
对于 gap lock,相信 DBA 们的心情是一样一样的,所以我的建议是:
1. 在绝大部分的业务场景下,都可以把 MySQL 的隔离界别设置为 READ-COMMITTED;
2. 在业务方便控制字段值唯一的情况下,尽量减少表中唯一索引的数量。
锁冲突矩阵
前面我们说的 GAP LOCK 其实是锁的属性,另外我们知道 InnoDB 常规锁模式有:S 和 X,即共享锁和排他锁。锁模式和锁属性是可以随意组合的,组合之后的冲突矩阵如下,这对我们分析死锁很有帮助:
请点击输入图片描述
以下五种方法可以快速定位全局锁的位置,仅供参考。
方法1:利用 metadata_locks 视图
此方法仅适用于 MySQL 5.7 以上版本,该版本 performance_schema 新增了 metadata_locks,如果上锁前启用了元数据锁的探针(默认是未启用的),可以比较容易的定位全局锁会话。
方法2:利用 events_statements_history 视图此方法适用于 MySQL 5.6 以上版本,启用 performance_schema.eventsstatements_history(5.6 默认未启用,5.7 默认启用),该表会 SQL 历史记录执行,如果请求太多,会自动清理早期的信息,有可能将上锁会话的信息清理掉。
方法3:利用 gdb 工具如果上述两种都用不了或者没来得及启用,可以尝试第三种方法。利用 gdb 找到所有线程信息,查看每个线程中持有全局锁对象,输出对应的会话 ID,为了便于快速定位,我写成了脚本形式。也可以使用 gdb 交互模式,但 attach mysql 进程后 mysql 会完全 hang 住,读请求也会受到影响,不建议使用交互模式。
方法4:show processlist
如果备份程序使用的特定用户执行备份,如果是 root 用户备份,那 time 值越大的是持锁会话的概率越大,如果业务也用 root 访问,重点是 state 和 info 为空的,这里有个小技巧可以快速筛选,筛选后尝试 kill 对应 ID,再观察是否还有 wait global read lock 状态的会话。
方法5:重启试试!
1、在mysql数据库中如何锁定一行数据,保证不被其他的操作影响。
2、从对数据的操作类型分为读锁和写锁。从对数据操作的粒度来分:表锁和行锁。
3、现在我们建立一个表来演示数据库的行锁讲解。
4、行锁基本演示如下图所示。
5、如果两个会话操作的是不同的行,就不会互相阻塞了。
下面通过一个例子来说明
场景如下:
用户账户有余额,当发生交易时,需要实时更新余额。这里如果发生并发问题,那么会造成用户余额和实际交易的不一致,这对公司和客户来说都是很危险的。
那么如何避免:
网上查了下,有以下两种方法:
1、使用悲观锁
当需要变更余额时,通过代码在事务中对当前需要更新的记录设置for update行锁,然后开始正常的查询和更新操作
这样,其他的事务只能等待该事务完成后方可操作
当然要特别注意,如果使用了Spring的事务注解,需要配置一下:
!-- (事务管理)transaction manager, use JtaTransactionManager for global tx --
bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
property name="dataSource" ref="dataSource" /
/bean
!-- 使用annotation定义事务 --
tx:annotation-driven transaction-manager="transactionManager" /
在指定代码处添加事务注解
@Transactional
@Override
public boolean increaseBalanceByLock(Long userId, BigDecimal amount)
throws ValidateException {
long time = System.currentTimeMillis();
//获取对记录的锁定
UserBalance balance = userBalanceDao.getLock(userId);
LOGGER.info("[lock] start. time: {}", time);
if (null == balance) {
throw new ValidateException(
ValidateErrorCode.ERRORCODE_BALANCE_NOTEXIST,
"user balance is not exist");
}
boolean result = userBalanceDao.increaseBalanceByLock(balance, amount);
long timeEnd = System.currentTimeMillis();
LOGGER.info("[lock] end. time: {}", timeEnd);
return result;
}
MyBatis中的锁定方式,实际测试该方法确实可以有效控制,不过在大并发量的情况下,可能会有性能问题吧
select id="getLock" resultMap="BaseResultMap" parameterType="java.lang.Long"
![CDATA[
select * from user_balance where id=#{id,jdbcType=BIGINT} for update;
]]
/select
2、使用乐观锁
这个方法也同样可以解决场景中描述的问题(我认为比较适合并不频繁的操作):
设计表的时候增加一个version(版本控制字段),每次需要更新余额的时候,先获取对象,update的时候根据version和id为条件去更新,如果更新回来的数量为0,说明version已经变更
需要重复一次更新操作,如下:sql脚本
update user_balance set Balance = #{balance,jdbcType=DECIMAL},Version = Version+1 where Id = #{id,jdbcType=BIGINT} and Version = #{version,jdbcType=BIGINT}
这是一种不使用数据库锁的方法,解决方式也很巧妙。当然,在大量并发的情况下,一次扣款需要重复多次的操作才能成功,还是有不足之处的。不知道还有没有更好的方法。
当 web 日志中出现行锁超时错误后,很多开发都会找我来排查问题,这里说下问题定位的难点!1. MySQL 本身不会主动记录行锁等待的相关信息,所以无法有效的进行事后分析。2. 锁争用原因有多种,很难在事后判断到底是哪一类问题场景,尤其是事后无法复现问题的时候。3. 找到问题 SQL 后,开发无法有效从代码中挖掘出完整的事务,这也和公司框架-产品-项目的架构有关,需要靠 DBA 事后采集完整的事务 SQL 才可以进行分析。
MySQL 5.1支持对MyISAM和MEMORY表进行表级锁定,对BDB表进行页级锁定,对InnoDB表进行行级锁定。
如果不能同时插入,为了在一个表中进行多次INSERT和SELECT操作,可以在临时表中插入行并且立即用临时表中的记录更新真正的表。
这可用下列代码做到:
mysql LOCK TABLES real_table WRITE, insert_table WRITE;
mysql INSERT INTO real_table SELECT * FROM insert_table;
mysql TRUNCATE TABLE insert_table;
mysql UNLOCK TABLES;