出现这种问题一般是由于有两个以上相同主键的sql,同时插入数据,第一个插入成功后,第二个插入的时候会报主键冲突。表的主键是唯一的,不允许重复。
我仔细检查了代码,发现代码逻辑会先根据主键从表中查询订单是否存在,如果存在则更新状态,不存在才插入数据,没得问题。
这种判断在并发量不大时,是有用的。但是如果在高并发的场景下,两个请求同一时刻都查到订单不存在,一个请求先插入数据,另一个请求再插入数据时就会出现主键冲突的异常。
解决这个问题最常规的做法是:加锁。
我刚开始也是这样想的,加数据库悲观锁肯定是不行的,太影响性能。加数据库乐观锁,基于版本号判断,一般用于更新操作,像这种插入操作基本上不会用。
剩下的只能用分布式锁了,我们系统在用redis,可以加基于redis的分布式锁,锁定订单号。
但后面仔细思考了一下:
加分布式锁也可能会影响消费者的消息处理速度。
消费者依赖于redis,如果redis出现网络超时,我们的服务就悲剧了。
所以,我也不打算用分布式锁。
而是选择使用mysql的INSERT INTO ...ON DUPLICATE KEY UPDATE语法:
INSERTINTOtable (column_list) VALUES (value_list) ONDUPLICATEKEYUPDATE c1 = v1, c2 = v2, ...;它会先尝试把数据插入表,如果主键冲突的话那么更新字段。
把以前的insert语句改造之后,就没再出现过主键冲突问题。
数据库主从延迟不久之后的某天,又收到商户投诉说下单后,在划菜客户端上看得到订单,但是看到的菜品不全,有时甚至订单和菜品数据都看不到。
这个问题跟以往的都不一样,根据以往的经验先看kafka的topic中消息有没有积压,但这次并没有积压。
再查了服务日志,发现订单系统接口返回的数据有些为空,有些只返回了订单数据,没返回菜品数据。
这就非常奇怪了,我直接过去找订单组的同事。他们仔细排查服务,没有发现问题。这时我们不约而同的想到,会不会是数据库出问题了,一起去找DBA。果然,DBA发现数据库的主库同步数据到从库,由于网络原因偶尔有延迟,有时延迟有3秒。
如果我们的业务流程从发消息到消费消息耗时小于3秒,调用订单详情查询接口时,可能会查不到数据,或者查到的不是最新的数据。
这个问题非常严重,会导致直接我们的数据错误。
为了解决这个问题,我们也加了重试机制。调用接口查询数据时,如果返回数据为空,或者只返回了订单没有菜品,则加入重试表。
调整后,商户投诉的问题被解决了。
重复消费kafka消费消息时支持三种模式:
at most once模式 最多一次。保证每一条消息commit成功之后,再进行消费处理。消息可能会丢失,但不会重复。
at least once模式 至少一次。保证每一条消息处理成功之后,再进行commit。消息不会丢失,但可能会重复。
exactly once模式 精确传递一次。将offset作为唯一id与消息同时处理,并且保证处理的原子性。消息只会处理一次,不丢失也不会重复。但这种方式很难做到。
kafka默认的模式是at least once,但这种模式可能会产生重复消费的问题,所以我们的业务逻辑必须做幂等设计。
而我们的业务场景保存数据时使用了INSERT INTO ...ON DUPLICATE KEY UPDATE语法,不存在时插入,存在时更新,是天然支持幂等性的。
多环境消费问题我们当时线上环境分为:pre(预发布环境) 和 prod(生产环境),两个环境共用同一个数据库,并且共用同一个kafka集群。
需要注意的是,在配置kafka的topic的时候,要加前缀用于区分不同环境。pre环境的以pre_开头,比如:pre_order,生产环境以prod_开头,比如:prod_order,防止消息在不同环境中串了。
但有次运维在pre环境切换节点,配置topic的时候,配错了,配成了prod的topic。刚好那天,我们有新功能上pre环境。结果悲剧了,prod的有些消息被pre环境的consumer消费了,而由于消息体做了调整,导致pre环境的consumer处理消息一直失败。
其结果是生产环境丢了部分消息。不过还好,最后生产环境消费者通过重置offset,重新读取了那一部分消息解决了问题,没有造成太大损失。
后记除了上述问题之外,我还遇到过:
kafka的consumer使用自动确认机制,导致cpu使用率100%。
kafka集群中的一个broker节点挂了,重启后又一直挂。
最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
BAT大佬写的刷题笔记,让我offer拿到手软
这两个问题说起来有些复杂,我就不一一列举了,有兴趣的朋友可以关注我的公众号,加我的微信找我私聊。