工具
mysql(引擎 innoDB,隔离级别 REPEATABLE-READ)
场景(一次事务中有两个操作)
- 更新用户表中的总积分值;
- 插入积分记录变动日志(关键字段:当前变动积分数、变动后用户的总分)
数据库表结构
用户表
用户积分变动表
相关代码
在数据库中执行的语句
-- 更新用户表中的总积分值
update t_user set score = score + #{score,jdbcType=INTEGER} where id = #{id,jdbcType=INTEGER}
-- 插入积分记录变动日志,其中总分需要从用户表中拿
insert into t_score_change_log (id, user_id, change_score, total_score)
select #{id,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{changeScore,jdbcType=INTEGER},
score from t_user where id = #{userId,jdbcType=INTEGER} limit 1
执行方法1 (先更新用户表再插入日志):
@Transactional(rollbackFor = Exception.class)
public void addScore(Integer userId, int changeScore) throws Exception {
User newUser = new User();
newUser.setId(userId);
newUser.setScore(changeScore);
boolean flag = userService.addScore(newUser);
if (flag) {
log.info("更新用户成功,用户 {},加分 {}", userId, changeScore);
Thread.sleep(System.currentTimeMillis()%10*1000);
ScoreChangeLog changeLog = new ScoreChangeLog();
changeLog.setUserId(userId);
changeLog.setChangeScore(changeScore);
scoreChangeLogService.addForConcurrent(changeLog);
log.info("新增日志成功,用户 {},加分 {}", userId, changeScore);
}
}
执行方法2 (先插入日志再更新用户表):
@Transactional(rollbackFor = Exception.class)
public void addScore(Integer userId, int changeScore) throws Exception {
ScoreChangeLog changeLog = new ScoreChangeLog();
changeLog.setUserId(userId);
changeLog.setChangeScore(changeScore);
boolean flag =scoreChangeLogService.addForConcurrent(changeLog);
if (flag) {
log.info("新增日志成功,用户 {},加分 {}", userId, changeScore);
Thread.sleep(System.currentTimeMillis()%10*1000);
User newUser = new User();
newUser.setId(userId);
newUser.setScore(changeScore);
userService.addScore(newUser);
log.info("更新用户成功,用户 {},加分 {}", userId, changeScore);
}
}
并发测试方法:
@Test
public void test() throws Exception {
int threadCount = 10;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
CountDownLatch flagLatch = new CountDownLatch(1);
for (int i=0;i<threadCount;i++) {
new Thread(() -> {
try {
flagLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取一个 1 - 10 的随机数
int changeScore = NumberUtil.getRandomInt(1, 10);
try {
userFacade.addScore(1, changeScore);
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
// 所有线程同时开始执行
flagLatch.countDown();
countDownLatch.await();
log.info("执行完成~~");
}
结果
方法1:
方法2:
png]
分析(先且不管数据的正确性)
方法1 个人认为是一个线程开启事务,更新(还未提交)用户信息时,会给该行加上锁,如果事务还未提交(方法还没执行完成),另一个线程只能处于等待状态。但是如果更新不同用户(用户id不一样),则多个用户间不会被阻塞
方法2 实际上除了第一次操作会成功,后面的都会报死锁异常,进而回滚事务,所以只会成功插入一条日志。个人认为这种情况是每个线程进入方法后,先执行插入日志操作(互不影响),线程 sleep x 秒后,多个线程同时需要拿到写锁更新用户,但是只有一个线程能够拿到,其它线程之间相互等待造成的死锁??
不知道本人分析的对不对,方法2的死锁原因也不太明白,还希望有大神能指教一二 ~ 小弟感激不尽
代码地址: https://gitee.com/gegepy/basi...
分支:dev-mysql
测试类:ScoreChangeLogServiceTest
对于方法1,它的执行顺序是
假设有两个事物A和B,他们对操作1和2的执行顺序如下:
由于所有事物都会在操作(1)处阻塞等待,所以10个事物是串行执行的,不存在死锁的情况。
对于方法2,死锁分析如下:
首先分析死锁日志:
从上面日志可以看出,事物1等待
lock_mode X locks rec but not gap waiting
,也就是X锁,事物2持有lock mode S locks rec but not gap
也就是S锁,等待X锁。那么这两个事物是如何形成死锁的呢? 先理解下S锁和X锁是什么。
在分析下方法中获取锁的顺序:
操作(1)
获取user.id=1
的S锁,操作(2)
获取user.id=1
的X锁事物A和事物B获取锁的顺序如下:以上就是整个过程,到最后只有部分事务正常提交。
题主可以关注下aneasystone's blog,他的一系列博客写的还是比较详细的。
mysql读写锁及事务