业务背景
广告主痛点的为进行一次全媒体联合投放,若投放10个媒体,需要制作和上传10+个创意、50+张不同尺寸和出血区要求的图片和视频素材、近100个元素,投放成本极高。这也是制约部分用户使用新产品投放的原因。
因此进行升级。以三个创意为例。广告主上传一个创意包,开发通过业务逻辑进行拆分为3个创意。整个过程用户只需要操作一次,体验感极佳。
在上面的图中,我们可以看到在右边一个创意包变为3个创意的时候,也是需要绑定到单元上。
其中创意包的绑定是用户触发。而后台绑定的操作是包绑定的binlog触发。
问题
在一个阳光明媚的中午,在没有做任何发布的中午,业务同学找过来因为死锁导致绑定创意失败了,有客诉。那必须要重视一下这个问题了。
Deadlock found when trying to get lock; try restarting transaction More
当用户同时绑定多个创意包 且 有普通创意时(即用户先绑定了一个普通创意后,用户在同时绑定两个创意包),此时会出现死锁的问题。
定位问题
找日志并分析
从应用日志来看,其实你并不知道你和谁冲突,因为死锁触发后会有一方是成功的。
此时只能求助DBA去排查。
其实很庆幸的是db里是有冗余trace的,所以发生死锁后我日志中会trace。
通过trace在加上日志系统是可以判断出并发的源头是下面红线的地方。
理论去证明问题
不过在日志中我发现了一个关键词lock_mode X locks gap,这不是八股文中的间隙锁和临间锁吗???
这种不应该是在可重复度隔离级别下解决幻读才会出现吗?我还用GPT问了一下
其实就在这,理论与实际发生冲突了,无解了。
我请教了一下DBA的同学,DBA同学给了个文档说案例3就是你的情况。
案例三用我们的问题去推演
T1 | T2 |
---|---|
begin | begin |
– | insert into order_tb ( id , biz_number , adgroup_id , creative_id , campaign_id , cust_id , member_id , product_id , ref_type , flow_ratio , online_status , audit_status , audit_time , audit_reason , op_id , task_id , action_id , start_time , end_time , begin_time , stop_time , gmt_create , gmt_modified ) values ( 14352655886, 0, 69735327295, 562734517, 69741860133, null, 40730745, 136001001, 0, null, 1, -1, ‘2024-02-29 14:14:05’, null, ‘0b64cf9617091872303006231d2e8c;9.1.1.7.1.6.6’, 0, 0, null, null, null, null, NOW(), NOW()) 此时获得唯一索引的行锁 |
insert into order_tb ( id , biz_number , adgroup_id , creative_id , campaign_id , cust_id , member_id , product_id , ref_type , flow_ratio , online_status , audit_status , audit_time , audit_reason , op_id , task_id , action_id , start_time , end_time , begin_time , stop_time , gmt_create , gmt_modified ) values ( 14352655886, 0, 69735327295, 562734517, 69741860133, null, 40730745, 136001001, 0, null, 1, -1, ‘2024-02-29 14:14:05’, null, ‘0b64cf9617091872303006231d2e8c;9.1.1.7.1.6.6’, 0, 0, null, null, null, null, NOW(), NOW()) 此时因为唯一索引发生冲突且没有拿到行锁,触发间隙锁,锁范围,此时等待T2 | – |
– | insert into order_tb ( id , biz_number , adgroup_id , creative_id , campaign_id , cust_id , member_id , product_id , ref_type , flow_ratio , online_status , audit_status , audit_time , audit_reason , op_id , task_id , action_id , start_time , end_time , begin_time , stop_time , gmt_create , gmt_modified ) values ( 14425938786, 0, 69735327295, 562714621, 69741860133, null, 40730745, 136001001, 0, null, 1, -1, ‘2024-02-29 14:14:05’, null, ‘0b64cf9617091872303066232d2e8c;9.1.1.6.1.6.6’, 0, 0, null, null, null, null, NOW(), NOW()) 此时因为插入的位置在间隙锁的范围内,所以此时等待T1 |
执行成功 | 触发死锁error释放资源 |
为什么会出现唯一冲突
业务侧的逻辑为:当收到创意包绑定单元的时间后,有一个全删全增的动作。如果用户同时绑定两个创意包,此时业务侧会收到两个请求,此时都执行全删全增的时候就会有插入的交集,从而触发唯一冲突。又因为主键序列通过区间段实现(不严格有序),所以后面会命中在间隙锁的范围从而出现死锁。
解决方案
- 逻辑修改
全删全增改为diff操作。这样做有两个好处:1)之前调用两个接口,现在调用1个 2)减少无效的binlog变更,甚至能规避环。 - 解决并发
当用户同时绑定多个创意包时,发送的binlog转mq的时候进行顺序消费,根据memberId路由到一个线程里。 - 监控报警
早发现早治疗。
参考
MySQL 并发insert 唯一键冲突导致的死锁