前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis乐观锁解决高并发抢红包的问题【redis】

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

作者头像
sinnoo
发布2022-01-04 16:28:03
9750
发布2022-01-04 16:28:03
举报
文章被收录于专栏:技术人生技术人生

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

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

CAS 原理概述

在 CAS 原理中,对于多个线程共同的资源,先保存一个旧值(Old Value),比如进入线程后,查询当前存量为 100 个红包,那么先把旧值保存为 100,然后经过一定的逻辑处理。 当需要扣减红包的时候,先比较数据库当前的值和旧值是否一致,如果一致则进行扣减红包的操作,否则就认为它已经被其他线程修改过了,不再进行操作,CAS 原理流程如图 1 所示。

CAS 原理并不排斥并发,也不独占资源,只是在线程开始阶段就读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较,即比较各个线程当前共享的数据是否和旧值保持一致。 如果一致,就开始更新数据;如果不一致,则认为该数据已经被其他线程修改了,那么就不再更新数据,可以考虑重试或者放弃。有时候可以重试,这样就是一个可重入锁,但是 CAS 原理会有一个问题,那就是 ABA 问题,下面先来讨论一下 ABA 问题。

ABA 问题

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

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

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

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

乐观锁重入机制

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

代码语言:javascript
复制
@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以通知调用者事务执行失败:

代码语言:javascript
复制
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示例代码:

代码语言:javascript
复制
<?php  
$redis = new redis();  
$result = $redis->connect('127.0.0.1', 6379);  
$mywatchkey = $redis->get("mywatchkey");  
$rob_total = 100;   //抢购数量  
if($mywatchkey<$rob_total){  
    $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 "抢购成功!<br/>";  
        echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";  
        echo "用户列表:<pre>";  
        var_dump($mywatchlist);  //打印抢购成功用户
    }else{  
        echo "手气不好,再抢购!";exit;  
    }  
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/01/27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CAS 原理概述
  • ABA 问题
  • 乐观锁重入机制
  • Redis乐观锁详解及应用
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档