MySql 事务解决并发问题

2016-07-06 18:49:04 +08:00
 Weixiao0725

先描述一下场景: 假设我有一张表 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 ,对于这种并发场景,有没有好的处理方式?

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

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

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

希望可以帮到你哈.
iyangyuan
2016-07-06 21:05:17 +08:00
分布式锁,锁记录 id
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
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
2016-07-06 21:49:36 +08:00
@billlee 额,这里还没有,这里只是普通的 select 一次,然后根据查到的结果要进行一些业务的判断,如果不符合条件,就直接返回了,或者抛出异常了, 不会进入到下面的事务
Weixiao0725
2016-07-06 21:59:42 +08:00
@emacsistyzy 嗯,也正在考虑 zookeeper.
Weixiao0725
2016-07-06 22:00:15 +08:00
@pubby awesome! :)
fengjianxinghun
2016-07-06 22:05:17 +08:00
@pubby +1
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
2016-07-06 22:42:23 +08:00
如果是 redis 分布锁 redis-cluster 用 redlock
单机简单 setnx
fengjianxinghun
2016-07-06 22:48:21 +08:00
@pubby A 拿到锁, master 挂了,从切过来 B 也拿到了锁?
pubby
2016-07-06 22:57:22 +08:00
@fengjianxinghun
B 如果在 master 挂之前在等锁 ,那 master 挂了 B 也能发现错误
B 如果在 master 挂之后向 slave 拿锁,那 A 反正事务完成不了,不影响 B

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/290706

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX