Golang Go语言操作 MySQL 更新数据,使用事务以后耗时反而增加 30 倍
一开始开了 100 个 goroute,在每个 goroute 里面一条一条更新数据:
for i:=0; i<100;i++{
go func(chan){
para1 := <- chan
stmt, _ := db.Prepare("update.....")
stmt.Exec(para1)
}
}
可以做到 2 秒钟更新 1000 条。
后来改用事务来批量更新
for i:=0; i<100;i++{
go func(chan){
var paraArray []string
for para := range chan{
paraArray = append(paraArray, para)
if len(paraArray) >= 1000 {
tx, _ := db.Begin()
for _, para := range paraArray{
tx.Exec("update.....", para)
}
tx.Commit()
paraArray = paraArray[:0]
}
}
}
}
这样每个事务里面的 1000 条语句,运行时间高达 1 分钟。请问为什么用事务反而导致效率严重降低了?
Golang Go语言操作 MySQL 更新数据,使用事务以后耗时反而增加 30 倍
更多关于Golang Go语言操作 MySQL 更新数据,使用事务以后耗时反而增加 30 倍的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你 paraArray 满 1000 个插入后,什么时候清空?
更多关于Golang Go语言操作 MySQL 更新数据,使用事务以后耗时反而增加 30 倍的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
插完以后就清空,Commit 后面,代码已经修改
事务就是批量???
根据网友的实际测试,正常情况下把很多条语句放到事务里面一次性 Commit,会快非常多,特别是对于减少网络 IO 的时间消耗很有用。我这个应该是不知道哪里不对。
改成只用一个 goroutine 看看时间是否有变化
我也测试过了。依旧非常满。
如果 1 个 routine 跟 100 个 routine 性能一样了,说明这是伪并发了
应该是事务的问题。如果我把事务里面只执行 1 条语句,那么速度又可以达到大概 5 秒 1000 条。虽然还是没有直接执行快,但是已经显著超过在事务里面执行 1000 条。
这代码把我看的一愣一愣的。
发现最近愣的比较多……
发现楼主连 go 这么玩不出花的语言都能玩的这么神奇,不做产品可惜了。
go 不太熟。range chan 遍历后消费掉了吗?没有消费掉岂不是每个协程都跑一遍。你第一种写法可是消费掉的。
吐槽完了具体说说。
说先,不知道为啥要开协程…………只是为了给 mysql 做压力测试么……
如果我没理解错的话,是 100 个协程同时链接 Mysql,然后还开事务,互相锁表竞争?感觉这个是最容易出问题的地方
更何况,如果 Chan 长度是 1000 的话,代码 1 更新了 1000 条记录
代码 2 更新了 1000×100 条记录
其次。代码 1prepare 了,代码 2 没有。虽然我不知道大妈 1 为啥每次都要 preapre
第三。每次都 append 一下干嘛。虽然对效率影响微乎其微,但既然是长度固定是 1000,直接 make 个 1000 的不就不了。
消费掉了的。channel 相当于 Python 里面的 queue,用一个少一个。
因为在生产环境里面的代码是分布在多个函数里面的,我这里把它们放在了一起。
代码 1 更新 1000 条记录,用时 2 秒左右。代码 2 每个协程都需要大概 1 分钟才能更新 1000 条记录。
好吧,我大概明白了。
你这代码肯定和线上的不太一样。
但这代码问题也实在太大了。
现在的问题就是,1000 条记录,一次 commit,时间长的夸张对吗?
按道理来说,开事务是没道理比不开事务快的,但是慢这么多也是不符合常理的。
你这个测试代码感觉没对,我自己简单测了下
one by one spend: 364.546674ms
transaction spend: 295.024446ms
事务应该是更快的
由于不知道你的服务器软硬件情况,能否跑个不带协程不带条数判断的单纯写入测试?
从我的角度看,你的代码应该是这样的:
有个全局 slice,操作带锁。
每满一千条,推到 chan 里。
读 chan 写入数据库的可以是单协程,可以是多协程。一次写 1000 条。
你也是用的 GO 语言吗?用的 MySQL driver 是 github.com/go-sql-driver/mysql 吗?
更新的字段有索引吗,有的话这样并发死锁几率应该挺高的
循环内开事务,不对吧。
```
stmt, _ := db.Prepare(“update…”)
for i:=0; i<100;i++{
go func(chan, stmt){
para1 := <- chan
stmt.Exec(para1)
}
}
stmt 本身就是并发安全的,你改成这样试试看,效率如何
后面的代码有点难懂,直观地看起来 2 个 for 循环,还都在从一个 chan 读数据?代码都不清晰的话,可能问题就很多了
这个 chan 你可以理解为 Python 的 Queue。本来就应该是从 chan 里面读数据。
你是不是应该先说说是 Go 拖慢了还是 MySQL 拖慢了?
首先网络是本地网,那么传输肯定是瞬时完成的。
你打开 htop / iotop 之类的看一下,你这 1 分钟里是谁在吃 CPU,谁在吃磁盘 IO,谁在卡。
从上到下这回复我看得一愣一愣的,23 层楼了还没确定是 MySQL 卡了还是 Go 程序卡了。
然后如果是 Go 卡了,这我就不管了,不懂 Go。
如果是 MySQL 卡,一个是看看 Process List,啥语句卡,卡在哪步。
另一个是看一下 MySQL 的 statistics,看看有没有异常数值出现。
补充楼上,如果是 Golang 的问题,用 pprof 查一下。
https://golang.org/pkg/net/http/pprof/
是远程的 MySQL
我现在就是这样用的,速度大概 1-2 秒 1000 条记录
后面的代码,100 个 goroutine 同时等待同一个 chan,如果每个 goroutine 速度差不多,要等到这 100 个 goroutine 内都满了 1000 个 param 才开始提交任务。也就是 2 个条件:1.chan 内数据填满 100*1000=100000,2. 填满后,100 个 goroutine 同时提交事务,每个事务中有 1000 条语句。
感觉两段代码没什么可比性,真的要用 chan 的话,也应该是多个地方往 chan 里面写,然后只有一个地方读取 chan 并更新数据库,满 1000 条提交一次事务。
各种数据库机制不同, MySQL 推荐做法是长事务还是短事物? 1000 个 update 提交一次会不会升级为表级锁?
你这个想法很有意思。我去试一试。
问题是,如果几个地方往 chan 写的速度很快。复杂提交事务的那个地方正在提交的过程中,此时另外 1000 个又来了,那就只有等待。
你需要一个函数。两个 chan
比如
chan record
chan record[]
这个函数的作用是从单条记录的 chan recrod 里读数据,拼够数据后,写入 chan record[]
操作的时候记得加锁
操作数据库的协程读 chan record[],再写入。
正式代码里面就是这样的。这个帖子的代码经过精简。协程没有必要加锁吧,协程又不会发送读写冲突。
开一百个线程有什么意义?
你需要把 chan record 的记录一条条读出来,写入一个临时的 record[]
临时的 record[]写满后,再写入 chan record[]
用的 go,我认为你慢的原因不在于事务,而是你的这段测试代码写的真的有问题…,上面也有人说,让你先确定是 mysql 问题还是代码执行的问题
如果数据库就是慢,那是没办法的。就只有使用缓冲 channel,排队更新数据库,这样写 chan 的地方也不会阻塞,待并发时段过去了,就好了。
为什么会扯到 mysql 去我也是不懂。各位看看这两段代码,假设所有 goroutine 获取到的 para 数量评论,显然在第二个案例里面的一个 goroutine 跑完 1000 个的时候整个系统已经提供 100x1000 个 para,这里 chan<-的速率是怎样的?数据是不是有重叠的?如果有重叠那事务之间就互斥了
数据确实可能有重叠
我怀疑应该是代码的问题。
mysql 默认单数据更新语句自动事物,你只发了一条 updae…,Mysql 会自动用 Begin 和 Commit 把你的语句事物化。单个语句加显式事物没什么意义
- 看看你的 update 是不是导致表锁了.
2. 多条语句合并到一个事务内提交以提高效率 比较适合 用于单会话 导数据
3. 高并发下,事务越短越好。
LZ 的 MySQL 用的是 InnoDB 还是 MyISAM 存储引擎?
InnoDB
第三条很重要。
MyISAM 没有事务,能开事务必然是 InnoDB
MariaDB 的 Aria 也能开事务,不过他没明确说是 MariaDB,那应该还是 InnoDB。