线上出现MySQL死锁了

90 阅读3分钟

一、线上问题

在周会上,组内同事介绍了一个关于死锁的原因以及解决方案,觉得还挺有意思的。在这里,记录一下。

这是一个异步更新学生宠物经验的场景。学生有多个宠物或者宠物碎片,当在课上产生经验值数据之后,需要更新宠物。为了防止MySQL拖慢课上业务,使用Kafka异步的方式进行处理。如果一位学生有个多宠物碎片需要更新,那么按照下面代码就会死锁的原因。具体业务流程,如下所示:

为了更加方便理解,我们抽象下上述操作,服务收到两个Kafka消息,一个是【A碎片加经验,B碎片加经验】,另外一个是【B碎片加经验,B碎片加经验】。这样的话,就有可能出现阻塞等待的情况。

针对于上述死锁的情况,有两种解决方案:

  • 锁;
  • 所有更新的数据进行排序;

二、线下模拟

构建测试环境

我们这边的数据库事物隔离级别是读提交,因此,将数据也改为读提交模式。按照下述步骤,创建数据库以及表;


(base) ➜  ~ docker run -d --name mysql-container1 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 mysql:latest

(base) ➜  ~ mysql -h 127.0.0.1 -P 3306 -u root

// 将事物模式改为读提交
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+


mysql> CREATE DATABASE d1;

mysql> CREATE TABLE users (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     name VARCHAR(255),
    ->     age INT
    -> );

mysql> INSERT INTO users (ID, Name, Age) VALUES
    -> (3, 'Charlie', 80),
    -> (1, 'Alice', 50),
    -> (2, 'Bob', 60);

模拟代码

func TestDemo1(t *testing.T) {
	dsn := "root@tcp(127.0.0.1:3306)/d1?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("无法连接数据库")
	}

	// 起多个协程更新数据行
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 20; i++ {
		go func() {
     // 如果打散,随机,就会出现死锁
			users1 := shuffle(users)

      // 如果不随机,不会出现死锁
			//users1 := users
			tx := db.Begin()

			defer func() {
				wg.Done()
				// 提交事务
				tx.Commit()
			}()

			for _, user := range users1 {
				fmt.Println(user)
				//time.Sleep(time.Second)
				// 使用事务执行插入操作
				if err := tx.Updates(&user).Error; err != nil {
					// 插入失败,回滚事务
					tx.Rollback()
					fmt.Println("更新失败:", err)
					return
				}
			}
		}()
	}

	wg.Wait()
	fmt.Println("Done")
}

// 将切片打散随机
func shuffle(slice []User) []User {
	n := len(slice)
	shuffledSlice := make([]User, n)
	copy(shuffledSlice, slice)

	// Fisher-Yates 洗牌算法
	for i := n - 1; i > 0; i-- {
		j := rand.Intn(i + 1)
		shuffledSlice[i], shuffledSlice[j] = shuffledSlice[j], shuffledSlice[i]
	}

	return shuffledSlice
}

在上述代码中,如果我们在每个协程开始之前,将所有的数据进行打散,就会出现死锁的情况(也就是图二中,资源互相等待的情况)。针对于这种情况,我们的解决方案是,在每个协程开始的时候加一个锁,或者将所有的资源进行排序,使得资源不会出现循环等待的情况;

三、总结

在业务开发中,如果使用事务,并且具有批量更新的操作,需要重点考虑下,是否会出现死锁的情况。