大数跨境
0
0

批量转账导致的db死锁问题

批量转账导致的db死锁问题 跨境人老刘
2025-10-20
39
导读:在ToC包含多虚拟币业务系统中,很常见C2C的转账业务:将虚拟币余额数据从A账户转到B账户。
在ToC包含多虚拟币业务系统中,很常见C2C的转账业务:将虚拟币余额数据从A账户转到B账户。
在此业务货币流转过程中,为保证数据一致性,在常规方案中(不考虑上分布式事务),用上本地事务+补偿脚本方式尽可能达到最终一致性。那么,在本地事务中,如果遇到一些并发的业务,对数据更新时序不同,会有不同的影响。
如果使用不当,或者业务代码设计考虑不周全的话,还会出现死锁问题。
以下由一个转账接口引发了db的死锁问题,进行总结思考。

在 MySQL 中,死锁(Deadlock)是指在多个事务在执行过程中 互相等待对方持有的资源,从而导致所有事务都无法继续执行的一种并发控制问题。死锁是数据库在进行并发操作时常见的现象,尤其在使用事务和锁机制时容易出现。

LLM大模型

业务异常场景
转账接口成功率下降,出现预警信息通知,人工介入排查日志:
error-2024122220.log: 2024-12-2220:56:30.859 flow/user_coin_dao.go:141ERR0R flow|UserCoinDao|addCoin|db update error.uid:110,coin_kind:start_moonnum:14100,err:Error 1213:Deadlock to get lock;try restarting transaction {"trace_id":"4e0598be598e37eb315d6cc2"}
error-2024122220.1og:2024-12-22 20:56:30.866 transfer_dao.go:136 ERROR 设置加币失败状态 to ORDER_STATUS_FAIL Failed:Error 1213Deadlock found when trying to gensToPinfo'"ID":5891"OrderID":"AA220590753907966737195634867197 TransactionTime":"2024-12-22T20:56:31+08:00 BackTime 2024-12-22T20:56:31+08:00 ceDesc“流转-星币-\u003e星钻-送礼"CreatedAt":"2024-12-22T20:56:31+08:00","UpdatedAt":"2021-12-22T20:56:31+08:00"""raw_query"headers"to"":[{"uid":1003249,"coin_kind":"start_moon","num":150,"origin_from_kind_num":150,"ex":""},{"uid":1005051,"coin_kind":"start_moon","num":750,"origin_from_kind_num":750,"ex"""},...
发现明显的db数据库层面的错误描述,出现了死锁。依据死锁的概念,说明接口业务更新的数据,出现了互相等待。
进一步从当前接口的转账职能可知道,初步判定大概率是由于当前接口并发情况下,对多个账户的余额更新,时序上出现加锁和等待的情况
当然了,还需要根据其他维度的QPM监控指标进行多方综合的信息收集和推测:
可以看到接口的QPS不高,但是接口成功率是有异常下降的。尽可能排除了一些外部攻击,服务压力导致资源性能不足等客观因素。

原因排查分析
transfer接口一对多转账,在单个事务对多行做更新操作,查看代码及日志,未对多个加减币用户按uid排序,由于在事务中加的锁在事务结束时才会释放,因此出现死锁。
简化问题,A事务按uid 1、uid 2顺序加币,B事务按uid 2、uid 1顺序加币,A锁住uid 1行、B锁住uid 2行,此时出现死锁。
根据接口异常的日志,查找dao层的相关用户的货币更新流程,可以按照并发事务中的时间线,分别罗列出来,方便查看。(下面两个图可以看成是两个并发事务对用户余额的分别更新操作)
所以基本能确认是由于在转账接口的并发处理过程中,两个事务对多个用户的余额更新在时间上的交错,先后次序加锁导致行记录阻塞等待,出现了死锁现象。
解决方案
1.  业务上事务内流程的功能顺序优化(尽可能降低并发造成锁原因)
转账中先插入流水,后更新余额,减少事务间锁等待:
开启事务加行锁,改余额插流水提交事务,释放锁
2.  并发事务内,对业务数据顺序的优化
事务中批量更新,为避免死锁,需按主键排序,依次更新
例如本次虚拟币转账业务,在接口中,需要先对用户排序,后在进行事务更新,如此,在并发下,在db层面会保证其中一个请求获取锁失败阻塞,等待上一个事务处理完后在进行处理。

并发事务建议
在使用 MySQL 的并发事务(尤其是基于 InnoDB 存储引擎)时,为了减少死锁、提高性能、保证数据一致性与系统稳定性,在业务设计和代码实现中需要注意以下关键点:
  • 事务设计原则
    保持事务短小,尽量减少事务的执行时间,避免长时间占用锁资源。
    使用一致性事务逻辑,多表操作,确保事务处理顺序一致,避免不同事务有不同顺序加锁行为

  • 加锁顺序建议
    为了消除死锁风险,应该:
    统一加锁:在不同事务中按照相同的顺序获取锁,避免形成循环等待
    避免嵌套事务:可能会增加锁冲突的概率,难以控制并发。

  • 锁类型与锁行为
    尽量使用行级锁,而不是全表锁。不要为同一行授予多个锁,会增加死锁发生的可能性。不要使用 SELECT FOR UPDATE 时完全不加条件,指定明确WHERE条件。

  • 事务并发控制策略
    控制事务的并发量:同一时间不要让太多事务同时执行,可以在业务层使用限流、队列控制等方式来减少并发请求冲击。
    设置合理的锁等待超时时间:可以通过 innodb_lock_wait_timeout 控制事务等待的超时时间(单位为秒)。

  • 数据库配置建议
    1. 启用死锁日志功能:
SHOW ENGINE INNODB STATUS;
SET GLOBAL innodb_status_output = ON;SET GLOBAL innodb_status_output_locks = ON;
    2. 设置锁冲突监控:(MySql 8.0+)
SHOW ENGINE INNODB DEADLOCKS;
 
综上所述,如果在并发事务中,没有处理好事务加锁顺序一致性问题的话,在某些情况下,还是会触发死锁,影响业务。所以,在日常研发过程中,遇到并发事务,事务内有多个表的更新情况时候,要特别注意表的加锁顺序,避免遇到死锁。 

【声明】内容源于网络
0
0
跨境人老刘
跨境分享录 | 长期输出专业干货
内容 40120
粉丝 3
跨境人老刘 跨境分享录 | 长期输出专业干货
总阅读169.5k
粉丝3
内容40.1k