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 · 3619 次点击
    这是一个创建于 2683 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一开始开了 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 我怀疑应该是代码的问题。
    alex8
        43
    alex8  
       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   ·   实用小工具   ·   2871 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 12:30 · PVG 20:30 · LAX 04:30 · JFK 07:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.