细思极恐! Web项目中你们是怎么实现这种“业务锁”的?

litescript 发布于 2015/08/22 10:15
阅读 1K+
收藏 0

以Java语言为例,我说的"锁"不是语言层级的,而是业务层的一种概念

举个例子, 要实现扣除某个用户的余额,于是很自然的有了下面的方法

//扣除ID为userid的用户money额度
public void dosomething(Integer userid, Integer money){
    
  User user = dao.findByid(userid);
  user.setMoney(user.getMoney() - money);
  dao.update(user);
  
}
这个方法很简单,似乎是没有问题的。但是,因为Web系统天然的多线程特性。所以这个方法很可能会同时被多个线程调用!而这时就可能出现调用2次方法但是只扣掉1次钱的情况了!

解决方法当然是加锁,但这个锁该加到哪里呢?因为要和userid关联,所以总不能把整个方法加锁吧!

加载中
1
eechen
eechen
获取数据时拿到版本号和余额,写入时比对版本号,相同则插入,并把版本号加1.
SELECT balance,version FROM user WHERE id=1 AND balance>10;
UPDATE user SET balance=balance-10,version=last_version+1 WHERE id=1 AND version=last_version;
注意到UPDATE里的last_version为SELECT获取的本次读写的版本号.
不需要数据库事务的支持,SELECT操作和UPDATE操作的时间跨度再大也没有问题.

上述版本号的方法借鉴了Memcached的CAS(Check And Set)冲突检测机制,这是一个乐观锁,能保证高并发下的数据安全.

版本号version超过字段能容纳的最大值比如int(10)也就是4294967295的处理方法:
version默认值为0,flag默认值1.
新增一个flag字段,1表示加1,-1表示减1.
if( $flag == 1 && $version != 4294967295 ) {
    // UPDATE user SET balance=balance-10,version=last_version+1 WHERE id=1 AND version=last_version AND flag=1;
} else if( $flag == 1 && $version ==4294967295 ) {
    // UPDATE user SET balance=balance-10,version=last_version-1,flag=-1 WHERE id=1 AND version=last_version AND flag=1;
} else if( $flag == -1 && $version !=0 ) {
    // UPDATE user SET balance=balance-10,version=last_version-1 WHERE id=1 AND version=last_version AND flag=-1;
} else if( $flag == -1 && $version ==0 ) {
    // UPDATE user SET balance=balance-10,version=last_version+1,flag=1 WHERE id=1 AND version=last_version AND flag=-1;

}

WHERE子句中的条件flag=1是为了避免下面这种情况,保证数据安全:
假设一个请求读到的version为4294967294,但version已经被其他请求更新为4294967295并回退到4294967294,
如果没有WHERE flag=1的条件限制,那么该请求会认为数据并没有更改过,从而执行更新,导致数据不一致.

zhhuang007
zhhuang007
回复 @eechen : UPDATE user SET balance=balance-10,version=last_version+1 WHERE id=1 AND version=last_version; 当where条件没满足的时候,balance就没有-10,这个怎么处理?
ihuotui
ihuotui
回复 @eechen : 直接让version增加(int的最小值),然后version又变会int的最大值,下次查询也会得到这个版本号,重新根据版本号比较。版本号的意义是对比自己的版本号和 获取的版本是否一致,才进行操作。不一致就算自己的信息过期了。
eechen
eechen
回复 @ihuotui : 还有个问题就是版本号version超过字段能容纳的最大值比如int(10)也就是4294967295的时候该怎么处理,我补充了贴子,你看看对不对。
ihuotui
ihuotui
我也觉得是这样,题主已经像互联网的高并发情况了。
0
乾坤摄
乾坤摄
.....直接更新数据库,不用先把数值取出计算在更新》。。
litescript
litescript
变相用了数据库的锁,是一种办法,但是对于复杂的业务需要用java计算的,就不适用了
0
gfzlenovo
gfzlenovo
先记流水账单,后结算
大嘴吃鸡腿
大嘴吃鸡腿
就是这样,这样也方便如果数据出错的恢复
0
如比如比
如比如比
建立一个叫锁的表,在要更新重要信息时,先取锁,取得锁后获取最新数据,更新数据库,解除锁。
小木头的冬天
小木头的冬天
目前就这么做的
0
原来如此
原来如此

使用以下sql语句

1、BEGIN   开始事务

2、SELECT * FROM user WHERE id=1 FOR UPDATE  使用FOR UPDATE 锁表,注意,where后面必须是主键或者是唯一性索引,否则不是行锁,而是整表锁

3、UPDATE user SET...

4、COMMIT   提交事务

这个通常叫悲观锁,楼上还有一个人介绍了乐观锁,如果读远远多于写,用乐观锁,它是假设数据一般情况下不会发生冲突,如果写操作很频繁,并且冲突的比例很高,用悲观锁

0
负心杏

个人认为直接用sql语句,锁什么的交给数据库。

如:update xxx set money=money-? where id=1;

因为是修改表记录,会触发数据库的锁。所以同一时间只有一条sql语句执行,避免了程序读取,程序运算,程序更新数据库的流程的锁的时间消耗。

0
jim19770812
jim19770812
尽量不要使用同步关键字,可以考虑放到redis缓存里,key就是用户id+一个固定的uuid,专门用来存锁定的金额,这样并发好一点,不会出现扣两次钱的问题,速度还快
0
莫铭
莫铭

像这种情况一般是方法级的加锁,或者用事务来做。 

(1)方法加锁:

public synchronized void dosomething(Integer userid,Integer money){
  ……
}

(2)数据库写个存储过程,存储过程主要是事务减的操作,程序调用存储过程即可。




0
素时踏花
素时踏花

楼上各位总结得不错,解决方案无非就是采用乐观锁和悲观锁,也算是受益匪浅。

Web 应用就是个实时并发的环境,对于像金额扣减、物料数目增减都需要采用“排队”的方式来解决,因为后一个请求的计算基础值必须是紧邻的上一个请求的结果值。为达此目的,乐观锁模式仰赖一个“时间戳”列,如果该列被别人变更了,那就说明当前请求的基础值已经过时,这时就抛异常提示用户重新操作吧;悲观锁模式要严谨些,从一开始就基于一个“写操作”拿到行级排他锁,迫使后续的操作全部进入“排队”状态,一个个的来,有条不紊。

不管是哪一种模式,隔离级别(Isolation Level)保持默认的“已提交读(read committed)”就可以。另外提醒,不要轻易把隔离级别提到“可持续读”,这个级别很容易造成死锁,因为此级别申请的共享锁会持续事务的全过程。

在偏 ERP 和 财务 这些方面的应用里建议统一用悲观锁模式,别偷懒省事,统一的编程模型从长远来看更省气力。

返回顶部
顶部