java抢红包并发问题,Redis乐观锁解决高并发抢红包的问题【redis】

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

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

CAS 原理概述

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

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

ae9669d6be0a98b3a7ab4fccb8c83048.png

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

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

ABA 问题

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

47f3d4cb05070828b8fbc598bc557f66.png

在 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 所示。

84145f26bc604b25eec5aedf9a0d7617.png

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

但是这样会导致一个新的问题,就是高并发的情况下失败率比较高。所以目前流行的重入会加入两种限制,一种是按时间戳的重入,也就是在一定时间戳内(比如说 100 毫秒),不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。

乐观锁重入机制

因为乐观锁造成大量更新失败的问题,使用时间戳执行乐观锁重入,是一种提高成功率的方法,比如考虑在 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 执行,维持系统稳定。乐观锁按时间戳重入 。

但是有时候时间戳并不是那么稳定,也会随着系统的空闲或者繁忙导致重试次数不一。

有时候我们也会考虑限制重试次数

通过 for 循环限定重试 3 次,3 次过后无论成败都会判定为失败而退出,这样就能避免过多的重试导致过多 SQL 被执行的问题

Redis乐观锁详解及应用

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。假设通过WATCH命令在事务执行之前监控了某个key,倘若在WATCH之后Key的值发生了变化,EXEC命令执行的事务将被放弃,同时返回nil以通知调用者事务执行失败:

redis> SET key 1

OK

redis> WATCH key

OK

redis> SET key 2

OK

redis> MULTI

OK

redis> SET key 3

QUEUED

redis> EXEC

(nil)

redis> GET key

"2"

因此,借用redis使用watch可以完成秒杀抢购功能,使用redis中两个key完成秒杀抢购功能,mywatchkey用于存储抢购数量和mywatchlist用户存储抢购列表。

php示例代码:

$redis = new redis();

$result = $redis->connect('127.0.0.1', 6379);

$mywatchkey = $redis->get("mywatchkey");

$rob_total = 100; //抢购数量

if($mywatchkey

$redis->watch("mywatchkey");

$redis->multi();

//设置延迟,方便测试效果。

sleep(5);

//插入抢购数据

$redis->hSet("mywatchlist","user_id_".mt_rand(1, 9999),time());

$redis->set("mywatchkey",$mywatchkey+1);

$rob_result = $redis->exec();

if($rob_result){

$mywatchlist = $redis->hGetAll("mywatchlist");

echo "抢购成功!
";

echo "剩余数量:".($rob_total-$mywatchkey-1)."
";

echo "用户列表:

";

var_dump($mywatchlist); //打印抢购成功用户

}else{

echo "手气不好,再抢购!";exit;

}

}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值