乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,它的设计里面由于不阻塞其他线程,所以并不会引发线程频繁挂起和恢复,这样便能够提高并发能力,所以也有人把它称为非阻塞锁,那么它的机制是怎么样的呢?

乐观锁使用的是 CAS 原理,所以我们先来讨论 CAS 原理的内容。

CAS 原理概述

在 CAS 原理中,对于多个线程共同的资源,先保存一个旧值(Old Value),比如进入线程后,查询当前存量为 100 个红包,那么先把旧值保存为 100,然后经过一定的逻辑处理。

当需要扣减红包的时候,先比较数据库当前的值和旧值是否一致,如果一致则进行扣减红包的操作,否则就认为它已经被其他线程修改过了,不再进行操作,CAS 原理流程如图 1 所示。

图 1  CAS原理

CAS 原理并不排斥并发,也不独占资源,只是在线程开始阶段就读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较,即比较各个线程当前共享的数据是否和旧值保持一致。

如果一致,就开始更新数据;如果不一致,则认为该数据已经被其他线程修改了,那么就不再更新数据,可以考虑重试或者放弃。有时候可以重试,这样就是一个可重入锁,但是 CAS 原理会有一个问题,那就是 ABA 问题,下面先来讨论一下 ABA 问题。

ABA 问题

对于乐观锁而言,我们之前讨论了存在 ABA 的问题,那么什么是 ABA 问题呢?下面看看表 1 的两个线程发生的场景。

表 1 ABA问题

时 刻

线程1

线程2

备 注

T0

——

——

初始化 X=A

T1

读入X=A

——

——

T2

——

读入X=A

——

T3

处理线程 1 的业务逻辑

X=B

修改共享变量为 B

T4

处理线程 2 业务逻辑第一段

此时线程1在 X=B 的情况下运行逻辑

T5

X=A

还原变量为 A

T6

因为判断 X=A,所以更新数据

处理线程 2 业务逻辑第二段

此时线程 1 无法知道线程 2 是否修改过 X,引发业务逻辑错误

T7

——

更新数据

——

在 T3 时刻,由于线程 2 修改了 X=B,此时线程 1 的业务逻辑依旧执行,但是到了 T5 时刻,线程 2 又把 X 还原为 A,那么到了 T6 时刻,使用 CAS 原理的旧值判断,线程 1 就会认为 X 值没有被修改过,于是执行了更新。

我们难以判定的是在 T4 时刻,线程 1 在 X=B 的时候,对于线程 1 的业务逻辑是否正确的问题。由于 X 在线程 2 中的值改变的过程为 A->B->A,才引发这样的问题,因此人们形象地把这类问题称为 ABA 问题。

ABA 问题的发生,是因为业务逻辑存在回退的可能性。如果加入一个非业务逻辑的属性,比如在一个数据中加入版本号(version),对于版本号有一个约定,就是只要修改 X 变量的数据,强制版本号(version)只能递增,而不会回退,即使是其他业务数据回退,它也会递增,那么 ABA 问题就解决了,如表 2 所示。

表 2 用版本号消除 ABA 问题

时刻

线程1

线程2

备 注

T0

——

——

初始化 X=A,version=0

T1

读入X=A

——

线程1旧值:version=0

T2

——

读入X=A

线程2旧值:version=0

T3

处理线程1的业务逻辑

X=B

修改共享变量为 B,version=1

T4

处理线程 2 业务逻辑第一段

——

T5

——

X=A

还原变量为A,version=2

T6

判断 version == 0,由于线程 2 两次更新数据,导致数据 version=2,所以不再更新数据

处理线程 2 业务逻辑第二段

此时线程 1 知道旧值 version 和当前 version 不一致,将不更新数据

T7

——

更新数据

——

只是这个 version 变量并不存在什么业务逻辑,只是为了记录更新次数,只能递增,帮助我们克服 ABA 问题罢了,有了这些理论,我们就可以开始使用乐观锁来完成抢红包业务了。

乐观锁实现抢红包业务

通过上述的讨论,我们清楚了 CAS 原理如何避免数据的不一致,如何规避 CAS 原理产生的 ABA 问题,在高并发的应用中使用 CAS 原理,我们称之为乐观锁。

为了顺利使用乐观锁,需要先在红包表(T_RED_PACKET)加入一个新的列版本号(version),这个字段在建表的时候已经建了,只是我们还没有使用,那么在 UserRedPacket.xml 的代码中加入新的方法,代码如下所示。

update T_RED_PACKET

set stock = stock - 1,

version = version + 1

where id = #{id}

and version = #{version}

注意加粗的代码,在扣减红包的时候,增加了对版本号的判断,其次每次扣减都会对版本号加一,这样保证每次更新在版本号上有记录,从而避免 ABA 问题。

对于查询也不使用 for update 语句,避免锁的发生,这样就没有线程阻塞的问题了,这里在对应的 UserRedPacketDao 接口上加入对应方法,然后就可以在类 UserRedPacketServiceImpl 中新增方法 grapRedPacketForVersion(需要在其接口 UserRedPacketService 加上同样的方法),完成对应的逻辑即可,代码如下所示。

@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

public int grapRedPacketForVersion(Long redPacketId, Long userId) {

// 获取红包信息,注意version值

RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);

// 当前小红包库存大于0

if (redPacket.getstock() > 0) {

// 再次传入线程保存的version 旧值给SQL判断,是否有其他线程修改过数据

int update = redPacketDao.decreaseRedPacketForVersion(redPacketId,redPacket.getVersion());

// 如果没有数据更新,则说明其他线程已经修改过数据,本次抢红包失败

if (update == 0) {

return FAILED;

}

// 生成抢红包信息

UserRedPacket userRedPacket = new UserRedPacket();

userRedPacket.setRedPacketId(redPacketId);

userRedPacket.setUserId(userId);

userRedPacket.setAmount(redPacket.getUnitAmount());

userRedPacket.setNote ("抢红包"+ redPacketId);

//插入抢红包信息

int result = userRedPacketDao.grapRedPacket(userRedPacket);

return result;

}

//失败返回

return FAILED;

}

version 值一开始就保存到了对象中,当扣减的时候,再次传递给 SQL,让 SQL 对数据库的 version 和当前线程的旧值 version 进行比较。如果一致则插入抢红包的数据,否则就不进行操作。

为了进行测试,在控制器 UserRedPacketController 内新建方法,代码如下所示。

@RequestMapping(value = "/grapRedPacketForVersion")

@ResponseBody

public Map grapRedPacketForVersion(Long redPacketId, Long userId) {

// 抢红包

int result = UserRedPacketService.grapRedPacketForVersion(redPacketId, userId);

Map retMap = new HashMap();

boolean flag = result > 0;

retMap.put("success", flag);

retMap.put("message", flag ? "抢红包成功" : "抢红包失败");

return retMap;

}

修改 test.jsp 的 JSP 文件中 JavaScript 的 POST 请求地址和红包,id 对应上新增控制器的方法,便可以进行测试,结果如图 2 所示。

图 2  乐观锁测试结果

从图 2 中我们看到,经过 3 万次的抢夺,还会存在大量的红包,也就是存在大量的因为版本不一致的原因造成抢红包失败的请求,不过这个失败率太高了一点。有时候会容忍这个失败,这取决于业务的需要,因为允许用户自己再发起抢夺红包,比如微信抢红包也常常发生错误返回。再次对性能进行测试,如图 3 所示。

图 3  测试乐观锁性能

33 秒完成所有抢红包的功能,这个结果和不使用任何锁一样,但是还存在大量的红包因为版本并发的原因而没有被抢到,而且这个概率比较高,下面解决这个问题。

为了克服这个问题,提高成功率,还会考虑使用重入机制。也就是一旦因为版本原因没有抢到红包,则重新尝试抢红包,但是过多的重入会造成大量的 SQL 执行,所以目前流行的重入会加入两种限制,一种是按时间戳的重入,也就是在一定时间戳内(比如说 100 毫秒),不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。

另外一种是按次数,比如限定 3 次,程序尝试超过 3 次抢红包后,就判定请求失效,这样有助于提高用户抢红包的成功率,下面讨论如何重入。

乐观锁重入机制

因为乐观锁造成大量更新失败的问题,使用时间戳执行乐观锁重入,是一种提高成功率的方法,比如考虑在 100 毫秒内允许重入,把 UserRedPacketServiceImpl 中的方法 grapRedPacketForVersion 修改为以下代码。

@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

public int grapRedPacketForVersion(Long redPacketId, Long userId) {

// 记录开始时间

long start = System.currentTimeMillis();

// 无限循环,等待成功或者时间满100亳秒退岀

while (true) {

// 获取循环当前时间

long end = System.currentTimeMillis();

// 当前时间己经超过100毫秒,返回失败

if (end - start > 100) {

return FAILED;

}

// 获取红包信息,注意version值

RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);

// 当前小红包库存大于0

if (redPacket.getStock() > 0) {

// 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据

int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());

// 如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺

if (update == 0) {

continue;

}

// 生成抢红包信息

UserRedPacket UserRedPacket = new UserRedPacket();

UserRedPacket.setRedPacketId(redPacketId);

UserRedPacket.setUserId(userId);

UserRedPacket.setAmount(redPacket.getUnitAmount());

UserRedPacket.setNote("抢红包" + redPacketId);

// 插入抢红包信息

int result = userRedPacketDao.grapRedPacket(UserRedPacket);

return result;

} else {

// 一旦没有库存,则马上返回

return FAILED;

}

}

}

当因为版本号原因更新失败后,会重新尝试抢夺红包,但是会实现判断时间戳,如果时间戳在 100 毫秒内,就继续,否则就不再重新尝试,而判定失败,这样可以避免过多的 SQL 执行,维持系统稳定。乐观锁按时间戳重入,如图 4 所示。

图 4  乐观锁按时间戳重入

从结果来看,之前大量失败的场景消失了,也没有超发现象,3 万次尝试抢光了所有的红包,避免了总是失败的结果,但是有时候时间戳并不是那么稳定,也会随着系统的空闲或者繁忙导致重试次数不一。

有时候我们也会考虑限制重试次数,比如 3 次。下面再次改写 UserRedPacketServiceImpl 中的方法 grapRedPacketForVersion,代码如下所示。

@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

public int grapRedPacketForVersion(Long redPacketId, Long userId) {

for (int i = 0; i < 3; i++) {

// 获取红包信息,注意version值

RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);

// 当前小红包库存大于0

if (redPacket.getStock() > 0) {

// 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据

int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());

// 如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺

if (update == 0) {

continue;

}

// 生成抢红包信息

UserRedPacket UserRedPacket = new UserRedPacket();

UserRedPacket.setRedPacketId(redPacketId);

UserRedPacket.setUserId(userId);

UserRedPacket.setAmount(redPacket.getUnitAmount());

UserRedPacket.setNote("抢红包" + redPacketId);

// 插入抢红包信息

int result = userRedPacketDao.grapRedPacket(UserRedPacket);

return result;

} else {

// 一旦没有库存,则马上返回

return FAILED;

}

}

return FAILED;

}

通过 for 循环限定重试 3 次,3 次过后无论成败都会判定为失败而退出,这样就能避免过多的重试导致过多 SQL 被执行的问题,从而保证数据库的性能。为此笔者也进行了测试,如图 5 所示。

图 5  乐观锁按重试次数重入

显然效果很好,3 万次请求,所有红包都被抢到了,也没有发生超发现象,这样就可以消除大量的请求失败,避免非重入的时候大量请求失败的场景。

但是现在是使用数据库的情况,有时候并不想使用数据库作为抢红包时刻的数据保存载体,而是选择性能优于数据库的 Redis。

之前我们学习过 Redis 事务,也学习过 Redis 的 Lua 语言,它是一种原子性的操作,为此我们将绕开数据库用 Redis 处理高并发的请求。

数据库并发抢红包_Redis乐观锁解决高并发抢红包的问题相关推荐

  1. 数据库并发抢红包_Redis悲观锁解决高并发抢红包的问题

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

  2. mysql乐观锁 秒杀_使用数据库乐观锁解决高并发秒杀问题,以及如何模拟高并发的场景,CyclicBarrier和CountDownLatch类的用法...

    数据库:mysql 数据库的乐观锁:一般通过数据表加version来实现,相对于悲观锁的话,更能省数据库性能,废话不多说,直接看代码 第一步: 建立数据库表: CREATE TABLE `skill_ ...

  3. Redis锁解决高并发问题

    Redis锁解决高并发问题 redis真的是一个很好的技术,它可以很好的在一定程度上解决网站一瞬间的并发量,例如商品抢购秒杀等活动. redis之所以能解决高并发的原因是它可以直接访问内存,而以往我们 ...

  4. 利用Redis锁解决高并发问题

    利用Redis锁解决高并发问题 参考文章: (1)利用Redis锁解决高并发问题 (2)https://www.cnblogs.com/yszr/p/11698696.html 备忘一下.

  5. PHP利用Mysql锁解决高并发

    前面写过利用文件锁来处理高并发的问题的,现在我们说另外一个处理方式,利用Mysql的锁来解决高并发的问题 先看没有利用事务的时候并发的后果 创建库存管理表 CREATE TABLE `storage` ...

  6. (13) 悲观锁和乐观锁解决hibernate并发(转)

    前言:  做项目时由于业务逻辑的需要,必须对数据表的一行或多行加入行锁,举个最简单的例子,图书借阅系统.假设 id=1 的这本书库存为 1 ,但是有 2 个人同时来借这本书,此处的逻辑为 Select ...

  7. mysql 高并发写入锁表_使用mysql中的锁解决高并发问题

    阿里云产品通用代金券,最高可领1888分享一波阿里云红包. 阿里云的购买入口 为什么要加锁 多核计算机的出现,计算机实现真正并行计算,可以在同一时刻,执行多个任务.在多线程编程中,因为线程执行顺序不可 ...

  8. Zookeeper锁实现分布式锁解决高并发高可用秒杀系统

    抢购前数据 库存表 订单表 zookeeper配置类 /*** @Author yqq* @Date 2022/05/30 23:24* @Version 1.0*/ @Configuration p ...

  9. 通俗易懂 高并发案例春节抢红包分析 乐观锁 悲观锁 分布式锁

    今年春节响应国家号召在家宅着抵抗疫情,拜年也改用微信红包,春节发了很多也抢了很多微信红包,也算支持了公司业务,微信支付融入生活,抢红包已经是非常平常的事情,想到WXG的小伙伴丰厚的年终奖我柠檬了. 抢 ...

  10. 乐观锁 -业务判断 解决高并发问题

    在解决高并发问题时,如果是分布式系统显然我们只能够使用数据库端加锁机制来解决这个问题,但是这种同步机制或者数据库物理锁机制会牺牲一部分的性能,所以常常以另外一种方式来解决这个问题 就是乐观锁模式 银行 ...

最新文章

  1. Java网页数据采集器[下篇-数据查询]【转载】
  2. clistctrl控件最后插入在后面_用图表控件做一个简单的员工信息查询系统
  3. pku 1236 Network of Schools (tarjan缩点)
  4. Asp.net中文件上传下载的简单实现
  5. Sql Server 2005中的快照隔离
  6. 清空缓存的命令_最实用的6个网络命令,网络故障不求人
  7. Linux下C语言程序的内存布局
  8. [论文阅读] End-to-End Incremental Learning
  9. ORACLE完整数据库实例迁移
  10. ibm服务器vga没有信号,华硕主板vga和boot灯常亮显示器无信号
  11. 数据中心机房建设方案
  12. 小米路由器Pro刷OpenWrt固件
  13. 为什么大家都说SELECT * 效率低
  14. 奶牛戴上VR眼镜“看片”,开心了可以多产奶:俄罗斯官方做了实验,拯救奶牛的冬季忧郁...
  15. 微信wifi服务器地址,微信连WIFI服务器时钟不同步
  16. windows系统开启虚拟化
  17. VR虚拟3d展示线上引流好帮手
  18. 如何防止网站被黑客入侵,避免网站被黑的防御方法
  19. 人民币贬值 ,对普通人、码农的影响
  20. 干货 | 携程酒店RSocket实践

热门文章

  1. 网络故障检查、PING命令使用(实测)
  2. 解决 python plt画柱状图(棒状图)时横坐标刻度线不在中间而在右边
  3. 用html计算长方形的面积公式,长方形的面积公式
  4. Unity线性空间UI的问题
  5. 解读华为的流程与 IT 管理部门
  6. Quorum 机制(分布式系统)
  7. coursera “the media could not be loaded either because the server or network falled...“解决办法
  8. arduino超声波测距接线图详细_Arduino Uno + HY-SRF05 超声波测距模块详细讲解演示实验...
  9. 耐得住寂寞,才能守得住繁华
  10. Kettle数据库连接中的集群与分片