The Go Programming Language
› http://golang.org/
› Go Playground
Go Projects
› Revel Web Framework
billion

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

  •  
  •   billion ·
    kingname · Jul 21, 2017 · 4673 views
    This topic created in 3392 days ago, the information mentioned may be changed or developed.

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

    发现最近愣的比较多……

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

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

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

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

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

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

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

    好吧,我大概明白了。

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

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

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

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

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

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

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

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

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

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

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

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

    临时的 record[]写满后,再写入 chan record[]
    specita
    specita
    Jul 21, 2017
    38
    @billion 用的 go,我认为你慢的原因不在于事务,而是你的这段测试代码写的真的有问题..,上面也有人说,让你先确定是 mysql 问题还是代码执行的问题
    nadoo
    nadoo
    Jul 21, 2017
    39
    @billion 如果数据库就是慢,那是没办法的。就只有使用缓冲 channel,排队更新数据库,这样写 chan 的地方也不会阻塞,待并发时段过去了,就好了。
    amghost
    amghost
    Jul 21, 2017 via iPhone
    40
    为什么会扯到 mysql 去我也是不懂。各位看看这两段代码,假设所有 goroutine 获取到的 para 数量评论,显然在第二个案例里面的一个 goroutine 跑完 1000 个的时候整个系统已经提供 100x1000 个 para,这里 chan<-的速率是怎样的?数据是不是有重叠的?如果有重叠那事务之间就互斥了
    billion
    billion
    OP
    Jul 21, 2017
    41
    @amghost 数据确实可能有重叠
    billion
    billion
    OP
    Jul 21, 2017
    42
    @specita 我怀疑应该是代码的问题。
    xaxb
    xaxb
    Jul 21, 2017
    43
    mysql 默认单数据更新语句自动事物,你只发了一条 updae....,Mysql 会自动用 Begin 和 Commit 把你的语句事物化。单个语句加显式事物没什么意义
    akira
    akira
    Jul 21, 2017
    44
    1. 看看你的 update 是不是导致表锁了.
    2. 多条语句合并到一个事务内提交以提高效率 比较适合 用于单会话 导数据
    3. 高并发下,事务越短越好。
    wdlth
    wdlth
    Jul 22, 2017
    45
    LZ 的 MySQL 用的是 InnoDB 还是 MyISAM 存储引擎?
    billion
    billion
    OP
    Jul 22, 2017
    46
    @wdlth InnoDB
    billion
    billion
    OP
    Jul 22, 2017
    47
    @akira 第三条很重要。
    abcbuzhiming
    abcbuzhiming
    Jul 22, 2017
    48
    @wdlth MyISAM 没有事务,能开事务必然是 InnoDB
    wdlth
    wdlth
    Jul 22, 2017
    49
    @abcbuzhiming MariaDB 的 Aria 也能开事务,不过他没明确说是 MariaDB,那应该还是 InnoDB。
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   3481 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 64ms · UTC 04:19 · PVG 12:19 · LAX 20:19 · JFK 23:19
    ♥ Do have faith in what you're doing.