高并发环境中mysql的死锁问题会直接导致事务阻塞甚至回滚,影响业务正常流转,而索引的设计和使用是影响锁范围的核心因素,合理的索引策略能从根源上减少死锁发生的概率。
mysql死锁与索引的关联原理
mysql的innodb引擎默认采用行级锁,锁的加锁范围依赖于查询使用的索引。如果查询没有命中索引,会触发全表扫描,此时innodb会对所有扫描到的行加锁,甚至升级为表级锁,大幅提升死锁风险。当多个事务按照不同顺序对相同范围的行加锁时,就会形成死锁闭环。
比如两个事务同时操作同一张用户表,事务A先更新id为1的记录再更新id为2的记录,事务B先更新id为2的记录再更新id为1的记录,若两个更新语句都没有命中索引,就会扫描全表加锁,最终形成死锁。
通过索引规避死锁的核心方法
1. 为查询条件添加有效索引
所有涉及更新、删除的where条件字段,都需要建立合适的索引,避免无索引导致的全表锁。同时要注意索引的选择性,选择性低的字段不适合单独作为索引,否则还是会扫描大量行。
以下是创建用户表索引的示例,为经常作为更新条件的user_id和status字段建立联合索引:
-- 创建用户表 CREATE TABLE `user_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(32) NOT NULL COMMENT '用户唯一标识', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '用户状态 1正常 2冻结', `balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '账户余额', PRIMARY KEY (`id`), -- 建立user_id和status的联合索引,覆盖更新场景的查询条件 KEY `idx_user_status` (`user_id`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; -- 基于索引的更新语句,只会锁定匹配的行,不会全表加锁 UPDATE user_info SET balance = balance - 100 WHERE user_id = 'u_123' AND status = 1;
2. 避免索引失效导致锁范围扩大
很多写法会导致索引失效,进而让查询从索引扫描变成全表扫描,扩大锁范围。常见的索引失效场景包括:对索引字段使用函数、字段类型隐式转换、like左模糊匹配、or条件中部分字段无索引等。
比如以下语句会导致idx_user_status索引失效,触发全表扫描加锁:
-- 错误写法:对索引字段使用函数,索引失效 UPDATE user_info SET balance = balance - 100 WHERE SUBSTR(user_id, 1, 3) = 'u_1' AND status = 1; -- 错误写法:user_id是varchar类型,传入数字会触发隐式转换,索引失效 UPDATE user_info SET balance = balance - 100 WHERE user_id = 123 AND status = 1;
3. 控制事务中索引访问的顺序
高并发下即使有索引,如果多个事务访问相同行的顺序不一致,还是可能产生死锁。可以通过统一事务中操作的索引顺序来规避,比如所有事务都按照user_id升序的顺序操作记录。
高并发场景下的锁优化策略
1. 缩小事务持锁时间
尽量把事务中的非数据库操作(比如接口调用、日志处理)移到事务外部,减少锁的持有时间。同时避免在一个事务中执行过多更新操作,拆分大事务为多个小事务。
2. 合理选择锁粒度
如果业务允许,优先使用行级锁,避免使用表级锁。对于读多写少的场景,可以开启innodb的MVCC机制,读操作使用快照读,不需要加锁,减少读写冲突。
3. 调整innodb锁相关参数
可以适当调整innodb_lock_wait_timeout参数,设置合理的锁等待超时时间,避免事务长时间阻塞。同时开启innodb_print_all_deadlocks参数,将死锁信息记录到错误日志,方便后续排查优化。
参数调整示例:
-- 查看当前锁等待超时时间 SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 设置锁等待超时时间为5秒,避免长事务阻塞 SET GLOBAL innodb_lock_wait_timeout = 5; -- 开启死锁日志记录 SET GLOBAL innodb_print_all_deadlocks = ON;
4. 使用乐观锁减少锁冲突
对于并发更新频率不高的场景,可以使用乐观锁替代悲观锁。在表中添加version字段,更新时带上version条件,只有version匹配才执行更新,更新成功后version加1。
乐观锁实现示例:
-- 查询时获取当前version SELECT id, balance, version FROM user_info WHERE user_id = 'u_123' AND status = 1; -- 更新时校验version,更新成功则version加1 UPDATE user_info SET balance = balance - 100, version = version + 1 WHERE user_id = 'u_123' AND status = 1 AND version = 1;
如果更新返回的影响行数为0,说明数据已经被其他事务修改,需要重试或者提示用户操作冲突。