首页 新闻 会员 周边 捐助

b/s的并发事务控制

0
悬赏园豆:10 [已解决问题] 解决于 2015-04-07 22:36

简单三层,在业务层调用数据访问层的UploadModel(Entity)方法更新模型

UploadModel拼接一个形似update model (字段1,字段2........) values (参数1,参数2.....)

现在业务增多后经常会有两个请求同时访问,在执行UploadModel方法时获取的相同Entity,在执行完UploadModel后后一个执行的就把前一个执行结果覆盖

后来在在拼接的语句中加入了一个时间戳判断 where timestamp=@timestamp

这样做避免掉了前面提到的bug,但是却经常提示更新失败。

大家有没有更好的方法?

王者永乐的主页 王者永乐 | 初学一级 | 园豆:29
提问于:2014-10-13 10:38
< >
分享
最佳答案
0

两个人或是两个浏览器同时去更新一行数据,这个本身就是要尽可能避免的事情,

如果万一出现了,一定是要让其中一个人失败的。就好象两个人都想当局长,有三种决策

一是先申请的当局长,二是后申请的当局长,三是两个人先打一架,活下来的再当局长。

就算是公务员这么冗余的队伍里面,一个职位三个人干活,通常他们也会只有一个人在电脑前面,

一个去买菜,另外一个到隔壁办公室喝茶聊天。

所以现在问题来了,你为什么会经常出现两个进程(请求)更新同一个Entity的情况?

收获园豆:10
爱编程的大叔 | 高人七级 |园豆:30844 | 2014-10-13 10:48

你的意思是得用队列?

王者永乐 | 园豆:29 (初学一级) | 2014-10-13 10:56

@王者永乐: 我的意思是通常大部份情况是设计者的方案设计问题。

1、对于并发冲突,用TimeStamp是一个很好的方案,拼接SQL就不说了,如果在ORM中,

有timeStamp字段可以让最终的SQL少好多字符。

2、使用timeStamp可以发现并发冲突,甚至非并发冲突。

    用户甲打开 Entity 1320 (TimeStamp: 1)

    用户乙打开 Entity 1320 (TimeStamp: 1)

    过了两个小时

    用户甲保存 Entity 1320 (发现数据库中 TimeStamp为1,更新成功,TimeStamp变为2)

    过了一个小时

   用户乙保存 Entity 1320(发现数据库中TimeStamp为2,与打开时的TimeStamp不符)

这时候应该报警,在关于这种情况如何处理的论述中,通常有三个方案,你可以自行Google,我就不再当文抄公了。

3、解决方案本身是逻辑问题,非编程问题,不能通过编程解决,只能先考虑要采用或者让用户选择采用哪个方案,没有十全十美的方案,选择了方案后,编程其实就那么回事。(随便找个写代码的搞定就行了)

爱编程的大叔 | 园豆:30844 (高人七级) | 2014-10-13 11:06

@爱编程的大叔: 三个方案应该搜索什么关键词啊,我搜索了好一会没搜到。

王者永乐 | 园豆:29 (初学一级) | 2014-10-13 11:23

@王者永乐: 

1、先申请的当局长,后申请的放弃。

2、先申请的当了局长,后申请的将他踢掉,自己当局长。(这个对前者不公平)

3、先申请的当局长,后申请发贴公示,由群众或者领导决定,保留前者当局长,还是后者替掉前者当局长。

就这三种方案了。

爱编程的大叔 | 园豆:30844 (高人七级) | 2014-10-13 11:30

@爱编程的大叔: 

这种做法的结果就是肯定有一个放弃了。

还请大神指点怎样避免这种同时更新的情况存在吧,毕竟用户来你网站,隔三差五的提交失败也很不友好。

我现在把一些更新改成了update 表名 set  数值字段=数值字段+变量参数

这样是否能解决这个问题?

王者永乐 | 园豆:29 (初学一级) | 2014-10-13 11:33

@王者永乐: 所以我说问题是出在你的方案设计上。

比如数据库结构设计,

WEB页面代码设计(要防止重复提交)

你没有告诉我你的实际场景或是数据表设计,我也不知道你为啥会经常提交失败。

因为通常正常一点或是说好一点的设计,是要避免两个不同用户操作同一条数据的。

当然,如果是用户访问计数器这一类的,那是另外一回事,比如你用UserCount=UserCount+1,

那就不能用timeStamp。

我刚刚所说的原则是这些数据行是类似订单、出库单一类的数据。

不同的数据模型你得采用不同的更新逻辑。

原则上大部份的业务数据(90%以上吧)都是要避免对同一行进行两次不同的保存的。否则总是会有对

前者或是对后者的不公平情况发生,提交仲裁又太麻烦。

如果是确实需要对一个表一行进行非常频繁的Update,则通常一是要么检查TimeStamp,也就是用第二个方案,后保存的生效。但是编程者要注意一种情况,

比如 UserCount=2

甲: Update table set UserCount=3

乙: Update table set UserCount=3 (其实你希望是4)

象这类要一直累加的,就得用Update Table set UserCount=userCount+1,而不检查TimeStamp.

爱编程的大叔 | 园豆:30844 (高人七级) | 2014-10-13 11:42

@爱编程的大叔: 我这边情况是一个商品库存,被购买时需要减少购买数量  交易取消时需增加购买数量  用户又可以自己加或者减自己的库存。

避免两个不同用户操作同一行数据,我觉得可能性不大。

TimeStamp的情况就是会造成用户偶尔会看到提交失败的提示

我现在采取了您说的Update Table set UserCount=userCount+1

但是您也说了在累加的时候才用,而且维护其它繁琐,每个表都要手写一个dal

有没有其它更好的方法

王者永乐 | 园豆:29 (初学一级) | 2014-10-13 12:17

@王者永乐: 呵呵,你早说啊。别的不太懂,进销存我马马虎虎算懂一点。

对于商品库存,有两种设计方案,一是动态计算,二是静态数据。

现在你采用的是静态数据,这就带来一个问题,两个或多个不同的用户更新时的处理。

不过这事情说起来话就长了,需要知道更多的信息,比如用户量,数据量,

服务器性能、网络性能等等,综合考虑选择最优的方案。

你想简单的解决是不可能了,我只能说,这不是一件简单的事。

爱编程的大叔 | 园豆:30844 (高人七级) | 2014-10-13 12:24
其他回答(2)
0

并发更新方式有两种:

一种是悲观并发(pessimistic concurrency)。就是先锁定数据,再读取数据,一致后再更新,然后解锁。(可能需要在锁定、读取数据后先和本地数据比较,如果不一致先更新本地数据)。这样通过锁定保证了更新的数据是最新的版本。锁定可以通过数据库提供的行锁定(甚至是表锁定)功能,或者是根据自己的业务逻辑自己做一个锁定表来处理。

另一种是乐观并发(optimistic concurrency)。就是不需要用锁,而是使用版本戳,比如sql server的timestamp类型字段。这样就可以和本地的数据的版本进行比较,不一致则更新失败。失败后需要自己根据业务逻辑来处理,有三种处理方式,一种是server win,就是以数据库当前状态为准,本次更新失败。第二种是client win,就是忽略版本戳再次提交,覆盖掉之前的版本。第三种是merge,需要读最新数据,根据业务来和失败的提交合并,然后在更新数据。这里后两种处理方式在更新的时候还是可能会失败,可能需要根据业务来设计重试的次数,和最终失败的处理。

悲观并发的逻辑更容易写,但是由于锁定,性能会降低。乐观并发需要考虑的情况更多,但是也更灵活。现在一般更多的考虑采用乐观并发的方式。

像是更新库存的业务,我建议使用乐观并发(也就是你目前使用的办法),在失败处理时使用merge,就是失败的时候先读最新库存,然后根据业务加减,再次提交。这时仍然可能失败,要根据系统的并发程度来确定一个重试次数,比如3次,每次都是先读最新库存,然后根据业务加减,再次提交。如果失败超过了重试次数,就放弃这次操作,提示用户失败。

如果再严格一些,更新库存的操作是需要考虑幂等的。就是如果用户可以在UI上本应该一次提交的地方能够进行多次提交(比如网页上刷新提交页面),那么多次提交的结果应该和一次提交一致(也就是防止重复提交)。一般的方法是可提交的页面生成时分配一个唯一id,提交时会同时提交这个id,如果提交成功,这个id就失效了,再次使用这个id的提交就忽略。

winnow | 园豆:292 (菜鸟二级) | 2014-10-18 20:17
0

简单地设置一个版本号,如 version=1
update table set Name='wtf', version+=1 where id=11 and version=1

如果execute 返回的是0 那么就是更新失败。 返回1 就是更新成功,其实和楼上两位说的是一样的。

沉默的糕点 | 园豆:1786 (小虾三级) | 2015-01-13 22:33
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册