0%

Mysql并法写入的思考与代码的佐证及优化

SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
一个项目中涉及到了对MySQL数据的并发更新,数据写入失败,而这个项目中些这部分代码的程序员没有写事务处理,一下子丢了很多很多订单

事出有因

昨天手贱在生产环境中搭建k8s,原本以为也就是跑几个容器嘛,实在不行就把容器删除了,结果技艺不精,一顿操作猛如虎,一句systemctl restart docker崩成屎!!!
虽然Docker的问题后来也解决了,但是由于项目中某个接口需要php7.3版本,而7.3版本是在docker中提供的,在我解决问题的同时,有很多订单堆积在了Redis中等待处理,等我将 Docker 修好,重新run一个php-fpm的时候,看着console中的入库成功,悬着的心终于落地了,但是这心放在肚子里没过几秒,大面积的error刷的我头皮发麻,我马上结束了处理订单的脚本,查看error,结果发现了SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
搜索了一下,发现是由于并法写入导致的问题

解决问题

既然知道是并法导致的问题,那么解决的方案也很简单了

  1. 控制写入的线程数量
  2. 让程序员写事务处理,入库失败重新入库
  3. 使用淘宝提供的api查询制定时间段的订单,重新入库

虽然开发人员在编写代码的时候已经考虑到订单数量很大(40+淘宝店),使用了多线程来处理订单,但是在这次实际使用过程中发现实际上并不仅仅是多线程这么简单

首先多线程确实是可以在处理多个订单的时候效率很高,但是在入库的时候数据库却成为了新的瓶颈

由于之前项目使用的是PHP,但是开发人员开发的时候并没有使用swoole等技术来实现一个数据库连接池,导致每次操作都要开一个MySQL线程,大家应该知道,MySQL每个连接会占用1个线程,大量的系统资源将被浪费在线程间上下文切换上,大量的线程写入的时候,会锁表,更新的时候还会有行锁,这会导致多线程也并没有什么卵用

在搜索这方面问题的时候,我看到了MySQL 死锁问题:它实际只能“并发读,单线程写入”?

1
1 个表,存在唯一索引(主键索引也算唯一索引)。先让 1 个 session 去插入唯一索引所在列,然后 hold 在那里即不回滚,也不提交。此时另外启动两个 session,也插入同样的列值;这两个 session 为了检测唯一索引是否已经存在,会去申请共享锁,但由于此时第一个 session 持有互斥锁,会让后两个 session 申请共享锁的行为阻塞。2 和 3 的 session 就会等在那里;此时 1 的 session 回滚,释放掉互斥锁,2 和 3 的 session 会同时获得共享锁;他们要进一步获得互斥锁才能进行插入,但是,因为互斥锁和共享锁是冲突的,会形成,2 为了得到互斥锁必须等待 3 释放共享锁和 3 为了得到互斥锁也必须等待 2 释放共享锁的死锁现象,直到死锁超时回滚

我猜测 这也就是上面错误出现的原因

代码测试(Golang)

数据库多线程写入

首先创建数据库,测试数据库多线程写入问题

数据库字段

1
2
id|created_at|updated_at|deleted_at|username|password|
--|----------|----------|----------|--------|--------|

测试核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Users struct {
gorm.Model
Username string
Password string
}
func init() {
SuDB, err = gorm.Open("mysql", "root:root@tcp(172.20.0.2)/my_test?charset=utf8&parseTime=True&loc=Local")
fmt.Println(SuDB)
}
func main() {
var wg sync.WaitGroup
//insert插入
susu := &Users{gorm.Model{CreatedAt: time.Now()}, "lol", "123456"}
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int, susu Users) { //这里开启协程模拟并发
susu.Username += strconv.Itoa(i)
create := SuDB.Create(&susu)
fmt.Println(create.Error)
//fmt.Println(susu)
wg.Done()
}(i, *susu)
}
wg.Wait()
}