MySQL死锁问题
问题描述:在一张流水生成的记录表中,当没有当前条件的数据时候,并发情况下会导致有线程因为死锁问题生成流水号失败。
场景
有一张生成流水的表:
场景复现:
简单来说,在根据流水类型、年、月、日生成流水信息的时候有则update流水号+1,无则insert;
Mapper
@Mapper
public interface NumberTestMapper {@Update("update number_test set serial_number = serial_number+1 where number_type = #{numberType} and number_year = #{numberYear} and number_month=#{numberMonth} and number_day = #{numberDay}")int update(NumberTest numberTest);@Insert("insert into number_test values (#{numberType},#{numberYear},#{numberMonth},#{numberDay},#{serialNumber})")int insert(NumberTest numberTest);@Select("select * from number_test where number_type = #{numberType} and number_year = #{numberYear} and number_month=#{numberMonth} and number_day = #{numberDay}")NumberTest selectById(NumberTest numberTest);
}
service
@Service
public class TestService {@Autowiredprivate NumberTestMapper mapper;@Transactionalpublic long generate() {NumberTest test = new NumberTest();test.setNumberType(1);test.setNumberYear("2024");test.setNumberMonth("01");test.setNumberDay("01");int update = mapper.update(test);if(update == 0){test.setSerialNumber(0);mapper.insert(test);}NumberTest select = mapper.selectById(test);return select.getSerialNumber();}
}
测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class NumberTestMapperTest {@Autowiredprivate TestService service;@Testpublic void test() throws InterruptedException {List<Long> result = new CopyOnWriteArrayList<>();CountDownLatch countDownLatch = new CountDownLatch(5);ExecutorService executorService = Executors.newFixedThreadPool(5);for (int i = 0; i < 5; i++) {executorService.submit(() -> {try{result.add(service.generate());}catch (Exception e){e.printStackTrace();}finally {countDownLatch.countDown();}});}countDownLatch.await();executorService.shutdown();result.forEach(item -> System.out.println(item));}@Testpublic void test2() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(10);ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {try{service.insert();}catch (Exception e){e.printStackTrace();}finally {countDownLatch.countDown();}});}countDownLatch.await();executorService.shutdown();System.out.println("success!");}
}
结果
测试在不存在当前行数据的情况下进行
- test方法会有死锁产生
- test2则报主键冲突问题
test方法为什么会产生死锁问题呢?
原因
开启两个事务分别执行:
T1:
BEGIN --step1
--step3
UPDATE number_test set serial_number = serial_number+1 where number_type = 1 and number_year = '2024' and number_month ='01' and number_day = '01';
--step5
insert into number_test VALUES (1,'2024','01','01',0);COMMIT
T2:
BEGIN --step2
--step4
UPDATE number_test set serial_number = serial_number+1 where number_type = 1 and number_year = '2024' and number_month ='01' and number_day = '01';
--step6
insert into number_test VALUES (1,'2024','01','01',0);
COMMIT
按照step1~step5执行发现:
- 执行step5的时候被阻塞,无法获取锁
- 执行step6的时候死锁产生
- 证明执行update的时候是有锁的,那锁的是什么呢?
- 经过多次实验发现,将insert中的number_type修改为2后不会去获取锁,证明update锁的是number_type字段。
将表结构修改:
然后发现:
- update没有使用id主键条件的时候是表锁,使用的时候是行锁
- 在加了联合索引后,问题仍然存在,而且是表锁,执行到step5后任何数据都插不进去
总结:
在并发情况下,多个线程下执行update会有锁,但是update和update直接不会有锁,update和insert之间有锁。insert和update之间相互抢锁形成死锁。
这里不知道为什么在没有数据的时候,同时update相同条件的时候不会有锁,而执行insert的时候就有锁,推测是Mysql为了解决幻读问题使用间隙锁的原因。如果有大神知道,多谢指教!
解决方案:
由于Mysql有解决死锁的机制了,采用try-catch重试解决这个问题
@Transactionalpublic long generate() {try {NumberTest test = new NumberTest();test.setNumberType(1);test.setNumberYear("2024");test.setNumberMonth("01");test.setNumberDay("01");//update number_test set serial_number = serial_number+1 where number_type = #{numberType} and number_year = #{numberYear} and number_month=#{numberMonth} and number_day = #{numberDay}int update = mapper.update(test);if(update == 0){test.setSerialNumber(0);//insert into number_test values (#{numberType},#{numberYear},#{numberMonth},#{numberDay},#{serialNumber})mapper.insert(test);}NumberTest select = mapper.selectById(test);return select.getSerialNumber();}catch (Exception e){return generate();}}