Categories
程式開發

MySQL死鎖與Spring事務



{“type”:”doc”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”MySQL死锁从产品之初就偶有发生,算是萦绕在心中的噩梦之一。由于死锁大都伴随着锁等待,所以一般都会拉低服务QPS,在死锁发生时肯定会出现各种意料不到的问题。前期一直采用“指标不治本”的办法,对特别容易产生死锁的方法增加retry。但当retry和事务嵌套在一起时也会出现不可预知的错误。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”对于数据库死锁这个万恶之源,真可谓深恶痛绝,所以这次在解决retry和事务嵌套问题时,将这个元凶也一并解决。”}]},{“type”:”heading”,”attrs”:{“align”:null,”level”:2},”content”:[{“type”:”text”,”text”:”一些关于事务的概念”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”为了更好的说明问题,我们先来解释一下基本概念”}]},{“type”:”heading”,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”隔离级别”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”TransactionDefinition 接口中定义了五个表示隔离级别的常量:”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”bulletedlist” ,”content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.ISOLATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”DEFAULT:”},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:” 使用後端數據庫默認的隔離級別,Mysql 默認採用的REPEATABLE”},{“type”:”text”,”text”:”READ隔離級別Oracle 默認採用的READ_COMMITTED隔離級別.”} ]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.ISOLATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”READ”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”UNCOMMITTED:”},{“type”:”text”,”text”:” 最低的隔離級別,允許讀取尚未提交的數據變更,”},{“type”:”text” ,”marks”:[{“type”:”strong”}],”text”:”可能會導致臟讀、幻讀或不可重複讀”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.ISOLATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”READ”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”COMMITTED:”},{“type”:”text”,”text”:” 允許讀取並發事務已經提交的數據,”},{“type”:”text”,”marks” :[{“type”:”strong”}],”text”:”可以阻止臟讀,但是幻讀或不可重複讀仍有可能發生”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.ISOLATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”REPEATABLE”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”READ:”},{“type”:”text”,”text”:” 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,” },{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”可以阻止臟讀和不可重複讀,但幻讀仍有可能發生。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.ISOLATION_SERIALIZABLE:”},{“type”:”text”,”text”:” 最高的隔離級別,完全服從ACID的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”該級別可以防止臟讀、不可重複讀以及幻讀”},{“type”:”text”,”text”:”。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。”}]}]}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”隔离级别是数据库和应用层(这里就是指Spring)都有的概念,一般来说应用层不应该修改隔离级别,都应默认使用数据库的隔离级别,也就是使用TransactionDefinition.ISOLATION_DEFAULT”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”事务传播行为”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”当一个事务方法被另一个事务方法调用时,对于已存在的事务如何处理?通过指定事务传播机制来解决。在TransactionDefinition定义中包括了如下几个表示传播行为:”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”strong”}],”text”:”支持當前事務的情況:”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”bulletedlist” ,”content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION_REQUIRED:”},{“type”:”text”,”text”:” 如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。” }]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION_SUPPORTS:”},{“type”:”text”,”text”:” 如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION_MANDATORY:”},{“type”:”text”,”text”:” 如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。”}]}]}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”strong”}],”text”:”不支持當前事務的情況:”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”bulletedlist” ,”content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”REQUIRES”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”NEW:”},{“type”:”text”,”text”:” 創建一個新的事務,如果當前存在事務,則把當前事務掛起。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”NOT”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”SUPPORTED:”},{“type”:”text”,”text”:” 以非事務方式運行,如果當前存在事務,則把當前事務掛起。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION_NEVER:”},{“type”:”text”,”text”:” 以非事務方式運行,如果當前存在事務,則拋出異常。”}]}]}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”text””marks”:[{“type”:”strong”}],”text”:”其他情況:”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”bulletedlist” ,”content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”TransactionDefinition.PROPAGATION”},{“type”:”text”,”marks”:[{“type”:”italic”},{“type”:”strong”}],”text”:”NESTED:”},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:” 如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION”},{“type”:”text “,”text”:”REQUIRED。”}]}]}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”一般常用的就是PROPAGATION_REQUIRED。特别需要注意的是事务传播行为并不是数据库的定义,而是应用层(这里就是指Spring)引入的概念。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”事务状态是如何传递的”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”假设默认使用PROPAGATION_REQUIRED,当一个事务方法A调用事务方法B(即都用@Transactional声明的方法),事务方法B为何知道此时已经存在事务了?”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”其实spring的事务管理器”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”TransactionManager”}]},{“type”:”text”,”text”:”內部會持有當前線程的事務狀態”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”transcationInfo”}]},{“type”:”text”,”text”:”來判斷一個事務是否已經開啟。每個線程都要有一個狀態,聰明的你一定會想到使用ThreadLocal這個框架必備的工具。”} ]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/0f/0f0ae9b5c5d4c6d48a8bd1fd13e6cb9e.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”那么对于嵌套事务,Spring是怎么实现子事务失败,父事务也失败呢?先来看commit方法,”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/5f/5f6809897c316b8061539609f6346f61.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”看标注①的代码,如果TransactionStatus里的rollbackOnly是true,那么就会processRollback。继续跟进标注②的代码,看什么时候会设置这个标志位。在processCommit这个方法内,一旦捕获RuntimeException都会进入doRollbackOnCommitException方法,继续跟进这个方法”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/f3/f36a7da46b60e894fb47d58466ca5dbb.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”可以看到一个条件分支,如果当前是新开启的事务,直接进行rollback。如果当前已有父事务,那么不会直接发送rollback到数据库,而是执行标注①的”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”doSetRollbackOnly”}]},{“type”:”text”,”text”:”方法,設置”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”TransactionStatus”}]},{“type”:”text”,”text”:”裡的rollbackOnly=true。這樣父事務在commit的時候就會rollback。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”由于篇幅原因,源码部分就不过多展示了, 如果感兴趣可以自行debug一下就能了解其中的机制。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:2},”content”:[{“type”:”text”,”text”:”当 Spring Retry 与事务相遇”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”上文谈到我们在处理死锁时采用重试的方式解决,但并不是说任何方法都套上”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”@Retryable”}]},{“type”:”text”,”text”:”就萬事大吉了。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”Retry的注意点”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”一般来说,在spring中使用retry非常的简单,在注解中定义好重试次数和避让策略就可以了。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”codeblock” ,”attrs”:{“lang”:”java”},”content”:[{“type”:”text”,”text”:”@[email protected](maxAttempts = MAX_RETIES, value = {MySQLIntegrityConstraintViolationException.class, MySQLTransactionRollbackException.class}, backoff = @Backoff(delay = 100, multiplier = 2))npublic Foo save(@Nullable Foo foo) {n // …n // foo.setXXX(…)n}”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”值得注意是,如果方法内部会对参数对象进行修改,那么实际上每次retry的逻辑就都不是幂等的了,因为这一次的retry会受上一次的影响。假设这个”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”save”}]},{“type”:”text”,”text”:”方法會根據有沒有id來判斷處理邏輯是創建還是更新,那麼第二次的retry很有可能就會變成更新邏輯,而不是預期的創建邏輯了。如果參數是不可變的(immutable)那麼可以很大程序上降低這種複雜度。這也告訴我們,immutable思想非常重要,可以在設計和編譯階段就解決很多“頭疼”的問題。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”回到正题,试想一下,如果这个”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”save”}]},{“type”:”text”,”text”:”是個子事務方法會發生什麼?按照上一節說的,第一次執行就會設置rollbackOnly=true,那麼即使第二次重試成功也並不會將rollbackOnly設置為false,所以父事務依然會失敗。換句話說,”},{“type”:”text”,”marks”:[{“type”:”strong”}],”text”:”retry在子事務方法上是沒​​有意義的”},{“type”:”text”,”text”:”。這點在編碼時尤其需要注意。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:2},”content”:[{“type”:”text”,”text”:”数据库死锁”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”事务的问题说完,回到万恶之源的数据库死锁问题。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”起因”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”其实死锁的困扰已久,一般一周次数在几次到十几次不等。而且死锁的对象都是索引或者主键,查阅了一些资料觉得可能是并发写入比较严重造成,但线下模拟了很久也没有复现。次数倒也不多,也就先用重试策略抵挡一下。当然也就遇到了上文提及的问题。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”近期由于业务增长,死锁发生的频繁愈发频繁,所以“治本”的任务也紧急地提上了日程。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”问题分析”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”死锁第一步分析肯定先从MySQL的死锁log入手”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”codeblock” ,”attrs”:{“lang”:””},”content”:[{“type”:”text”,”text”:”————————nLATEST DETECTED DEADLOCKn————————n2020-03-27 10:35:51 0x7f5124f91700n (1) TRANSACTION:nTRANSACTION 860653361, ACTIVE 0 sec insertingnmysql tables in use 1, locked 1nLOCK WAIT 15 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 2nMySQL thread id 11732538, OS thread handle 139984633485056, query id 5318215849 10.0.20.22 linkflow updatenINSERT INTO contactidentity (contactid, externalid, lastupdated, tenantid) VALUES (34868134, ‘oPawF5kfWh7BjsgMesjfsndYc548’, ‘2020-03-27 10:35:51.579’, 1)n (1) WAITING FOR THIS LOCK TO BE GRANTED:nRECORD LOCKS space id 433 page no 911218 n bits 600 index idxtcontact of table linkflow.contactidentity trx id 860653361 lockmode X locks gap before rec insert intention waitingnRecord lock, heap no 412 PHYSICAL RECORD: nfields 3; compact format; info bits 0n 0: len 8; hex 800000000000016e; asc n;;n 1: len 8; hex 800000000107ef32; asc 2;;n 2: len 8; hex 80000000020c7f31; asc 1;;nn (2) TRANSACTION:nTRANSACTION 860653362, ACTIVE 0 sec insertingnmysql tables in use 1, locked 1n15 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 2nMySQL thread id 11736226, OS thread handle 139986489382656, query id 5318215869 10.0.20.22 linkflow updatenINSERT INTO contactidentity (contactid, externalid, lastupdated, tenantid) VALUES (34868135, ‘oPawF5t1K1a1i96ZDrJQRx22T8w’,’2020-03-27 10:35:51.588′, 1)n (2) HOLDS THE LOCK(S):nRECORD LOCKS space id 433 page no 911218 n bits 600 index idxtcontact of table linkflow.contactidentity trx id 860653362 lock mode S locks gap before recnRecord lock, heap no 412 PHYSICAL RECORD: nfields 3; compact format; info bits 0n 0: len 8; hex 800000000000016e; asc n;;n 1: len 8; hex 800000000107ef32; asc 2;;n 2: len 8; hex 80000000020c7f31; asc 1;;nn (2) WAITING FOR THIS LOCK TO BE GRANTED:nRECORD LOCKS space id 433 page no 911218 n bits 600 index idxtcontact of table linkflow.contactidentity trx id 860653362 lockmode X locks gap before rec insert intention waitingnRecord lock, heap no 412 PHYSICAL RECORD: n_fields 3; compact format; info bits 0n 0: len 8; hex 800000000000016e; asc n;;n 1: len 8; hex 800000000107ef32; asc 2;;n 2: len 8; hex 80000000020c7f31; asc 1;;nn WE ROLL BACK TRANSACTION (2)n”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””text”:”這是兩個insert操作,事務2失敗,是由於等待索引idx”}{“type”:”text””marks”:[{“type”:”text””text”:”这是两个insert操作,事务2失败,是由于等待索引idx”}{“type”:”text””marks”:[{“type”:”text””text”:”這是兩個insert操作,事務2失敗,是由於等待索引idx”}{“type”:”text””marks”:[{“type”:”text””text”:”这是两个insert操作,事务2失败,是由于等待索引idx”}{“type”:”text””marks”:[{“type”:”italic”}],”text”:”t”},{“type”:”text”,”text”:”contact的排他鎖。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”我们分析一下”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”codeblock” ,”attrs”:{“lang”:””},”content”:[{“type”:”text”,”text”:”死锁的索引约束n KEY idxtcontact (tenantid,contactid),n产生死锁的原因是:nTRANSACTION 860653361,插入的记录是:INSERT INTO contactidentity (contactid, externalid, lastupdated, tenantid) VALUES (34868134, ‘oPawF5kfWh7BjsgMesjfsndYc548’, ‘2020-03-27 10:35:51.579’, 1)nn他被阻塞,需要等待这个记录的锁提供:n 0: len 8; hex 800000000000016e; asc n;;n 1: len 8; hex 800000000107ef32; asc 2;;n 2: len 8; hex 80000000020c7f31; asc 1;;nn而这个记录的锁,被事务TRANSACTION 860653362持有了,没释放,没释放的情况下,接着在事务860653361后,又执行了一个插入(在860653362的事务中):nINSERT INTO contactidentity (contactid, externalid, lastupdated, tenantid) VALUES (34868135, ‘oPawF5t1K1_a1i96ZDrJQRx22T8w’,’2020-03-27 10:35:51.588′, 1)nn它也需要这个记录的锁(其实它已经持有了这个记录的锁,但是他需要在队列里等待):n 0: len 8; hex 800000000000016e; asc n;;n 1: len 8; hex 800000000107ef32; asc 2;;n 2: len 8; hex 80000000020c7f31; asc 1;;n n这样就造成了死循环,产生死锁”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”这个就挺奇怪的,两个insert没有涉及到同一个主键或者同一个唯一索引,居然会持有S锁,并造成相同等待。咨询了阿里云的售后DBA,表示很有可能是在插入前有一个产生S锁的操作导致了这个问题,比如锁定行记录,或者是更新操作。但在我们的业务中,在写入之前只有查询操作,查询记录为空后执行插入,按理说是不会产生S锁的。之后我们又逐个的排查了应用日志中输出的SQL,也没有发现会加S锁的操作。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”这一下线索又断了,在与阿里云的DBA沟通了一天后也没什么进展,期间各种查binlog也没有任何发现。好在最后DBA建议我们打开RDS的SQL洞察功能,看看会不会有什么蛛丝马迹。虽然这个功能是收费的,不过也正是由于它我们才找到了真正的元凶。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”转机”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”SQL洞察购买以后才会开始收集,所以这时我们只需要静静地等待死锁的再次发生。由于是回顾,我们还是以刚刚死锁日志中的两个事务来看下SQL洞察中的记录。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”事务1″}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/d8/d86863b695566d08f71c0de3120659f2.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”事务2″}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/27/272d682b87896976e7642815bb8c67a5.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”可以看到在两次查询时都执行了”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;”}]},{“type”:”text”,”text”:”,這個就非常奇怪了,而且正是由於這個操作導致了死鎖,因為設置隔離級別為串行化以後,所以查詢都會加S鎖。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”我们来模拟下死锁过程”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/ef/ef2c73228464115d2b7c676d80bf7003.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{” type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””text”:”事務1和事務2都開啟了串行化,那麼在查詢時會將滿足條件的索引都加上S鎖,這裡鎖的就是tenant”}{“type”:”text””marks”:[{“type”:”text””text”:”事务1和事务2都开启了串行化,那么在查询时会将满足条件的索引都加上S锁,这里锁的就是tenant”}{“type”:”text””marks”:[{“type”:”text””text”:”事務1和事務2都開啟了串行化,那麼在查詢時會將滿足條件的索引都加上S鎖,這裡鎖的就是tenant”}{“type”:”text””marks”:[{“type”:”text””text”:”事务1和事务2都开启了串行化,那么在查询时会将满足条件的索引都加上S锁,这里锁的就是tenant”}{“type”:”text””marks”:[{“type”:”italic”}],”text”:”id=1的記錄,這個時候其實在事務2的第三步插入時就會產生鎖等待,因為它在等待事務1釋放S鎖。這時事務1也提交一個插入操作,並且contact”},{“type”:”text”,”text”:”id和ten​​ant”},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:”id都一樣(命中idx”},{“type”:”text”,”text”:”t_contact索引),就會產生死鎖,事務1失敗回滾, 事務2成功。” }]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/94/9431d950cb85cb98c750ce2c94f9d75e.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”heading”,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”分析”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”既然知道了造成了死锁的原因,但是究竟是什么代码会设置隔离级别为串行化呢?”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”我们排查了所有代码依然没有任何发现,直觉告诉我,应该是某些三方库的设置导致的。目前应用中涉及到数据库的三方库也并不是很多,决定逐个摸排。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”这里还得提一个阿里出品的线上调试神器”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”arthas”}]},{“type”:”text”,”text”:”,拿來直接watch mysql驅動內”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”connectionImpl”}]},{“type”:”text”,”text”:”的”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”setTransactionIsolation”}]},{“type”:”text”,”text”:”方法,只要有觸發了改變隔離級別的操作,那麼立刻就可以看到輸出。當然這一切都是在staging環境進行的。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”一起准备就绪后,我就开始通过UI操作可疑的功能。果然在进行数据导出的时候,发现”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”arthas”}]},{“type”:”text”,”text”:”的控制台有輸出了,從圖上可以看到隔離級別在 2 (READ_COMMITTED) 和 8 (SERIALIZABLE) 之間來回切換。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/75/7560f2a7e7492e1b6ffbbd28221d9f08.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”其实这样的切换和SQL洞察的输出也对的上,在事务结束后会将隔离级别恢复到数据库默认级别。阅读源码得知,设置回READ_COMMITTED是HikariCP的逻辑,在connection还回pool的时候会reset connection到默认级别。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”解决”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”原因弄清楚了,接下来着手解决。导出业务使用的是spring batch做异步任务。分析其源码,发现默认隔离级别是”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”ISOLATION_SERIALIZABLE”}]},{“type”:”text”,”text”:”。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/6b/6bed30cbe7f3a37341302194cdc1bf88.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”所以确实是该三方库导致的。解决很简单,既然都已经有public的setter了,那么初始化时直接设置其隔离级别,”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”setIsolationLevelForCreate(“ISOLATION_DEFAULT”)”}]},{“type”:”text”,”text”:”,避免使用默認值。修改以後再進行測試,確實沒有修改隔離級別的情況出現了。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”寻根溯源”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””text”:”可還是有疑問,我們的查詢業務跟springbatch並沒有關係。另一方面,即使在使用springbatch後,SERIALIZABLE的connection也應該被重置回了READ”}{“type”:”text””marks”:[{“type”:”text””text”:”可还是有疑问,我们的查询业务跟springbatch并没有关系。另一方面,即使在使用springbatch后,SERIALIZABLE的connection也应该被重置回了READ”}{“type”:”text””marks”:[{“type”:”text””text”:”可還是有疑問,我們的查詢業務跟springbatch並沒有關係。另一方面,即使在使用springbatch後,SERIALIZABLE的connection也應該被重置回了READ”}{“type”:”text””marks”:[{“type”:”text””text”:”可还是有疑问,我们的查询业务跟springbatch并没有关系。另一方面,即使在使用springbatch后,SERIALIZABLE的connection也应该被重置回了READ”}{“type”:”text””marks”:[{“type”:”italic”}],”text”:”COMMITTED,那麼業務中獲得connection時應當還是READ”},{“type”:”text”,”text”:”COMMITTED,不應該出現問題。正好我們有另一個服務也使用spring batch,測試下來發現過程就如上面預想的,一切正常。看來還有更深的秘密等待著我…”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”根据”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”arthas”}]},{“type”:”text”,”text”:”給出的調用棧信息,定位到spring-orm的”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”EclipseLinkJpaDialect”}]},{“type”:”text”,”text”:”這個方言適配類(我們使用的是EclipseLink而不是Hibernate)。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”image” ,”attrs”:{“src”:”https://static001.geekbang.org/infoq/b8/b8f8151322befc789614b62d9990e782.png”,”alt”:null,”title”:null,”style”:null,”href “:null,”fromPaste”:true,”pastePass”:true}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null, “origin”:null}},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content” :[{“type”:”text”,”text”:”问题就出在划红线的这行。”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”uow.getLogin()”}]},{“type”:”text”,”text”:”獲得是一個可複用的對象(簡單地認為是個單例就好),這個值在設置成SERIALIZABLE後,後續的操作都不能被設置回來。為什麼後面的執行代碼都不能滿足”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT”}]},{“type”:”text”,”text”:”這個條件判斷呢?我們來腦補一下步驟:”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”numberedlist” ,”attrs”:{“start”:1,”normalizeStart”:1},”content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:1,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”执行导出,这时由于使用了spring batch,在获取connection时将其隔离级别设置为SERIALIZABLE (8)。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:2,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”执行完成后,将connection放回pool,HikariCP恢复connection的隔离级别为READ_COMMITTED (2)。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:3,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”一个非导出业务,需要执行数据库操作,从HikariCP的pool中借出一个connection,此时”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”TransactionDefinition”}]},{“type”:”text”,”text”:”的”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”IsolationLevel”}]},{“type”:”text”,”text”:”是默認值”},{“type”:”codeinline”,”content”:[{“type”:”text””text”:”ISOLATION”}{“type”:”text””marks”:[{“type”:”text””text”:”ISOLATION”}{“type”:”text””marks”:[{“type”:”text””text”:”ISOLATION”}{“type”:”text””marks”:[{“type”:”text””text”:”ISOLATION”}{“type”:”text””marks”:[{“type”:”italic”}],”text”:”DEFAULT”}]},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:”(”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”TransactionDefinition”}],”marks”:[{“type”:”italic”}]},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:”其實就是方法上”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”@Transactional”}],”marks”:[{“type”:”italic”}]},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:”註解的信息,一般都不會設置”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”IsolationLevel”}],”marks”:[{“type”:”italic”}]},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:”的)。所以無法滿足條件判斷,”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”uow.getLogin()”}],”marks”:[{“type”:”italic”}]},{“type”:”text”,”marks”:[{“type”:”italic”}],”text”:”的隔離級別還是第1步設置的SERIALIZABLE,這也是為什麼READ”},{“type”:”text”,”text”:”COMMITTED (2)又會被設置成SERIALIZABLE (8 ) 的原因。”}]}]},{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:4,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”这个业务执行完成后,将connection放回pool,HikariCP恢复connection的隔离级别为READ_COMMITTED (2)。”}]}]}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”总算水落石出了,是spring-orm中一个“全局变量”的状态设置出现了问题。我也给spring-orm提了[issue](https://github.com/spring-projects/spring-framework/issues/24802),並提供了[最简Project](https://github.com/deanwong/spring-orm-sample)來方便復現這個bug。感興趣的可以看一下這個project。 “}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”其实spring batch也算是躺枪,假设我们手动设置一个事务隔离级别,也会触发这个异常。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”codeblock” ,”attrs”:{“lang”:”java”},”content”:[{“type”:”text”,”text”:”public interface UserRepository extends JpaRepository {n @Transactional(isolation = Isolation.SERIALIZABLE)n @Overriden List findAll();n}”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text””text”:”所以rootcasue並不是springbatch設置了SERIALIZABLE的隔離級別,而是”}{“type”:”codeinline””content”:[{“type”:”text””text”:”所以rootcasue并不是springbatch设置了SERIALIZABLE的隔离级别,而是”}{“type”:”codeinline””content”:[{“type”:”text””text”:”所以rootcasue並不是springbatch設置了SERIALIZABLE的隔離級別,而是”}{“type”:”codeinline””content”:[{“type”:”text””text”:”所以rootcasue并不是springbatch设置了SERIALIZABLE的隔离级别,而是”}{“type”:”codeinline””content”:[{“type”:”text”,”text”:”uow.getLogin()”}]},{“type”:”text”,”text”:”的狀態處理有bug。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading” ,”attrs”:{“align”:null,”level”:2},”content”:[{“type”:”text”,”text”:”总结”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”整个问题解决差不多用了3天,特别感谢阿里云RDS的SQL洞察服务和阿里出品的”},{“type”:”codeinline”,”content”:[{“type”:”text”,”text”:”arthas”}]},{“type”:”text”,”text”:”(真不是軟廣)。”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”paragraph” ,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”全局state设置一直是个难题,无论是客户端代码还是服务端代码都会面临相同的问题,当我们希望用尽量少的开销保存更多的状态时,复杂度就难以避免。即便Pivotal团队的大神也不能保证bug free。Trouble shooting就像侦破案件一般,有时候黑夜中唯一的一束光就能带来真相,但往往需要巨大的耐心等待它的出现。”}]},{“type”:”horizo​​ntalrule”},{“type”:”blockquote”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”转载自本人博客”}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”原文地址:”},{“type”:”link”,”attrs”:{“href”:”https://www.deanwangpro.com/2019/07/28/zombie-thread-trouble-shooting”,”title”:null},”content”:[{“type”:”text”,”text”:”https://www.deanwangpro.com/2…”}]}]}]},{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}}]}