V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MySQL 5.5 Community Server
MySQL 5.6 Community Server
Percona Configuration Wizard
XtraBackup 搭建主从复制
Great Sites on MySQL
Percona
MySQL Performance Blog
Severalnines
推荐管理工具
Sequel Pro
phpMyAdmin
推荐书目
MySQL Cookbook
MySQL 相关项目
MariaDB
Drizzle
参考文档
http://mysql-python.sourceforge.net/MySQLdb.html
Weixiao0725
V2EX  ›  MySQL

MySql 事务解决并发问题

  •  1
     
  •   Weixiao0725 · 2016-07-06 18:49:04 +08:00 · 5684 次点击
    这是一个创建于 3068 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先描述一下场景: 假设我有一张表 orders(id, order_id, money, type), 每次我选择某一个订单的最后一笔进行消耗。例如,现在表里有一条记录(3, 777, 34.00, 1),我现在要消耗 2.00 ,先把原来的记录查出来并update成(3, 777, 34.00, 0), 然后insert一条新的记录(4, 777, 32.00, 1)。

    现在如果如果我的服务部署在多台机器上,就有可能两个并发连接同时取到同一条记录,然后在这条记录上消耗。比如,现在两个连接同时取到记录(3, 777, 34.00, 1),然后具体的消耗代码我放在事务中做,但是仍会得到结果记录为

    (3, 777, 34.00, 0)
    (4, 777, 32.00, 1)
    (5, 777, 32.00, 1)
    

    而如果正常消耗的话应该为

    (3, 777, 34.00, 0)
    (4, 777, 32.00, 0)
    (5, 777, 30.00, 1)
    

    我的业务代码使用 java 写的,必须先查上次最后的记录,然后具体的消耗的时候,即 update 和 insert 语句放到事务 里做,用的 read committed 隔离级别。

    给出一个 pseudocode in Java

    last = getInfoByOrderId(orderid, type);
    //...
    // 使用 last 先进行一些业务判断,如果不满足一些条件就直接报错返回了
    //...
    TransactionTemplate.execute(new TransactionCallback() {
        String sql = "select * from " + last.getTableName() + " where id = " + last.getId() + " for update";
        lastinfo = queryByIdForUpdate(sql);
        update(lastinfo);
        insertOneItem(consumeMoney);
            
    
    });
    
    

    业务场景有点复杂,望见谅:D ,对于这种并发场景,有没有好的处理方式?

    24 条回复    2016-07-11 01:25:52 +08:00
    billlee
        1
    billlee  
       2016-07-06 19:57:15 +08:00
    SELECT FOR UPDATE?
    Weixiao0725
        2
    Weixiao0725  
    OP
       2016-07-06 20:00:10 +08:00
    @billlee 是,进到事务里,我会先对这条记录加一个 X 锁。
    billlee
        3
    billlee  
       2016-07-06 20:03:05 +08:00
    @Weixiao0725 我好像没说清楚,你这个逻辑,在 getInfoByOrderId 这里取出最后一笔订单 last 的时候,就已经进入临界区了吧?这里就应该加锁了
    wy315700
        4
    wy315700  
       2016-07-06 20:52:16 +08:00
    可以由一个进程取数据,然后分发给其他进程进行处理
    wander2008
        5
    wander2008  
       2016-07-06 20:57:09 +08:00 via iPhone
    乐观锁吧
    codingadog
        6
    codingadog  
       2016-07-06 20:57:42 +08:00 via iPhone
    最近同遇到差不多这么个问题,水平不够还没想到怎么解决
    ipconfiger
        7
    ipconfiger  
       2016-07-06 21:00:24 +08:00
    有三个方法可以解决
    1, 提高事务隔离度, 强制串行执行
    2, 用分布式锁, 比如可以用 redis 来实现一个分布式锁
    3, 用一个队列来排队执行, 简单点就单对列, 复杂点可以多个队列跑, 但是需要保证每一个资源都只能在一个队列里出现
    brucefeng
        8
    brucefeng  
       2016-07-06 21:03:57 +08:00
    乐观锁比较好解决,而且不影响性能,用悲观锁太耗性能
    emacsistyzy
        9
    emacsistyzy  
       2016-07-06 21:04:36 +08:00   ❤️ 1
    其实这并不是 MySQL 并发的事, 而是你在业务上就应该协调好避免重复做这动作.
    在同一个 JVM 里, 可以用同步方式来控制.
    你这种跨 JVM 的方式, 可以使用分布式锁来协调.

    比如, 用方法参数的组合来确定一个分布式锁.

    至于分布式锁的解决方案, 可以用 redis 或者 zookeeper.

    希望可以帮到你哈.
    iyangyuan
        10
    iyangyuan  
       2016-07-06 21:05:17 +08:00 via iPhone
    分布式锁,锁记录 id
    pubby
        11
    pubby  
       2016-07-06 21:26:10 +08:00
    事务解决不了并发问题,你要的是一个分布式锁

    用 MySQL 就可以简单实现一个
    SELECT GET_LOCK('lockerName',<int timeout>)
    SELECT RELEASE_LOCK('lockerName')
    SELECT IS_FREE_LOCK('lockerName')
    SELECT IS_USED_LOCK('lockerName')

    查一下 MySQL 手册,自己封装一个 locker 类就行了。
    gamexg
        12
    gamexg  
       2016-07-06 21:31:40 +08:00
    @billlee +1

    last = getInfoByOrderId(orderid, type); 时就应该加锁了。如果不想这里加锁,那么需要在 lastinfo = queryByIdForUpdate(sql); 前面再次检查是否已经被其他进程修改了这一行 row 。

    现在出现故障的原因是 getInfoByOrderId 获得了最新记录,但是并没有锁定,这时候其他人是可以修改的。然后 queryByIdForUpdate 查询虽然有锁,但是这时候 row 已经被其他事务修改了,并且你没有判断是不是已经被修改了,所以结果不符合预期。解决办法是 queryByIdForUpdate 后再次检查。

    我有时候会用另一个玩法,既然确定一个用户永远之恩能够有一条可用余额记录,那么也可以用唯一索引来做。增加一个 is_terminated 字段, 0 表示本行有效,非零表示记录已被废弃,每次废弃行时将 is_terminated 设置为主键 id 。设置唯一索引,列是 userid+is_terminated 。
    可以保证代码处 BUG 也能保证单个用户永远只有一行可用记录。
    Weixiao0725
        13
    Weixiao0725  
    OP
       2016-07-06 21:49:36 +08:00
    @billlee 额,这里还没有,这里只是普通的 select 一次,然后根据查到的结果要进行一些业务的判断,如果不符合条件,就直接返回了,或者抛出异常了, 不会进入到下面的事务
    Weixiao0725
        14
    Weixiao0725  
    OP
       2016-07-06 21:59:42 +08:00
    @emacsistyzy 嗯,也正在考虑 zookeeper.
    Weixiao0725
        15
    Weixiao0725  
    OP
       2016-07-06 22:00:15 +08:00
    @pubby awesome! :)
    fengjianxinghun
        16
    fengjianxinghun  
       2016-07-06 22:05:17 +08:00 via iPhone
    @pubby +1
    pubby
        17
    pubby  
       2016-07-06 22:12:22 +08:00
    @Weixiao0725 如果有主从数据库,记得大家都要连到同一台 mysql 上上锁

    一般我对一个订单的所有操作都用同一个锁名锁定

    比如
    try{

    $locker = new Locker("ORDER_<OrderID>");
    $locker->lock(30); // timeout 30s

    $con->beginTransaction();

    ... select

    ... maybe Exception ..


    ... update

    $con->commit();

    $locker->unlock();


    }catch(Exception $e){

    if($con->isInTransaction()) $con->forceRollback();
    $locker->unlock();
    }
    fengjianxinghun
        18
    fengjianxinghun  
       2016-07-06 22:42:23 +08:00 via iPhone
    如果是 redis 分布锁 redis-cluster 用 redlock
    单机简单 setnx
    fengjianxinghun
        19
    fengjianxinghun  
       2016-07-06 22:48:21 +08:00 via iPhone
    @pubby A 拿到锁, master 挂了,从切过来 B 也拿到了锁?
    pubby
        20
    pubby  
       2016-07-06 22:57:22 +08:00
    @fengjianxinghun
    B 如果在 master 挂之前在等锁 ,那 master 挂了 B 也能发现错误
    B 如果在 master 挂之后向 slave 拿锁,那 A 反正事务完成不了,不影响 B
    icegreen
        21
    icegreen  
       2016-07-07 00:18:34 +08:00
    1. 加锁,上面的都提到了.
    2. 乐观锁, update 语句加一个 where 条件 type=1; 每次更新的时候, 判断 update 语句影响的条数, 如果更新了 0 条, 抛异常, 回滚事务; 如果更新一条记录, 成功;
    phttc
        22
    phttc  
       2016-07-07 08:46:51 +08:00
    好巧,,昨天我也遇到了类似问题。。
    我的是这样的: UPDATE db_table SET value=value+1 WHERE id=1;
    有没有方法在一句 sql 里把更新后的 value 值给 select 出来?考虑并发情况,要求取出的 value 不能被污染。。
    最后采用的方法是先 select 出原始值。。然后 update 的时候加上判断,,根据返回影响的条数判断是不是被污染。
    Weixiao0725
        23
    Weixiao0725  
    OP
       2016-07-07 09:27:29 +08:00
    @phttc 其实,这个问题还是用分布式锁解决比较合适。我准备先用楼上说的 get_lock 试一下,不行的话就直接上 zookeeper 了
    ihuotui
        24
    ihuotui  
       2016-07-11 01:25:52 +08:00 via Android
    系统设计问题,同一用户的操作一定要保持在一个服务器上,要不然要设计复杂的分布式事物和同步
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2679 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 10:29 · PVG 18:29 · LAX 02:29 · JFK 05:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.