如何实现查询分离
项目组选择的是第2种触发方案:修改业务代码异步更新查询数据。考虑使用MQ(Message Queue,消息队列)作为实现方案:
每次程序处理写操作请求时,都会发一个通知给MQ,MQ收到通知后唤醒一个线程来更新查询数据。
使用MQ时,需要考虑以下几个问题:
问题1:MQ如何选型?
MQ的选型建议如下:
召集技术中心所有能做技术决策的人共同投票选型。
在并发量不高的情况下,不管选择哪个MQ,最终都能实现想要的功能,只不过存在是否易用、业务代码多少的区别,因此从易用性考量即可。当然,前提是支持自己使用的编程语言。
问题2:MQ宕机了怎么办?
考虑以下场景:
工单A更新后要通知MQ,但是MQ宕机了,于是MQ没有这条消息,出现消息丢失的情况。
MQ收到消息,然后消费者读到消息,但是MQ宕机了,于是MQ不知道消费者是不是已经消费成功,可能造成消息重复投递。
如果MQ宕机了,项目组只需要保证主流程正常进行,且MQ恢复后数据正常处理即可,具体方案分为2步:
每次进行写操作时,在主数据中加标识NeedUpdateQueryData=true,这样发到MQ的消息只是一个简单的信号告知数据需要进行更新,并不包含更新的数据ID。
MQ的消费者获取信号后,先批量查询待更新的主数据,然后批量更新查询数据,更新完成后查询数据的主数据标识NeedUpdateQueryData更新为false。
结合以上处理过程,再分析一下前面的两个MQ宕机场景:
工单A更新后要通知MQ,但是MQ宕机了,于是MQ没有这条消息,出现消息丢失的情况。
等MQ恢复后,假设工单B也更新了,此时触发了一个消费者线程,这个线程会查询NeedUpdateQueryData=true 的数据,结果工单A和B都被查询到了。这两个工单都将被同步到查询数据库。
MQ收到消息,然后消费者读到消息,但是MQ宕机了,于是MQ不知道消费者是不是已经消费成功,可能造成消息重复投递。
假设工单A更新后,MQ收到一条消息,然后消费者消费了这条消息,同步了工单A,但是在回调给MQ告知消费成功时,MQ宕机了,于是MQ不知道这条消息已经被消费,它恢复后又投递了同步工单的消息。此时消费者收到消息后,去查询数据库,但是其实工单A已经同步,NeedUpdateQueryData标识改成了false,待更新工单不再包含工单A,所以消息重复投递问题也解决了。
问题3:更新查询数据的线程失败了怎么办?
如果更新的线程失败了,NeedUpdateQueryData标识就不会更新,后面的消费者会再次将有NeedUpdateQueryData标识的数据拿出来处理。
但如果一直失败,可以在主数据中添加一个尝试迁移次数,每次尝试迁移时将其加1,成功后就清零,以此监控那些尝试迁移次数过多的数据。
问题4:消息的幂等消费。
消费者同步数据的流程如下:
消费者收到通知,更新操作触发
消费者获取NeedUpdateQueryData为true的工单
消费者将前面获取的工单同步到查询数据库
消费者将主数据库中相应工单的字段NeedUpdateQueryData改为false
在上述过程中,如果第3步和第4步间出现问题,可能导致数据已经进入查询库,但是NeedUpdateQueryData状态却并未得到更改,下次更新操作触发后仍然会去获取这些已经同步了的数据记录,从而出现数据重复。因此,需要保持消息的幂等性。
问题5:消息的时序性问题。
比如某个订单A更新了一次数据变成A1,线程甲将A1的数据迁移到查询数据中。不一会儿,后台订单A又更新了一次数据变成A2,线程乙也启动工作,将A2的数据迁移到查询数据中。
这里的时序性问题是,如果线程甲启动比乙早,但迁移数据的动作比线程乙还要慢,就有可能导致查询数据最终变成过期的A1。
此时解决方案为:
为记录增加一个名为last_update_time的字段,代表记录的最后更新时间
消费者在更新完查询库后,先不要将NeedUpdateQueryData变更为false
而是再从主库中将刚更新完的记录取出来,比较新旧数据的last_update_time字段,如果last_update_time未发生变化,但NeedUpdateQueryData却已经变为了false。
last_update_time未发生变化,说明当前操作的是最新的记录
NeedUpdateQueryData变为了false,必定是其他线程操作的结果,而该线程一定是旧纪录
此种情况下存在发生时序冲突的可能
上述情况满足,就将NeedUpdateQueryData改为true,再做一次迁移
查询数据库是否存在条件更新逻辑,即完成类似如下SQL的处理逻辑:
MQ的作用
服务的解耦:这样主业务逻辑就不会依赖更新查询数据这个服务了。
控制更新查询数据服务的并发量:如果直接调用更新查询数据服务,因写操作速度快,更新查询数据速度慢,写操作一旦并发量高,就会造成更新查询数据服务的超载。如果通过消息触发更新查询数据服务,就可以通过控制消息消费者的线程数来控制负载。