背景
- 在将xxl-job-admin部署到正式环境后,发现存在重复调度的问题。系统部署在k8s中,共起了3个pods,后端存储为TIDB。
- 发现问题后,当即降pods的副本数降到1,可见重复调度问题消除。
- 打开xxl-job-admin的日志问题可以看到一直在刷Write conflict xxx等报错日志。
排查过程
开源项目遇到问题第一步,先上GitHub上搜issue。果不其然被我搜索到一模一样的问题。(ISSUE)。根据issues的描述,这个错误可以与TIDB有关。结合自己使用的存储底层,着手向这方面进行了排查。
接下来让我们再次回看下xxl-job-admin中竞争调度的代码,从下面的图片中可以看出流程主要分为这么几个部分
- (1)先通过jdbc获取一个数据库连接。
- (2)将事务自动commit关掉。
- (3)通过select for update的方式来锁住一行(排它锁)。
- (4)执行事务逻辑。
- (5)事务执行完毕手动提交事务。
通过查询TIDB的相关文档,发现在TIDB版本<= v3.0.8之前使用的乐观事务模型,也就是说不会进行锁等待,只会在事务提交冲突的时候进行报错(Write conflict)。再结合文档中描述,发现有两处可以需要确认的地方
- (1)使用的TIDB版本<= v3.0.8?不是,我使用的是4.0.6版本。当文档也提这么一句话:只有新创建的集群才会默认使用悲观事务模式,从旧版本升级上来的并不会修改事务类型
- (2)是否在事务中使用了自动提交?从上面的代码上看,在获取JDBC的时候有显式的关闭的事务提交。
于是问题就转行成确认我当前使用的TIDB是不是从低版本升级上来的,或者确认当前默认使用的是不是悲观事务模型。经过询问TIDB同事,告诉我当前就是默认悲观事务,但是我不信。
既然不信,那Talk is cheap. Show me the code,手动写两个main方法来测试下吧!就认准一条准则,已经是悲观事务模式的话,那么tidb执行for update的结果应该跟mysql的一样(锁等待)。假设表kantlin中有(2,1)这行数据,事务执行时序大概如下:
MySQL的结果是在Tx1和Tx2都可以成功提交, t7时刻, select执行结果为b=22,且但Tx2从t3时刻开始,会被阻塞,一直到t6时刻, Tx1完成提交后, Tx2才能提交,所有在mysql中执行select for update排它锁语句是会等待锁的,没问题。
但是TiDB中, Tx1提交会失败(WriteConflict),到了t7时刻, select执行结果为b=21,所以证明当前的还是处于乐观事务模式。
有了这些证据后,再次请TIDB同事确认,TIDB同事经过一番排除后发现确实是从旧版本升级上来的,应该没有改默认的事务类型,并帮我手动的指定事务级别为悲观事务模式。我接着再对系统增加pods副本,发现没有重复调度的问题,日志打印也正常了。
其它思考
使用乐观锁事务模型优缺点是什么?
TiDB 使用 Percolator 事务模型, 冲突检测只在事务提交时才触发, 而MySQL则是通过锁等待机制(如SELECT … FOR UPDATE)解决冲突问题.TiDB这样设计有好有坏, 在冲突小的情况下,由于没有锁等待,系统的并发性能更好. 但在冲突严重的情况下, 会造成事务失败增多,影响并发性能。
如何使用TIDB?
作为开发,当前正从mysql向tidb切换过度,总是很直接就认为当mysql来用就可以了,而没有过多的研究。从这次小小的掉坑,也学到不少tidb的知识。作为DBA的同事,他们确实是没有责任帮你升级事务模型,那么就全靠开发把握了,还好是很容易在测试期间就发现的问题,但是想象下如何是在生产发现,并且涉及到跟钱有关的批扣,那问题就大了。生产无小事!
其它的方式实现锁?
如果你手头上真的只有一个<= v3.0.8版本的TIDB,但是又想多实例部署xxl-job-admin的话,那么其实基于DB实现分布式锁主要有三种,xxl-jobs使用的是排它锁,另外的唯一索引锁或CAS乐观锁也都可以实现的,保险点再弄个监控线程清理长期占用的锁就可以了。
网友评论