请问各位一个关于并发下MySQL数据库操作的问题

工具

mysql(引擎 innoDB,隔离级别 REPEATABLE-READ)

场景(一次事务中有两个操作)

  1. 更新用户表中的总积分值;
  2. 插入积分记录变动日志(关键字段:当前变动积分数、变动后用户的总分)

数据库表结构

用户表
clipboard.png
用户积分变动表
clipboard.png

相关代码

在数据库中执行的语句

-- 更新用户表中的总积分值
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:

clipboard.png

方法2:
clipboard.png
png]

分析(先且不管数据的正确性)

方法1 个人认为是一个线程开启事务,更新(还未提交)用户信息时,会给该行加上锁,如果事务还未提交(方法还没执行完成),另一个线程只能处于等待状态。但是如果更新不同用户(用户id不一样),则多个用户间不会被阻塞

方法2 实际上除了第一次操作会成功,后面的都会报死锁异常,进而回滚事务,所以只会成功插入一条日志。个人认为这种情况是每个线程进入方法后,先执行插入日志操作(互不影响),线程 sleep x 秒后,多个线程同时需要拿到写锁更新用户,但是只有一个线程能够拿到,其它线程之间相互等待造成的死锁??

不知道本人分析的对不对,方法2的死锁原因也不太明白,还希望有大神能指教一二 ~ 小弟感激不尽

代码地址: https://gitee.com/gegepy/basi...
分支:dev-mysql
测试类:ScoreChangeLogServiceTest

阅读 2.6k
1 个回答

对于方法1,它的执行顺序是

update user where id = 1   ------------------------- (1)
insert log (id,xxx) select x where user id = 1 ------(2)

假设有两个事物A和B,他们对操作1和2的执行顺序如下:

------------事物A------------------------------------------事物B------------------
----更新操作获取user.id=1的X锁-----------------------------------------------------
------------------------------由于user.id=1的X锁已经被其他事物获取,事物B阻塞等待----
---已经持有X锁读取和修改数据并释放锁------------------------------------------------
-------------------------------获取user.id=1的X锁,然后执行一些列的操作-------------

由于所有事物都会在操作(1)处阻塞等待,所以10个事物是串行执行的,不存在死锁的情况。

对于方法2,死锁分析如下:
首先分析死锁日志:

#获取死锁日志命令
show engine innodb status;


=====================================
2018-10-31 10:36:02 7faef1990700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 5 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 59 srv_active, 0 srv_shutdown, 3933403 srv_idle
srv_master_thread log flush and writes: 3933462
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 123
OS WAIT ARRAY INFO: signal count 106
Mutex spin waits 110, rounds 1151, OS waits 25
RW-shared spins 94, rounds 2817, OS waits 93
RW-excl spins 6, rounds 166, OS waits 5
Spin rounds per wait: 10.46 mutex, 29.97 RW-shared, 27.67 RW-excl
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-10-31 10:26:48 7faef190e700
*** (1) TRANSACTION:
TRANSACTION 24058981, ACTIVE 8 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 358, OS thread handle 0x7faef0298700, query id 76122 192.168.2.127 root updating
update t_user
    set score = score + 6
    where id = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 9890 page no 3 n bits 72 index `PRIMARY` of table `ahamp-cms`.`t_user` trx id 24058981 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 0000016f1c5d; asc    o ];;
 2: len 7; hex 75000001481517; asc u   H  ;;
 3: len 4; hex 74657374; asc test;;
 4: len 4; hex 80000037; asc    7;;

*** (2) TRANSACTION:
TRANSACTION 24058980, ACTIVE 8 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 357, OS thread handle 0x7faef190e700, query id 76125 192.168.2.127 root updating
update t_user
    set score = score + 10
    where id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 9890 page no 3 n bits 72 index `PRIMARY` of table `ahamp-cms`.`t_user` trx id 24058980 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 0000016f1c5d; asc    o ];;
 2: len 7; hex 75000001481517; asc u   H  ;;
 3: len 4; hex 74657374; asc test;;
 4: len 4; hex 80000037; asc    7;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 9890 page no 3 n bits 72 index `PRIMARY` of table `ahamp-cms`.`t_user` trx id 24058980 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 0000016f1c5d; asc    o ];;
 2: len 7; hex 75000001481517; asc u   H  ;;
 3: len 4; hex 74657374; asc test;;
 4: len 4; hex 80000037; asc    7;;

*** WE ROLL BACK TRANSACTION (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锁是什么。

  • S锁:读锁(共享锁)是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。 如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
  • X锁:写锁(排他锁)如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

在分析下方法中获取锁的顺序:

insert log (id,xxx) select x where user id = 1 ------ (1)
update user where id = 1 -----------------------------(2)

操作(1)获取user.id=1的S锁,操作(2)获取user.id=1的X锁事物A和事物B获取锁的顺序如下:

------------事物A------------------------------------------事物B------------------
----更新操作获取user.id=1的S锁-----------------------------------------------------
--------------------------------------S锁是共享锁,事物B也能获取user.id=1的S锁------
--写数据时需要获取X锁,但是事务A已经-------------------------------------------------
--获取了user.id=1的S锁,这里只能等待------------------------------------------------
----------------------------------同样需要获取user.id=1的X锁,此时发现已经形成了死锁,
---------------------------------------------------就会滚当前事务退出--------------
---其他事物退出得到X锁成功更新-----------------------------------------------------

以上就是整个过程,到最后只有部分事务正常提交。
题主可以关注下aneasystone's blog,他的一系列博客写的还是比较详细的。
mysql读写锁及事务

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题