Go 语言操作 MySQL 更新数据,使用事务以后耗时反而增加 30 倍 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
billion
V2EX    Go 编程语言

Go 语言操作 MySQL 更新数据,使用事务以后耗时反而增加 30 倍

  •  
  •   billion
    kingname 2017-07-21 11:22:29 +08:00 3914 次点击
    这是一个创建于 3015 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一开始开了 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 分钟。请问为什么用事务反而导致效率严重降低了?

    49 条回复    2017-07-22 18:14:40 +08:00
    sagaxu
        1
    sagaxu  
       2017-07-21 11:25:05 +08:00
    你 paraArray 满 1000 个插入后,什么时候清空?
    billion
        2
    billion  
    OP
       2017-07-21 11:25:45 +08:00
    @sagaxu 插完以后就清空,Commit 后面,代码已经修改
    ipconfiger
        3
    ipconfiger  
       2017-07-21 11:26:36 +08:00
    事务就是批量?????????
    fqzz
        4
    fqzz  
       2017-07-21 11:27:01 +08:00
    难道事务还能让 update 更快?
    billion
        5
    billion  
    OP
       2017-07-21 11:28:30 +08:00
    @fqzz 根据网友的实际测试,正常情况下把很多条语句放到事务里面一次性 Commit,会快非常多,特别是对于减少网络 IO 的时间消耗很有用。我这个应该是不知道哪里不对。
    sagaxu
        6
    sagaxu  
       2017-07-21 11:29:03 +08:00
    @billion 改成只用一个 goroutine 看看时间是否有变化
    billion
        7
    billion  
    OP
       2017-07-21 11:31:54 +08:00
    @sagaxu 我也测试过了。依旧非常满。
    sagaxu
        8
    sagaxu  
       2017-07-21 11:33:56 +08:00
    @billion 如果 1 个 routine 跟 100 个 routine 性能一样了,说明这是伪并发了
    billion
        9
    billion  
    OP
       2017-07-21 11:36:18 +08:00
    @sagaxu 应该是事务的问题。如果我把事务里面只执行 1 条语句,那么速度又可以达到大概 5 秒 1000 条。虽然还是没有直接执行快,但是已经显著超过在事务里面执行 1000 条。
    jarlyyn
        10
    jarlyyn  
       2017-07-21 11:39:11 +08:00
    这代码把我看的一愣一愣的。

    发现最近愣的比较多……

    发现楼主连 go 这么玩不出花的语言都能玩的这么神奇,不做产品可惜了。
    mansur
        11
    mansur  
       2017-07-21 11:41:03 +08:00
    go 不太熟。range chan 遍历后消费掉了吗?没有消费掉岂不是每个协程都跑一遍。你第一种写法可是消费掉的。
    jarlyyn
        12
    jarlyyn  
       2017-07-21 11:48:22 +08:00
    吐槽完了具体说说。

    说先,不知道为啥要开协程…………只是为了给 mysql 做压力测试么……

    如果我没理解错的话,是 100 个协程同时链接 Mysql,然后还开事务,互相锁表竞争?感觉这个是最容易出问题的地方

    更何况,如果 Chan 长度是 1000 的话,代码 1 更新了 1000 条记录

    代码 2 更新了 1000×100 条记录

    其次。代码 1prepare 了,代码 2 没有。虽然我不知道大妈 1 为啥每次都要 preapre

    第三。每次都 append 一下干嘛。虽然对效率影响微乎其微,但既然是长度固定是 1000,直接 make 个 1000 的不就不了。
    billion
        13
    billion  
    OP
       2017-07-21 11:49:14 +08:00
    @mansur 消费掉了的。channel 相当于 Python 里面的 queue,用一个少一个。
    billion
        14
    billion  
    OP
       2017-07-21 11:50:47 +08:00
    @jarlyyn 因为在生产环境里面的代码是分布在多个函数里面的,我这里把它们放在了一起。
    billion
        15
    billion  
    OP
       2017-07-21 11:52:37 +08:00
    @jarlyyn 代码 1 更新 1000 条记录,用时 2 秒左右。代码 2 每个协程都需要大概 1 分钟才能更新 1000 条记录。
    jarlyyn
        16
    jarlyyn  
       2017-07-21 12:02:47 +08:00
    @billion

    好吧,我大概明白了。

    你这代码肯定和线上的不太一样。

    但这代码问题也实在太大了。

    现在的问题就是,1000 条记录,一次 commit,时间长的夸张对吗?

    按道理来说,开事务是没道理比不开事务快的,但是慢这么多也是不符合常理的。
    specita
        17
    specita  
       2017-07-21 12:04:19 +08:00
    你这个测试代码感觉没对,我自己简单测了下
    one by one spend: 364.546674ms
    transaction spend: 295.024446ms

    事务应该是更快的
    jarlyyn
        18
    jarlyyn  
       2017-07-21 12:06:00 +08:00
    由于不知道你的服务器软硬件情况,能否跑个不带协程不带条数判断的单纯写入测试?

    从我的角度看,你的代码应该是这样的:

    有个全局 slice,操作带锁。
    每满一千条,推到 chan 里。

    读 chan 写入数据库的可以是单协程,可以是多协程。一次写 1000 条。
    billion
        19
    billion  
    OP
       2017-07-21 12:33:10 +08:00
    @specita 你也是用的 GO 语言吗?用的 MySQL driver 是 github.com/go-sql-driver/mysql 吗?
    pubby
        20
    pubby  
       2017-07-21 12:52:04 +08:00 via Android
    更新的字段有索引吗,有的话这样并发死锁几率应该挺高的
    elgae
        21
    elgae  
       2017-07-21 13:20:06 +08:00 via iPhone
    循环内开事务,不对吧。
    cloudzhou
        22
    cloudzhou  
       2017-07-21 13:31:09 +08:00
    @billion
    ```
    stmt, _ := db.Prepare("update.....")
    for i:=0; i<100;i++{
    go func(chan, stmt){
    para1 := <- chan
    stmt.Exec(para1)
    }
    }
    stmt 本身就是并发安全的,你改成这样试试看,效率如何
    nadoo
        23
    nadoo  
       2017-07-21 13:57:55 +08:00
    后面的代码有点难懂,直观地看起来 2 个 for 循环,还都在从一个 chan 读数据?代码都不清晰的话,可能问题就很多了
    billion
        24
    billion  
    OP
       2017-07-21 15:18:47 +08:00
    @nadoo 这个 chan 你可以理解为 Python 的 Queue。本来就应该是从 chan 里面读数据。
    msg7086
        25
    msg7086  
       2017-07-21 15:19:35 +08:00
    你是不是应该先说说是 Go 拖慢了还是 MySQL 拖慢了?
    首先网络是本地网,那么传输肯定是瞬时完成的。
    你打开 htop / iotop 之类的看一下,你这 1 分钟里是谁在吃 CPU,谁在吃磁盘 IO,谁在卡。

    从上到下这回复我看得一愣一愣的,23 层楼了还没确定是 MySQL 卡了还是 Go 程序卡了。
    msg7086
        26
    msg7086  
       2017-07-21 15:20:54 +08:00
    然后如果是 Go 卡了,这我就不管了,不懂 Go。
    如果是 MySQL 卡,一个是看看 Process List,啥语句卡,卡在哪步。
    另一个是看一下 MySQL 的 statistics,看看有没有异常数值出现。
    khowarizmi
        27
    khowarizmi  
       2017-07-21 15:32:03 +08:00
    补充楼上,如果是 Golang 的问题,用 pprof 查一下。
    https://golang.org/pkg/net/http/pprof/
    billion
        28
    billion  
    OP
       2017-07-21 15:35:14 +08:00
    @msg7086 是远程的 MySQL
    billion
        29
    billion  
    OP
       2017-07-21 15:36:45 +08:00
    @cloudzhou 我现在就是这样用的,速度大概 1-2 秒 1000 条记录
    nadoo
        30
    nadoo  
       2017-07-21 16:09:51 +08:00
    @billion 后面的代码,100 个 goroutine 同时等待同一个 chan,如果每个 goroutine 速度差不多,要等到这 100 个 goroutine 内都满了 1000 个 param 才开始提交任务。也就是 2 个条件:1.chan 内数据填满 100*1000=100000,2. 填满后,100 个 goroutine 同时提交事务,每个事务中有 1000 条语句。

    感觉两段代码没什么可比性,真的要用 chan 的话,也应该是多个地方往 chan 里面写,然后只有一个地方读取 chan 并更新数据库,满 1000 条提交一次事务。
    sun1991
        31
    sun1991  
       2017-07-21 16:25:03 +08:00
    各种数据库机制不同, MySQL 推荐做法是长事务还是短事物? 1000 个 update 提交一次会不会升级为表级锁?
    billion
        32
    billion  
    OP
       2017-07-21 16:33:34 +08:00
    @nadoo 你这个想法很有意思。我去试一试。
    billion
        33
    billion  
    OP
       2017-07-21 16:35:42 +08:00
    @nadoo 问题是,如果几个地方往 chan 写的速度很快。复杂提交事务的那个地方正在提交的过程中,此时另外 1000 个又来了,那就只有等待。
    jarlyyn
        34
    jarlyyn  
       2017-07-21 16:38:39 +08:00
    @billion

    你需要一个函数。两个 chan
    比如
    chan record
    chan record[]
    这个函数的作用是从单条记录的 chan recrod 里读数据,拼够数据后,写入 chan record[]
    操作的时候记得加锁
    操作数据库的协程读 chan record[],再写入。
    billion
        35
    billion  
    OP
       2017-07-21 16:54:33 +08:00
    @jarlyyn 正式代码里面就是这样的。这个帖子的代码经过精简。协程没有必要加锁吧,协程又不会发送读写冲突。
    kurtzhong
        36
    kurtzhong  
       2017-07-21 17:08:04 +08:00
    开一百个线程有什么意义?
    jarlyyn
        37
    jarlyyn  
       2017-07-21 17:13:34 +08:00
    @billion

    你需要把 chan record 的记录一条条读出来,写入一个临时的 record[]

    临时的 record[]写满后,再写入 chan record[]
    specita
        38
    specita  
       2017-07-21 17:24:23 +08:00
    @billion 用的 go,我认为你慢的原因不在于事务,而是你的这段测试代码写的真的有问题..,上面也有人说,让你先确定是 mysql 问题还是代码执行的问题
    nadoo
        39
    nadoo  
       2017-07-21 18:08:15 +08:00
    @billion 如果数据库就是慢,那是没办法的。就只有使用缓冲 channel,排队更新数据库,这样写 chan 的地方也不会阻塞,待并发时段过去了,就好了。
    amghost
        40
    amghost  
       2017-07-21 18:10:27 +08:00 via iPhone
    为什么会扯到 mysql 去我也是不懂。各位看看这两段代码,假设所有 goroutine 获取到的 para 数量评论,显然在第二个案例里面的一个 goroutine 跑完 1000 个的时候整个系统已经提供 100x1000 个 para,这里 chan<-的速率是怎样的?数据是不是有重叠的?如果有重叠那事务之间就互斥了
    billion
        41
    billion  
    OP
       2017-07-21 22:49:30 +08:00
    @amghost 数据确实可能有重叠
    billion
        42
    billion  
    OP
       2017-07-21 22:49:54 +08:00
    @specita 我怀疑应该是代码的问题。
    xaxb
        43
    xaxb  
       2017-07-21 23:18:16 +08:00
    mysql 默认单数据更新语句自动事物,你只发了一条 updae....,Mysql 会自动用 Begin 和 Commit 把你的语句事物化。单个语句加显式事物没什么意义
    akira
        44
    akira  
       2017-07-21 23:49:30 +08:00
    1. 看看你的 update 是不是导致表锁了.
    2. 多条语句合并到一个事务内提交以提高效率 比较适合 用于单会话 导数据
    3. 高并发下,事务越短越好。
    wdlth
        45
    wdlth  
       2017-07-22 00:16:18 +08:00
    LZ 的 MySQL 用的是 InnoDB 还是 MyISAM 存储引擎?
    billion
        46
    billion  
    OP
       2017-07-22 07:52:18 +08:00
    @wdlth InnoDB
    billion
        47
    billion  
    OP
       2017-07-22 07:52:34 +08:00
    @akira 第三条很重要。
    abcbuzhiming
        48
    abcbuzhiming  
       2017-07-22 10:14:20 +08:00
    @wdlth MyISAM 没有事务,能开事务必然是 InnoDB
    wdlth
        49
    wdlth  
       2017-07-22 18:14:40 +08:00
    @abcbuzhiming MariaDB 的 Aria 也能开事务,不过他没明确说是 MariaDB,那应该还是 InnoDB。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5328 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 47ms UTC 08:10 PVG 16:10 LAX 01:10 JFK 04:10
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86