概述

上一篇文章中使用ssm+mysql实现,存在并发超发问题,这里我们使用悲观锁的方式来解决这个逻辑错误,并验证数据一致性和性能状况。


超发问题分析

针对这个案例,用户抢到红包后,红包总量应-1,当多个用户同时抢红包,此时多个线程同时读得库存为n,相应的逻辑执行后,最后将均执update T_RED_PACKET set stock = stock - 1 where id = #{id} ,很明显这是错误的。


使用数据库锁的解决方案

使用悲观锁(排它锁 for update)

  1. 线程1在查询红包数时使用排他锁 select id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount, stock, version, note from T_RED_PACKET where id = #{id} for update

  2. 然后进行后续的操作(redPacketDao.decreaseRedPacket 和 userRedPacketDao.grapRedPacket),更新红包数量,最后提交事务。

  3. 线程2在查询红包数时,如果线程1还未释放排他锁,它将等待。

  4. 线程3同线程2,依次类推。


使用乐观锁(依靠表的设计和代码)

  1. 在红包表添加version版本字段或者timestamp时间戳字段,这里我们使用version

  2. 线程1查询后,执行更新变成了update T_RED_PACKET set stock = stock - 1, version = version + 1 where id = #{id} and version = #{version}

这样,保证了修改的数据是和它查询出来的数据是一致的,而其他线程并未进行修改。当然,如果更新失败,表示在更新操作之前有其他线程已经更新了该红包数,那么就可以尝试重入机制来保证更新成功。


总结

1. 悲观锁使用了排他锁,当程序独占锁时,其他程序就连查询都是不允许的,导致吞吐较低。如果在查询较多的情况下,可使用乐观锁。

2. 乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入较频繁,对吞吐要求不高,可使用悲观锁。


悲观锁(抽象的描述,不真实存在这个锁)

悲观锁是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,所以悲观锁需要耗费较多的时间。另悲观锁是由数据库自己实现了的,使用的时候,直接调用数据库的相关语句即可。

由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

数据库的增删改操作默认都会加排他锁,而查询不会加任何锁。


共享锁(S锁)

共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。

对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。

语法:

select * from table lock in share mode ;

排他锁(X锁)

排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。

与共享锁类型,在需要执行的语句后面加上for update就可以了。

语法:

select * from table for update

代码改造

分析

为了不影响上个版本,我们新加个接口方法和Mapper映射。 因为悲观锁是数据库提供的功能,所以仅仅在Dao层修改Sql,Service层无需新增新的接口,只需要切换下调用的Dao层的方法即可。


RedPacketDao新增接口方法

/**  * 获取红包信息. 悲观锁的实现方式  *   * @param id  *            --红包id  * @return 红包具体信息  */ public RedPacket getRedPacketForUpdate(Long id);

RedPacket.xml配置映射文件

<!-- 查询红包具体信息  悲观锁的实现方式for update --> <select id="getRedPacketForUpdate" parameterType="long"   resultType="com.artisan.redpacket.pojo.RedPacket">   select      id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount, stock, version, note   from      T_RED_PACKET    where id = #{id} for update </select>

悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新。

在 SQL 中加入的 for update 语句,意味着将持有对数据库记录的行更新锁(因为这里使用主键查询,所以只会对行加锁。如果使用的是非主键查询,要考虑是否对全表加锁的问题,加锁后可能引发其他查询的阻塞〉,那就意味着在高并发的场景下 , 当一条事务持有了这个更新锁才能往下操作,其他的线程如果要更新这条记录,都需要等待,这样就不会出现超发现象引发的数据一致性问题了。


Service层调用新的Dao方法


还原数据,部署测试

将T_RED_PACKET和T_USER_RED_PACKET中的数据还原为初始数据后,启动应用,通过FireFox 访问http://localhost:8080/ssm_redpacket/grap.jsp


统计报告

一致性数据统计:

SELECT a.id, a.amount, a.stockFROM T_RED_PACKET aWHERE a.id = 1UNION ALL SELECT   max(b.user_id),   sum(b.amount),   count(*) FROM   T_USER_RED_PACKET b WHERE   b.red_packet_id = 1;

这里已经解决了超发的问题,所以结果是正确的,最起码逻辑是正确的了。除了结果正确,我们还需要考虑性能问题,统计来看下。

性能数据统计:

SELECT (   UNIX_TIMESTAMP(max(a.grab_time)) - UNIX_TIMESTAMP(min(a.grab_time))  )  AS lastTimeFROM T_USER_RED_PACKET a;


注意事项

不使用悲观锁时,2万个红包190秒【主机配置很低】抢完(但存在超发现象),现在是275秒。 目前只是对数据库加了一个锁,当加的锁比较多的时候,数据库的性能还会持续下降,所以要区分不同的业务场景,慎重使用。


悲观锁导致性能下降的原因探究

对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时, CPU 就会将这些得不到资源的线程挂起,挂起的线程也会消耗 CPU 的资源尤其是在高并发的请求中。

只能有一个事务占据资源,其他事务被挂起等待持有资源的事务提交并释放资源。当此时就进入了线程 2 , 线程 3……线程n,开始抢夺资源的步骤了,这里假设线程 3 抢到资源。

一旦线程1 提交了事务,那么锁就会被释放,这个时候被挂起的线程就会开始竞争红包资源,那么竞争到的线程就会被 CPU 恢复到运行状态,继续运行。

于是频繁挂起,等待持有锁线程释放资源, 一旦释放资源后,就开始抢夺,恢复线程,直至所有红包资源抢完。

在高并发的过程中,使用悲观锁就会造成大量的线程被挂起和恢复,这将十分消耗资源,这就是为什么使用悲观锁性能不佳的原因。

有些时候,我们也会把悲观锁称为独占锁,毕竟只有一个线程可以独占这个资源,或者称为阻塞锁,因为它会造成其他线程的阻塞。无论如何它都会造成并发能力的下降,从而导致 CPU频繁切换线程上下文,造成性能低下。

为了克服这个问题,提高并发的能力,避免大量线程因为阻塞导致 CPU 进行大量的上下文切换,目前比较普遍的是乐观锁机制。


代码

https://github.com/yangshangwei/ssm_redpacket

扩展阅读

抢红包案例分析以及代码实现

缓存在高并发场景下的常见问题

Redis 分布式锁:乐观锁的实现,以秒杀系统为例

来源:https://blog.csdn.net/yangshangwei/article/details/82980659

文章来源网络,版权归作者本人所有,如侵犯到原作者权益,请与我们联系删除

抢红包案例分析以及代码实现(二)相关推荐

  1. 抢红包案例分析以及代码实现(三) 侵立删

    转自:https://mp.weixin.qq.com/s/Pp-nCYrzXXXfLcFFS_ttWg 前文回顾 抢红包案例分析以及代码实现(一) 抢红包案例分析以及代码实现(二) 接下来我们使用乐 ...

  2. DL之Keras:基于Keras框架建立模型实现【预测】功能的简介、设计思路、案例分析、代码实现之详细攻略(经典,建议收藏)

    DL之Keras:基于Keras框架建立模型实现[预测]功能的简介.设计思路.案例分析.代码实现之详细攻略(经典,建议收藏) 目录 Keras框架使用分析 Keras框架设计思路 案例分析 代码实现 ...

  3. html中放大镜案列,Canvas实现放大镜效果完整案例分析(附代码)

    本文主要记录 canvas 在图像.文字处理.离屏技术和放大镜特效的实现过程中使用到的api.先看下效果吧: 一张模糊的图片: 鼠标点击任意位置,产生放大效果: 哇塞~ 一个帅哥,哈哈哈哈~ 1.放大 ...

  4. html放大镜原理,Canvas实现放大镜效果完整案例分析(附代码)

    本文主要记录 canvas 在图像.文字处理.离屏技术和放大镜特效的实现过程中使用到的API.先看下效果吧: 一张模糊的图片: 鼠标点击任意位置,产生放大效果: 哇塞~ 一个帅哥,哈哈哈哈~ 1.放大 ...

  5. 泰坦尼克号python数据分析统计服_Python-数据可视化案例分析之泰坦尼克号(二)...

    在第一节"Python-数据清洗与分析案例之泰坦尼克号(一)"网址:https://www.lixdx.cn/archives/93 中进行了数据清洗与缺失值填充,接下来进行数据可 ...

  6. Hadoop-HBASE案例分析-Hadoop学习笔记二

    之前有幸在MOOC学院抽中小象学院hadoop体验课.  这是小象学院hadoop2.X概述第八章的笔记  主要介绍HBase,一个分布式数据库的应用案例. 案例概况: 1)时间序列数据库(OpenT ...

  7. 建造者模式——案例分析与代码演示

    1.概述 建造者模式多用在对象构成比较复杂的场景中,比如汽车.电脑等包含的组件数量和种类很多很大的情形下.建造者(Builder)模式的定义如下,把一个复杂对象的构造与它的装配分离,使同样的构造过程可 ...

  8. 案例分析 | 无代码助力国企数字化转型破旧立新

    数字经济已成为国策,国企数字化转型更是排头兵,正成为数字化转型标杆.企业数字化转型是用信息技术全面重塑企业经营管理模式,是企业发展模式的变革与创新,是迈向数字经济时代的必然选择. 2022年9月底,国 ...

  9. android 类似金山词霸 每日一句源代码 csdn,个人作业2——英语学习APP案例分析(示例代码)...

    第一部分 调研, 评测 软件:微软必应词典(Android客户端) 版本:5.5.2 第一次上手体验: 整体界面上还算简洁,功能也算完全,但是并没有什么特别吸引我的地方.就我个人而言,如果不是这次作业 ...

最新文章

  1. Cocos Creator 键盘监听事件
  2. 【设计模式】装饰器模式类图和代码
  3. 春运首日山东烟台海上安全巡航
  4. Python——文件操作
  5. 向iGoogle中添加Google日历及其他小工具
  6. win7优化设置_Win10系统优化软件,这是我用的最舒服的一款软件了!
  7. C语言的关键字 详解
  8. Java零基础系列001——第一个程序
  9. vue获取接口数据_c#中HttpWebRequest调用接口获取数据
  10. Tomcat系列(4)——Tomcat 组件及架构详细部分
  11. ubuntu:磁盘清理
  12. 中国期货交易技术的逆袭之路
  13. 操作系统实验报告 lab6
  14. [索引汇总帖] 【eoeAndroid社区索引】Cocos2d-x部分汇总 [转贴]
  15. python下载互联网上的的图片
  16. 【南方者】【考证】【软考】【系统规划与管理师】论文万能模板
  17. 倍福--控制雷赛步进电机
  18. 从头学android_activity之间的切换_姻缘测算器
  19. 在AID Learning中用IPad或电脑连接手机
  20. 前端浏览器常见兼容性问题及解决方案

热门文章

  1. 纯c++读取与显示BMP图片
  2. 美丽的诗句 撩妹首选哦!
  3. [css选择器] 后代选择器
  4. 小学计算机室培训心得,小学计算机培训心得体会范文
  5. 【unity 保卫星城】--- 开发笔记07(追踪导弹武器)
  6. MEM/MBA英语基础(06)复合句-名词性从句
  7. 苹果电脑怎么打开计算机管理,mac开机启动管理怎么设置_mac如何设置开机启动管理-win7之家...
  8. ADO简介(未完成)
  9. 资源收藏:扁平化风格的图标
  10. 2022年学C++开发好比49年入国军,没什么公司在用C++了?