先描述一下场景: 假设我有一张表 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 ,对于这种并发场景,有没有好的处理方式?
1
billlee 2016-07-06 19:57:15 +08:00
SELECT FOR UPDATE?
|
2
Weixiao0725 OP @billlee 是,进到事务里,我会先对这条记录加一个 X 锁。
|
3
billlee 2016-07-06 20:03:05 +08:00
@Weixiao0725 我好像没说清楚,你这个逻辑,在 getInfoByOrderId 这里取出最后一笔订单 last 的时候,就已经进入临界区了吧?这里就应该加锁了
|
4
wy315700 2016-07-06 20:52:16 +08:00
可以由一个进程取数据,然后分发给其他进程进行处理
|
5
wander2008 2016-07-06 20:57:09 +08:00 via iPhone
乐观锁吧
|
6
codingadog 2016-07-06 20:57:42 +08:00 via iPhone
最近同遇到差不多这么个问题,水平不够还没想到怎么解决
|
7
ipconfiger 2016-07-06 21:00:24 +08:00
有三个方法可以解决
1, 提高事务隔离度, 强制串行执行 2, 用分布式锁, 比如可以用 redis 来实现一个分布式锁 3, 用一个队列来排队执行, 简单点就单对列, 复杂点可以多个队列跑, 但是需要保证每一个资源都只能在一个队列里出现 |
8
brucefeng 2016-07-06 21:03:57 +08:00
乐观锁比较好解决,而且不影响性能,用悲观锁太耗性能
|
9
emacsistyzy 2016-07-06 21:04:36 +08:00 1
其实这并不是 MySQL 并发的事, 而是你在业务上就应该协调好避免重复做这动作.
在同一个 JVM 里, 可以用同步方式来控制. 你这种跨 JVM 的方式, 可以使用分布式锁来协调. 比如, 用方法参数的组合来确定一个分布式锁. 至于分布式锁的解决方案, 可以用 redis 或者 zookeeper. 希望可以帮到你哈. |
10
iyangyuan 2016-07-06 21:05:17 +08:00 via iPhone
分布式锁,锁记录 id
|
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 类就行了。 |
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 也能保证单个用户永远只有一行可用记录。 |
13
Weixiao0725 OP @billlee 额,这里还没有,这里只是普通的 select 一次,然后根据查到的结果要进行一些业务的判断,如果不符合条件,就直接返回了,或者抛出异常了, 不会进入到下面的事务
|
14
Weixiao0725 OP @emacsistyzy 嗯,也正在考虑 zookeeper.
|
15
Weixiao0725 OP @pubby awesome! :)
|
16
fengjianxinghun 2016-07-06 22:05:17 +08:00 via iPhone
@pubby +1
|
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(); } |
18
fengjianxinghun 2016-07-06 22:42:23 +08:00 via iPhone
如果是 redis 分布锁 redis-cluster 用 redlock
单机简单 setnx |
19
fengjianxinghun 2016-07-06 22:48:21 +08:00 via iPhone
@pubby A 拿到锁, master 挂了,从切过来 B 也拿到了锁?
|
20
pubby 2016-07-06 22:57:22 +08:00
@fengjianxinghun
B 如果在 master 挂之前在等锁 ,那 master 挂了 B 也能发现错误 B 如果在 master 挂之后向 slave 拿锁,那 A 反正事务完成不了,不影响 B |
21
icegreen 2016-07-07 00:18:34 +08:00
1. 加锁,上面的都提到了.
2. 乐观锁, update 语句加一个 where 条件 type=1; 每次更新的时候, 判断 update 语句影响的条数, 如果更新了 0 条, 抛异常, 回滚事务; 如果更新一条记录, 成功; |
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 的时候加上判断,,根据返回影响的条数判断是不是被污染。 |
23
Weixiao0725 OP @phttc 其实,这个问题还是用分布式锁解决比较合适。我准备先用楼上说的 get_lock 试一下,不行的话就直接上 zookeeper 了
|
24
ihuotui 2016-07-11 01:25:52 +08:00 via Android
系统设计问题,同一用户的操作一定要保持在一个服务器上,要不然要设计复杂的分布式事物和同步
|