大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
前言
最近公司发生了一起生产事故,在退款时产生了 重复退款
,事件报告中指出是因为针对重复MQ消息做幂等控制时,幂等控制方案失效,导致重复处理了两条退款消息,最终造成重复退款。
失效的幂等控制方案其实很简单,就是基于数据库的 唯一索引
来进行幂等控制,这其实是很常用也很简单的一种实现方案,但为什么在这起生产事故中,唯一索引实现幂等就失效了呢,问题就出在这个唯一索引上。
正文
一. 场景抽象
为了便于理解,这里先给出整个场景的抽象交互图。
上游重复投递消息后,重复投递的消息有两个下游消费,两个下游都基于唯一索引做了幂等控制,但是结果就是下游-1幂等控制失败,另一个下游-2幂等控制成功。
上游投递的消息有四个字段,记为field_1,field_2,field_3和field_4,其中下游-1的幂等控制表的创表语句如下。
CREATE TABLE idempotent_1 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
field_1 VARCHAR(255) NOT NULL,
field_2 VARCHAR(255) NOT NULL,
field_3 VARCHAR(255) DEFAULT NULL,
field_4 VARCHAR(255) NOT NULL,
UNIQUE INDEX idempotent_index(field_1, field_2, field_3)
)
下游-2的幂等控制表的创表语句如下。
CREATE TABLE idempotent_2 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
field_1 VARCHAR(255) NOT NULL,
field_2 VARCHAR(255) NOT NULL,
field_3 VARCHAR(255) DEFAULT NULL,
field_4 VARCHAR(255) NOT NULL,
UNIQUE INDEX idempotent_index(field_2)
)
到这里你能猜到为什么下游-1幂等控制会失效,而下游-2不会了吗。
二. 问题分析
不知道你猜到没有,原因就是下游-1的唯一索引会失效。
下游-1的唯一索引是一个复合索引,包含field_1,field_2和field_3,其中field_3允许为NULL,而在MySQL中,NULL表示 未知
,也就是前后两次插入数据时,如果field_3都是NULL,此时就算field_1和field_2完全一样,也是能够插入成功的,因此唯一索引的唯一约束就失效了,最终幂等控制也就失败了。
我们可以把idempotent_1表创建出来自行做一下测试,执行如下插入语句两次,是可以插入成功的。
INSERT INTO idempotent_1 (field_1, field_2, field_3, field_4) VALUES ('A', 'B', NULL, 'D');
三. 问题解决
问题的解决很简单,为field_3添加NOT NULL约束,就能够规避因为存在NULL而导致的唯一索引失效,进而幂等控制就能成功。
进一步的,其实建议所有字段都添加上NOT NULL约束,这样能够规避很多问题。
总结
情况是一个简单情况,就是对重复MQ消息基于唯一索引做幂等控制时,因为唯一索引是一个复合索引且存在字段允许为NULL,从而唯一索引失效,最终幂等控制失败。
解决方案也十分简单,就是为所有字段都添加上NOT NULL约束,从而唯一索引就不会因为存在NULL而失效。
那么最后思考一个问题,为什么MySQL允许字段可以为NULL呢,毕竟很多问题都是因为NULL的存在而出现的。
个人认为最本质的原因就是NULL可以节约空间,比如一个字段是VARCHAR,当该字段允许为NULL且实际就是NULL时,是不会占用空间的,但如果该字段不允许为NULL,那么至少都会存储一个空字符串,而空字符串的占用空间包含两部分,即 长度信息
和 实际内容
,因为是空字符串,所以实际内容是0字节,但长度信息至少都会占用1字节,所以 节约空间
是NULL存在的意义。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈