技术分享贴:充值系统购物系统金融系统,如果保证多线程并发下,金额不出错?

yzy8788 2017-09-17 08:48:00
通常情况下,大家最容易想到的解决办法就是在程序中加锁,本文分享一个比加锁更为友好的方式

场景:数据表member中有列money,拿两个线程举例,分别叫线程1和线程2,两个线程同时从member表中get到一个对象,这个对象中的money=20,线程1握着这个对象准备给money列+100元,线程2握着这个对象准备给money列-50元,线程1先干完活update到数据库后,member表的money变成20+100=120,接着线程2也干完活了,update到数据库后,member表的money变成20-50=-30,最终member表中的money变成-30,这显然是不正确的,正确的money值应该为20+100-50=70

如何来解决这个问题呢?上面只是举例两个线程,现实中可能会有更多的线程、更复杂的场景,会把money更新的面目全非

解决思想:第一个干完活的线程update后,其他线程手里攥的对象全部过期失效

解决办法:可以给member表新增一个列,列名就叫rowver吧,类型是timestamp,这种类型会在每次update之后,自动变成一个新值,新增了这一列后每个线程get到的对象都带有一个属性rowver,注意timestamp类型在程序中对应的类型是byte[],也就是字节数值

--新增列
alter table member add rowver timestamp not null

然后update语句就可以修改成

--改造update语句
update member set money=money+@money where userid=@userid and rowver=@rowver

这种机制还是行级别的,是不是很棒!比程序加锁的方式温柔多了

使用新机制后,再还原一下上面的场景:线程1 update后列rowver值变了,线程2 磨磨唧唧慢慢悠悠再来update的时候,发现影响的行数为0,为啥?因为线程2手里攥着的对象的rowver值已经过期了
...全文
292 12 打赏 收藏 转发到动态 举报
写回复
用AI写文章
12 条回复
切换为时间正序
请发表友善的回复…
发表回复
繁花尽流年 2017-09-20
  • 打赏
  • 举报
回复
引用 11 楼 shadowpj 的回复:
以前遇到过,还是SQL2000版本时!多个客户端需要更新票号字段(老版本就是流水号,会同时取最后记录产生重复票号。现在新程序改版了,票号产生方式直接工号加流水就解决了。。。)。避免重复就采用的信号灯模式,既可保证并发数据的唯一性,又可以让所有用户都更新到数据。我说说看大家觉得怎么样。流程如下: 配置表增加一字段比如叫“MAC”,默认值为“无”。流程图如下: 这里需要注意终端得到信号灯控制权后突然断网、死机等锁死情况。可以在服务器上挂一个小程序检测这个MAC值。每隔一段时间(30秒)去检测mac字段,如果某个终端占用信号灯超过30秒,说明此终端死锁,死锁监控模块立即强行将mac字段更新为“无”。 如果系统处理记录速度很快,可以缩短死锁监控模块检测时间间隔。
其实就是一般门店每个客户端的定一个唯一标识号,组合到流水号里。再细点的就是定位到POS机标识。
shadowpj 2017-09-20
  • 打赏
  • 举报
回复
以前遇到过,还是SQL2000版本时!多个客户端需要更新票号字段(老版本就是流水号,会同时取最后记录产生重复票号。现在新程序改版了,票号产生方式直接工号加流水就解决了。。。)。避免重复就采用的信号灯模式,既可保证并发数据的唯一性,又可以让所有用户都更新到数据。我说说看大家觉得怎么样。流程如下:
配置表增加一字段比如叫“MAC”,默认值为“无”。流程图如下:

这里需要注意终端得到信号灯控制权后突然断网、死机等锁死情况。可以在服务器上挂一个小程序检测这个MAC值。每隔一段时间(30秒)去检测mac字段,如果某个终端占用信号灯超过30秒,说明此终端死锁,死锁监控模块立即强行将mac字段更新为“无”。
如果系统处理记录速度很快,可以缩短死锁监控模块检测时间间隔。
吉普赛的歌 2017-09-18
  • 打赏
  • 举报
回复
这种方式绝大多数情况还是可以接受的。 当然,一条记录同时允许几十成百上千的人来改, 一是可能设计有问题 二是类似12306售票,这种谁也没什么很好的办法来让每个人都满意,只能业务逻辑上去规避
唐诗三百首 2017-09-18
  • 打赏
  • 举报
回复
学习了, 感谢分享.
繁花尽流年 2017-09-18
  • 打赏
  • 举报
回复
timestamp不可逆,即使你希望达到他的效果,我更建议,自己语句里去update,而不要借用timestamp。 因为你根本无法区分timestamp刷新的原因是你所需要的操作造成的。唯一能知道的就是这玩意又变了。
顺势而为1 2017-09-18
  • 打赏
  • 举报
回复
学习了, 谢谢楼主
二月十六 2017-09-17
  • 打赏
  • 举报
回复
这种方式线程2就没办法实现它想操作的数据了。
听雨停了 2017-09-17
  • 打赏
  • 举报
回复
感谢分享
yzy8788 2017-09-17
  • 打赏
  • 举报
回复
明白了,谢谢补充
OwenZeng_DBA 2017-09-17
  • 打赏
  • 举报
回复
引用 楼主 yzy8788 的回复:
通常情况下,大家最容易想到的解决办法就是在程序中加锁,本文分享一个比加锁更为友好的方式 场景:数据表member中有列money,拿两个线程举例,分别叫线程1和线程2,两个线程同时从member表中get到一个对象,这个对象中的money=20,线程1握着这个对象准备给money列+100元,线程2握着这个对象准备给money列-50元,线程1先干完活update到数据库后,member表的money变成20+100=120,接着线程2也干完活了,update到数据库后,member表的money变成20-50=-30,最终member表中的money变成-30,这显然是不正确的,正确的money值应该为20+100-50=70 如何来解决这个问题呢?上面只是举例两个线程,现实中可能会有更多的线程、更复杂的场景,会把money更新的面目全非 解决思想:第一个干完活的线程update后,其他线程手里攥的对象全部过期失效 解决办法:可以给member表新增一个列,列名就叫rowver吧,类型是timestamp,这种类型会在每次update之后,自动变成一个新值,新增了这一列后每个线程get到的对象都带有一个属性rowver,注意timestamp类型在程序中对应的类型是byte[],也就是字节数值

--新增列
alter table member add rowver timestamp not null
然后update语句就可以修改成

--改造update语句
	update member set money=money+@money where userid=@userid and rowver=@rowver
这种机制还是行级别的,是不是很棒!比程序加锁的方式温柔多了 使用新机制后,再还原一下上面的场景:线程1 update后列rowver值变了,线程2 磨磨唧唧慢慢悠悠再来update的时候,发现影响的行数为0,为啥?因为线程2手里攥着的对象的rowver值已经过期了
很不错,谢谢分享,
二月十六 2017-09-17
  • 打赏
  • 举报
回复
引用 4 楼 yzy8788 的回复:
[quote=引用 3 楼 sinat_28984567 的回复:] 这种方式线程2就没办法实现它想操作的数据了。
版主有没有想过,这比充当死锁牺牲品好多了吧?死锁住了,消耗性能不说,还卡的要死,浪费时间 线程2操作失败后,可以友好的提示给用户“抱歉!保存失败,稍后重试”,或者程序自动重试一次,具体如何控制 这是程序端应该考虑的问题[/quote] 1、死锁不死锁,看代码怎么写,如果写的合理完全可以避免死锁。 2、这种“友好”在产品和运营看起来一点也不友好,如果这种情况比较少,他们或许还能接受,如果频繁出现这个友好提示,他们只会觉得程序出问题了。
yzy8788 2017-09-17
  • 打赏
  • 举报
回复
引用 3 楼 sinat_28984567 的回复:
这种方式线程2就没办法实现它想操作的数据了。
版主有没有想过,这比充当死锁牺牲品好多了吧?死锁住了,消耗性能不说,还卡的要死,浪费时间 线程2操作失败后,可以友好的提示给用户“抱歉!保存失败,稍后重试”,或者程序自动重试一次,具体如何控制 这是程序端应该考虑的问题

22,209

社区成员

发帖
与我相关
我的任务
社区描述
MS-SQL Server 疑难问题
社区管理员
  • 疑难问题社区
  • 尘觉
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧