基于Redis的分布式锁实现(秒杀优惠券的优化)

探讨Redis分布式锁解决优惠券拼抢问题 分布式锁是控制不同系统之间访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来确保数据一致性。本文通过单机环境本地锁->集群环境锁演变->分布式锁的延伸,深入浅出的介绍了不同方式实现分布式锁的问题和改进之处,通过不同的例子详细展开说明,并且分析了Redission分布式锁的实现原理和实现。 阅读详情

目录

什么是分布式锁​

基于Redis的分布式锁(非阻塞实现)

当业务阻塞时出现的并发问题(误删Redis分布式锁)

总结


前文链接:

(1条消息) Redis——实现优惠券秒杀_486过于烦躁的博客-优快云博客

什么是分布式锁

修改秒杀优惠券的流程

分布式锁的实现


基于Redis的分布式锁(非阻塞实现)

 

 ILock接口

public interface ILock {
    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的过期时间,过期后自动释放
     * @return true代表获取锁成功,false代表获取锁失败
     */
    boolean trLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

创建一个类并实现ILock接口,用自定义的name值来区分锁,同时获取当前线程的Id作为value值存入进redis中,并设置超时时间

public class SimpleRedisLock implements ILock {

    private static final String KEY_PREFIX="lock:";

    private String name;

    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean trLock(long timeoutSec) {
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

实现

   @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //获取用户Id
        UserDTO user = UserHolder.getUser();
        Long userId = user.getId();

        //创建锁对象
        SimpleRedisLock redisLock = new SimpleRedisLock("order" + userId, stringRedisTemplate);

        //尝试获取锁
        boolean isLock = redisLock.trLock(5);
        //判断
        if(!isLock){
            //获取锁失败,直接返回失败或者重试
            return Result.fail("不允许重复下单");
        }

        try {
            //一人一单
            Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            if(count>0){
                //说明用户之前买过了
                return Result.fail("用户已经购买过一次");
            }

            VoucherOrder voucherOrder = new VoucherOrder();

            //扣减库存
            seckillVoucherService.update()
                    .setSql("stock = stock - 1")
                    .eq("voucher_id",voucherId).gt("stock",0)
                    .update();

            //设置订单Id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            //设置优惠券Id
            voucherOrder.setVoucherId(voucherId);
            //设置用户Id
            voucherOrder.setUserId(userId);

            save(voucherOrder);

            //返回订单Id
            return Result.ok(orderId);
        } finally {
            //释放锁
            redisLock.unlock();
        }
    }

当业务阻塞时出现的并发问题(误删Redis分布式锁)

可能会出现误删Redis分布式锁的情况

修改流程 ,需要判断锁标示是否是自己的

 

需要确保标示是唯一的,可以采用UUID拼接线程Id的做法

业务代码不需要改,只需要修改锁的获取与释放,增加逻辑判断

public class SimpleRedisLock implements ILock {

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

    private String name;

    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean trLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();

        //获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if(threadId.equals(id)){
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}


虽然增加了锁标示的判断,但仍然存在一些极端的问题,如下图,当判断标示与释放锁之间产生了阻塞,也有可能发生误删分布式锁的情况,所以得让判断标示与释放锁成原子性操作,要么一起成功,要么一起失败

我们可以采用Redis中的Lua脚本进行编写,然后在Java程序中进行调用

Redis的Lua脚本

Lua语言的数组第一个下标为1

业务逻辑转换为Lua脚本

再次改进Redis的分布式锁

unlock.lua脚本的编写 

-- 比较线程标识是否是锁的一致
if(redis.call('get',KEYS[1]==ARGV[1])) then
    -- 释放锁
    return redis.call('del',KEYS[1])
end
return 0

实现

public class SimpleRedisLock implements ILock {

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    private String name;

    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean trLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    //基于Lua脚本
    @Override
    public void unlock() {
        //调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

}

总结

仍存在优化的空间

Redis实战-优惠券秒杀(超卖问题,一人一单,分布式锁,Redis优化秒杀,消息队列实现异步秒杀) 本文介绍了分布式系统中实现秒杀功能的完整技术方案,主要包括以下内容: 全局唯一ID生成器设计 基于Redis的自增功能实现分布式ID生成 采用时间戳+序列号的结构保证唯一性和递增性 秒杀基础功能实现 使用乐观锁解决库存超卖问题 通过分布式锁保证一人一单限制 Redis分布式锁优化 基于Redisson实现可重入锁 利用WatchDog机制实现锁自动续期 通过主从集群提高可用性 秒杀性能优化方案 将库存校验前置到Redis中 使用Lua脚本保证原子性操作 采用消息队列实现异步下单 高级消息队列实现 基于Str 阅读详情

相关推荐

黑马点评—优惠券秒杀相关

具体场景举例:线程1获取Redis分布式锁后执行业务逻辑,但是由于某种原因导致其业务阻塞直到Redis分布式锁被自动释放,此时线程2可以重新获取释放后的Redis分布式锁然后执行自己的任务,如果在线程2执行自己任务的过程中,线程1阻塞结束并且完成了自己的业务,按照之前的设计此时线程1会释放Redis分布式锁,但是由于当前的Redis分布式锁已经被线程2持有,所以线程1实际上释放的是线程2的Redis分布式锁,因此出现Redis分布式锁误删的情况。另外由于使用的是内存,如果服务宕机,用户的订单信息会丢失;

yj000703的博客 1749

【使用Redis分布式锁实现优惠券秒杀功能】-Redis学习笔记05

利用Redis分布式锁解决《超卖问题》、《一人一单问题》,悲观锁、乐观锁详细实现方案

weixin_42587931的博客 684

黑马Redis实战项目——黑马点评笔记04 | 优惠券秒杀

使用postman添加秒杀券//秒杀券信息 {

2301_77450803的博客 1674

Redis分布式锁实战(库存扣减 Setnx、Redisson及底层源码、RedLock)

Redis高并发分布式锁实战(Setnx、Redisson及底层源码、RedLock):库存扣减中理解分布式锁的含义

Jay112011的博客 4344

手把手教你用Logisim搞定华科计组实验:单总线CPU硬布线控制器设计(含Excel自动生成电路技巧)

本文详细解析了如何使用Logisim完成华科计组实验中的单总线CPU硬布线控制器设计,特别分享了利用Excel自动生成电路逻辑的高效技巧。从实验准备、状态机表格填写到模块化构建和系统联调,提供全流程实战指导,帮助读者快速掌握计算机组成原理核心概念,大幅提升实验效率。

weixin_28744423的博客 321

从零构建智慧农业监测终端:STM32数据采集与LoRa组网实战

本文详细介绍了如何从零构建智慧农业监测终端,涵盖STM32数据采集与LoRa组网实战。通过DHT22温湿度传感器和YL-69土壤湿度传感器实现精准环境监测,结合LoRa无线传输技术解决偏远农田信号问题。文章还分享了硬件选型、电路设计、低功耗优化及Flutter跨平台应用开发等关键技巧,助力农业智能化升级。

weixin_29172963的博客 357

Redis 基础 - 优惠券秒杀《分布式锁(使用Redisson)》

Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 Redis 基础 - 优惠券秒杀《非集群》 Redis 基础 - 优惠券秒杀《分布式锁(初级)》 基于setnx实现的分布式锁存在的问题 1)不可重入 同一个线程无法多次获取同一把锁 2)不可重试 获取锁只尝试一次就返回false,没有重试机制 3)超时释放 锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患 4)主

PurineKing之博客 857

Python OpenCV基于颜色通道分离法去除图片中的红色印章

最近一段时间的工作都是研究如何去除图片中的红色印章,在网上查找了大量的大佬写过的方法,发现大多数是采取颜色通道分离法来做,并且效果还不错。站在前人的肩膀上,我又做了些许调整,谈不上是改进,但是能应对更对的情况总还是好的,特此记录。 拿发票做例子,印章相较于其它部分最大的不同就是印章是红色的,而其它部分不是(这是一句废话)。 那么第一步当然就是将红色分离出来,这一步可以采用Python第三方库OpenCV里的 cv2.split()方法,该方法是把RGB图像的三个通道拆分开来,得...

wl_Honest的博客 1万+

Redis实战案例及问题分析——分布式锁(解决优惠券秒杀场景集群并发下的安全问题)

目录 分布式锁 基于redis实现分布式锁改进Redis的分布式锁上述的改进依然存在问题:上述改进后仍然存在的问题: ReidssonRedisson可重入锁原理Redisson的锁重试和WatchDog机制 分布式锁解决上述问题的方式总结Redisson分布式锁主从一致性问题 总结:1)不可重入Redis分布式锁:2)可重入的Redis分布式锁:3)Redisson的multiLock:集群下一人一单的线程并发安全问题:集群模式下用synchronized并不能解决并发安全问题......

PnJgHT的博客 921

服务器基础知识介绍

服务器,不仅仅是一台高性能计算机,更是网络的核心节点,承担着网络服务的责任。然而,相较于普通计算机,服务器对于稳定性、安全性和性能等方面提出了更高的要求,这使得它们在数字化浪潮中傲然独立。在硬件成本结构中,CPU和芯片组占据约50%的成本,内存占15%,外部存储占10%,其余硬件约占25%。服务器的逻辑结构与普通计算机相似,但由于其需要提供高性能计算,对稳定性、可靠性、安全性、可扩展性和可管理性的要求更高。服务器的分类多种多样,根据产品形式可分为塔式服务器、机架服务器、刀片服务器和机柜服务器。

m0_73576645的博客 1883

Cursor 使用教程(2025年更新版)

官方Cursor详细使用教程在“扩展学习”

我是赛博AI Lewis 7082

【Redis实战篇】Redis的擅长实现的功能 优惠券秒杀、分布式锁、分布式锁Redission、Redis消息队列、秒杀优化

基于setnx实现的分布式锁存在下面的问题重入问题:可重入锁(Reentrant Lock)是一种多线程同步机制,允许线程在持有锁的情况下再次请求相同的锁,而不会被自己所持有的锁所阻塞。这种锁的主要特性是它允许一个线程多次获取同一把锁而不会造成死锁。比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

2301_77358195的博客 961

【论文阅读1】机理引导的残差学习用于电池全生命周期状态监测 | Nature Communications

本文提出了一种机理引导的残差学习框架,用于提升电池全生命周期内荷电状态(SOC)和健康状态(SOH)的监测精度。该方法融合等效电路模型的先验知识与机器学习,设计了修正模型和补偿模型两种通用框架。实验验证表明,该方法在三种不同类型电池数据集上均表现出色,SOC和SOH估计误差较传统方法降低50%以上,且在未见工况下仍保持良好外推能力。模型具有强鲁棒性,在减少训练数据量和降低采样频率时性能稳定,并兼具物理可解释性。该框架无需离线测试和运行中校准,为电池安全监测提供了有效解决方案。

qq_21843433的博客 616

redis优惠券秒杀、setnx分布式锁的实现和优化、Redisson、Redisson重入和超时以及重试锁的原理、redis优惠券秒杀优化、redis实现消息队列

测试 实现优惠券秒杀下单 超卖问题 一人一单 分布式锁 改进Redis的分布式锁 解决锁的误删 分析下面一个场景: 使用Lua脚本解决锁原子性问题 我们上面解决了锁的误删问题,通过线程标识解决锁的误删问题,但是这还是不完美的,因为判断线程标识和释放锁的操作不是原子的: 至此redis分布式锁相对已经完善了,但是还有以下问题需要考虑: 1、引入依赖: 2、配置Redisson客户端 Redisson可重入锁的原理 setnx无法实现锁重入 setnx是无法实现redi

m0_45364328的博客 482

AutoSAR系列讲解(入门篇)1.2-AutoSAR概述

这里主要说明了AutoSAR是个什么,还有其工具链

雪云飞星的博客 5万+

(SEED-Lab)Buffer Overflow Vulnerability Lab缓冲区溢出实验

(SEED-Lab)Buffer Overflow Vulnerability Lab 欢迎大家访问我的GitHub博客 https://lunan0320.cn 文章目录一、实验目的二、实验步骤与结果2.1 环境初始化2.2 Task1 运行shellcode2.3漏洞程序2.4 Task 22.5 Task 3: ASLR2.6 Task 4: Stack Guard2.7 Task 5:不可执行栈三、参考文献 一、实验目的 理解缓冲区溢出攻击的原理以及如何实施攻击 理解针对缓冲区溢出的防御措施

lunan的博客 8982

Redis实现优惠券秒杀功能

例如,使用Long类型,占8个字节,64bit位,1bit符号位,前31bit使用时间戳,支持69年使用时间,后32位使用序列号,每秒产生2^32个不同ID。tb_voucher包含平价券和特价券,tb_seckill_voucher只描述特价券,包含库存,生效时间,失效时间。优点是性能好,缺点是有可能出现少买。缺点,用户还是能下多单,因为多个线程判断成功后可以继续购买,和后续扣减库存,生成订单不是原子性操作。2、修改lua脚本,向stream.orders中添加消息,包含优惠券id,用户id,订单id。

qq_41523340的博客 1309

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

redis解决优惠券秒杀

redis实现解决优惠券秒杀

m07170729的博客 1173

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Redis——优惠卷秒杀

每个店铺都可以发布优惠券:场景分析:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

m0_73618358的博客 722

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

优惠券秒杀业务实现与原理

针对秒杀项目中的优惠券部分做了专门的原理介绍和实现,希望有所帮助

m0_67647682的博客 467

整整3W字笔记,Redis最核心的秒杀业务、分布式锁、消息队列相关原理一篇文章就搞定(黑马点评项目)

本文为 黑马点评项目的第四部分——秒杀优惠卷系列功能实现,在本文中,你能学到: 1. 全局ID生成器的应用场景及原理实现 2. 理解原理并解决秒杀业务中出现的超卖问题、一人一单问题。 3. 理解原理并分析 分布式锁的特点,使用分布式锁优化秒杀业务,提高系统可靠性 4. 熟练使用 Redisson锁工具包 5. 了解Redis中提供的消息队列模型实现原理,并使用消息模型实现异步秒杀功能

wzc200406的博客 2676

黑马点评开发日记-优惠券秒杀(悲观锁,乐观锁,分布式锁,Redisson)

Override//查询优惠券信息//判断秒杀是否开始//未开始return Result.fail("秒杀尚未开始");//已经结束return Result.fail("秒杀已经结束");//判断库存是否充足return Result.fail("库存不足");//6.一人一单//用户id//6.1查询订单//6.2判断用户是否已经购买过了//购买过了return Result.fail("您已经购买过了");//扣减库存.update();if (!

2302_81153890的博客 1457

黑马Redis详细笔记(实战篇---优惠券秒杀系统的实现与优化)

在高并发的秒杀场景中,如何实现高效、可靠的秒杀系统是一个极具挑战性的问题。本文将详细介绍如何基于Redis和Java实现一个完整的优惠券秒杀系统,涵盖全局唯一ID生成、秒杀下单流程、分布式锁的实现与优化,以及消息队列的应用。通过这些技术手段,我们可以有效解决秒杀场景中的常见问题,如超卖、重复下单、线程安全等。

qq_51749909的博客 1340

【黑马点评】优惠券秒杀

问题解决。

qq_57882997的博客 747

Redis项目实战——优惠券秒杀

三种消息队列对比总结

weixin_46838605的博客 1147

Redis学习笔记

业务阻塞后会出现极端情况,一个线程的锁超时释放后,这个进程结束阻塞的同时也会把另一个线程的锁给释放掉了。先刷新用户的token如果不存在就接着往下走,然后走到下一个拦截器中拦截。目前都是同步进行的,优化方案是使用异步。使用独立的线程来解决耗时的部分。解决方案,存放一个线程标识,是自己的锁才能释放不释放别人的锁。在之前的写法高并发的场景下库存会出现超卖的问题。使用map结构来计算同一个线程进入了多少次锁。先操作数据库在写入缓存出现异常的情况较少。乐观锁是用来解决更新数据时候的并发问题,添加redis线程锁。

这个人很懒什么都没有留下 1064
上一篇: Redis——实现优惠券秒杀
下一篇: 操作系统——死锁
486过于烦躁
博客等级 码龄6年 17粉丝 34原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值