mysql + spring transaction

背景是调用一个外部接口要做集群限流、想到不依赖过多中间件的方法就是用db的唯一性、比如集群限制并发限制每秒最多调用10次

遇到的问题:
1. 调用限制是基于秒为单位、create_time的定义是timestamp、mysql保存timestamp是毫秒时间还是秒级时间、用一个YY-MM-DD hh:mm:ss的时间格式是否能筛选出条件、当时问题比较多、这块儿为了保险起见又弄了个varchar的字段保存创建时间的秒级表示、后来用create_time用字符串匹配也可以匹配出来、mysql时间存储的细节后面可以多挖下, 后面就用create time就是好用的
2. mybatis的cache问题、当时多个相同的transaction运行、debug发现第一个transaction(t1)运行完了之后、后续其他相同的transaction(t2、t3)拿不到t1新写入的记录、但是debug截断时用相同的sql在mysql terminal执行可以拿到t1新写入的记录、第一个反应就是后续的transaction没有真正的执行db而是用了mybatis的cache、用了flushCache = true 和useCache = false后、debug执行进入mybatis里发现、确实执行的是查询db、拿到数值依然是过期的,关于mybatis的cache设计、面试的时候也经常和别人聊、后面可以细化一下后面、比如为啥这样设计、缘由等等
3. mysql的mvcc、这个东西我一直就知道、在实际用的时候似乎没什么概念、我是用spring的@Transactional实现的事务、t2debug截断时其实已经进入了事务、此时它获取得到是t1未能生效的镜像读(snapshot read、mysql默认是RR)、而在mysql terminal里读的时候t1已经执行完了、获取当前最新的snapshot read就是t1执行的结果。因为spring@Transactional的实现是基于spring aop后面实现的Bean post processor、这里因为没有实现接口所以走的cglib的实现、它构造了一个子类继承目标类、在方法执行之前处理Transaction相关的东西(比如开启事物、设置autocommit等等)、当时换了一个思路上来就插入记录、假如成功了就是这一秒钟的第一个记录成功放回、假如失败了用悲观锁hold住这条记录做上界判断、计数+1、伪代码如下、为了防止各个服务器的时间误差、统一用db的时间来衡量


mysql + spring transaction_第1张图片
image.png

最开始用唯一索引做悲观锁的条件、但是同时并发请求数目大于20后基本上一定会出死锁、当时想当然的认为是gap的问题、unquie index一把锁、带上cluster index一把锁、几十个Transaction同时运行、假如顺序不对会发生死锁的、但是为了尝试干脆用create time做primary key、查询悲观锁的条件也用primary key、每次只有一把record lock应该就不会出问题了、这样改造完运行、2-3个并发请求必出死锁、盲目的改隔离界别和传播级别都不管用、有时候没有理论指导乱尝试是非常低效的、死锁日志如下:


image.png

当时搞到夜里3点多、怎么想也不明白为啥会有S锁的问题、没有刻意加S锁、后来请教dba同学、后来发现这一段笔记一直安静的睡在我的OneNote里

mysql + spring transaction_第2张图片
image.png

innodb engine在唯一索引或者primary的情况下、并发插入时失败的Transaction会在对应记录上加上S锁、失败的Transaction上后续使用悲观锁、基本上一定会出deadlock、所以插入失败后应该结束当前的Transaction、用一个新的Transaction去争抢失败的记录、发生DuplicateKeyException时(继承自RuntimeException)spring-tx里在方法异常时会rollback事务、放弃那把S锁、伪代码如下:

while(true){
        try{
            tryToInsert
            break;
        }catch(DuplicateKeyException){
            if waitUpdate suc
                break;
        }
    }
  1. 上面tryToInsert和waitUpdate 都加了@Transactional, waitUpdate 方法也指定了开启一个新事物REQUEST_NEW, 但是发现waitUpdate 中后续每一个事务是并发执行的、比如t1插入成功、t2和t3同时都拿到了这条记录(count = 1)、然后并发更新出错、最后count = 2而不是3、后来加入了Transaction support打印当前事务状态、发现waitUpdate 虽然加了@Transactional注解但是根本不在事务里、想了一下BPP执行后代理对象代替原来的bean、经过代理对象的方法引用才能执行前置和后置处理、public方法间的对象调用不经过代理对象(因为代理类继承目标类、目标类执行方法中找到的是目标的方法而不是代理类继承的方法)、包了一层引用、测试166的并发限制17秒全部出去、waitUpdate 设置了一个比较大的timeout、这样并发量比较大的时候后续的Transaction因为悲观锁的等待出现了些show sql的警示、比如瞬间几千个qps打过来、这样报警而排查没有啥意义、后续改成乐观锁、既增加了并发度还去掉了无意义的slow sql

总结一下:
1. 每一个知识点都知道、也自认为了解、但是在实际的过程中却并不那么顺畅、天下之事,闻者不如见者知之为详,见者不如居者知之为尽
2. 再有能灵活运用的东西才是自己、而不是安静沉睡的笔记
3. 其实最简单的办法是每个机器加一个保守的sleep、比如2台机器sleep 200ms、这样不会出错、但是两个请求打到一个机器会效率慢、增加机器要调整sleep的时间、做调用频率的统计不那么方便;但是手上其实还要其他工作要做、客观上头天弄到3点多、第二天上班会影响工作效率、所以第二天强迫自己抽出来(这与人本身的求知欲和解决问题的欲望是相悖的)、应该是做个临时work的版本、不忙的时候搞定它、这个问题就是周末的凌晨搞定的、客服人本身的情绪缺陷合理高效的安排工作

你可能感兴趣的:(mysql + spring transaction)