# 记一次数据库主从同步引起错误问题的排查与修复
背景:最近接手了会员模块,这模块的设计有点奇怪,把一个会员的功能拆成了两个服务实现,一个服务负责存数据(下文称为A服务),另一个服务负责写具体的用户关系、升降级业务逻辑(下文称为B服务)。
周一,租户反馈有个用户的等级数据异常,这类异常已经出现了很久了,一直都是反馈数据问题,然后找开发人员处理数据。根据等级变更记录显示,从粉丝升级成会员后,几十秒内等级又降回了粉丝。升级的条件是这个用户购买了某个特定的商品(下文称为C商品)。
排查过程:
排查过程中通过看代码发现,A服务冗余了用户是否购买C商品的标记于数据库,并且暴露了一个有缓存的接口给B服务查询。
猜想:这个缓存的实现是“先更新数据库,后删缓存”,这个并不是强一致性的实现方案,在读写并发请求时,确实可能有数据一致性问题。首先,这个场景下,数据是要求强一致性的。其次,这个数据的查询的频率比较低,只有会员升降级才会用到。最后,并且表里有唯一索引,查询效率也是比较高的。所以,这个缓存作用不大,直接删了它,并且在读、写两个方法中加入必要的日志,再观察观察。
下周一,租户反馈,又出现等级先升后降问题了。观察日志,是先写购买标记再读数据库,间隔7s多,写了之后查不出更新后的数据。经过查看配置,原来A服务开启了polar db的数据库代理读写分离,一致性级别是“会话一致性”,这个一致性级别是AP的方案,不能保证数据的强一致性。
所以这是读写分离的数据一致性问题。每次发生的时间都在周一早上,应该是因为周一早上这个租户搞活动,TPS比较大导致主从复制延迟比较大。
备选的解决方案:
1.提供强一致性的查询接口和非一致性的查询接口。项目中配两个数据源,一个是主节点、一个是配了会话一致性的代理节点。一致性级别通过选择数据源保证。这个方案通用性比较强,其他的数据库厂商也能用,但会稍微提高一点主节点的压力。但需要改造的点比较多,如果其他服务有类似的问题,也要进行类似的改造。
2.提供强一致性的查询接口和非一致性的查询接口。需要强一致性时,加上sql注释作为hint,例:
/*FORCE_MASTER*/ select * from user;
这个是官方提供的方案,只有polar db才能用。也是会稍微提高一点主节点的压力。
3.更新时,先写数据库,后更新缓存。这个方案的缓存过期时间不容易设置,如果设置了过期时间为1分钟,但数据库主从同步需要2分钟,还是可能会有问题。
最终了方案2,估计其他的模块也会遇到类似的问题,所以写了个Mybatis的Interceptor,当ThreadLocal中有“强行读主节点”的标识时,就把*/*FORCE_MASTER*/* 这段注释拼到sql里再进行查询。代码就先不放出来了,以后有时间出个MyBatis的学习分享。
参考资料:
polardb会话一致性说明
https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/consistency-levels#section-lnv-grf-2gb
Polardb数据库代理常见问题
https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/faq-about-polarproxy