一、线上问题
在周会上,组内同事介绍了一个关于死锁的原因以及解决方案,觉得还挺有意思的。在这里,记录一下。
这是一个异步更新学生宠物经验的场景。学生有多个宠物或者宠物碎片,当在课上产生经验值数据之后,需要更新宠物。为了防止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
}
在上述代码中,如果我们在每个协程开始之前,将所有的数据进行打散,就会出现死锁的情况(也就是图二中,资源互相等待的情况)。针对于这种情况,我们的解决方案是,在每个协程开始的时候加一个锁,或者将所有的资源进行排序,使得资源不会出现循环等待的情况;
三、总结
在业务开发中,如果使用事务,并且具有批量更新的操作,需要重点考虑下,是否会出现死锁的情况。