Redis用于全局ID生成器、分布式锁的解决方案

news2025/1/9 14:27:10

全局ID生成器

每个店铺都可以发布优惠卷

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增id就存在一些问题:

1.id的规律性太明显

2.受单表数据量的限制

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息

ID的组成部分

1.符号位:1bit,永远为0;

2.时间戳:31bit,以秒为单位,可以使用69年

3.序列号:32bit,秒内的计数器,支持每秒产生2⋀32个不同的ID

RedisIdWorker

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long Begin_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;
    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        //1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - Begin_TIMESTAMP;
        //2.生成序列号
        //2.1获取当前日期。精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        //3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

private ExecutorService es = Executors.newFixedThreadPool(500);

    @Resource
    private RedisIdWorker redisIdWorker;

    @Test
    void testIdWorker() throws InterruptedException {

        CountDownLatch latch = new CountDownLatch(300);
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id=" + id);
            }
            latch.countDown();
        };
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time" + (end - begin));
    }

全局唯一ID生成策略

1.UUID

2.Redis自增

3.snowflake算法

4.数据库自增

Redis自增ID策略

1.每天一个key,方便统计订单量

2.ID构造是时间戳+计数器

实现优惠券秒杀下单

每个店铺都可以发布优惠券,分为平价劵。平价券可以任意购买,而特价券需要秒杀抢购

表关系如下:

tb_voucher:优惠券的基本信息,优惠金额,使用规则等

tb_seckill_vouvher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

实现优惠券秒杀下单

在VoucherController实现了一个接口,可以实现添加秒杀优惠券、

@RestController
@RequestMapping("/voucher")
public class VoucherController {

    @Resource
    private IVoucherService voucherService;
 /**
     * 新增普通券
     * @param voucher 优惠券信息
     * @return 优惠券id
     */
    @PostMapping
    public Result addVoucher(@RequestBody Voucher voucher) {
        voucherService.save(voucher);
        return Result.ok(voucher.getId());
    }

    /**
     * 新增秒杀券
     * @param voucher 优惠券信息,包含秒杀信息
     * @return 优惠券id
     */
    @PostMapping("seckill")
    public Result addSeckillVoucher(@RequestBody Voucher voucher) {
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }   
}

用户可以在这些店铺页面中抢购这些优惠券

下单时需要判断两点

1.秒杀是否开始或结束,如果尚未开始或已经结束则无法下单

2.库存是否充足,不足则无法下单

超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁

乐观锁

乐观锁的关键是判断之前查询得到的数据是否被修改过,常见的方式有两种

超卖总结

超卖这样的线程安全问题,解决方案有哪些

1.悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴

  • 缺点:性能一般

2.乐观锁:不加锁,在更新时判断是否有其他线程在修改

  • 优点:性能好

  • 存在成功率低的问题

一人一单

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

VoucherOrderServiceImpl

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {

        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        //2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始");
        }

        //3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //秒杀已经结束
            return Result.fail("秒杀已经结束");
        }
        //4.判断库存是否充足
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            //获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //5.一人一单
        Long userId = UserHolder.getUser().getId();

        //5.1查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2判断是否存在
        if (count>0) {
            //用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }
        //6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherId).gt("stock",0).update();
        if (!success) {
            //扣减失败
            return Result.fail("库存不足");
        }
        //7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //7.2用户id
        Long userid = UserHolder.getUser().getId();
        voucherOrder.setUserId(userid);
        //7.3代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回订单id
        return Result.ok(orderId);
    }
}

一人一单的并发安全问题

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了

1.我们将服务启动两份,端口分别是8081和8082:

2.然后修改nginx的conf目录下的nginx.cong文件,配置反向代理和负载均衡:

现在用户节点会在这两个节点上负载均衡,再次测试下是否存在线程安全问题

分布式锁

什么是分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

分布式锁的实现

分布式锁的核心是实现多进程之间互斥,而满足这一节点的方式有很多种,常见的有三种:

基于Redis的分布式锁

实现分布式锁需要实现的两个基本方法:

1.获取锁:

  • 互斥:确保只能有一个线程获取锁

  • 非阻塞:尝试一次,成功返回true,失败返回false

2.释放锁

  • 手动释放

基于Redis实现分布式锁的初级版本

ILock

SimpleRedisLock

public class SimpleRedisLock implements ILock {
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";

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

    @Override
    public boolean tryLock(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);
    }
}

基于Redis实现分布式锁的初级版本存在问题分析

1,业务阻塞超时或业务未执行完释放其他线程的锁

2.释放锁之前判断是否是该线程的锁

改进Redis的分布式锁

需求:修改之前的分布式锁实现,满足:

1.在获取锁时存入线程标识(可以用UUID表示)

2.在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致

  • 如果一致则释放锁

  • 如果不一致不释放锁

SimpleRedisLock

public class SimpleRedisLock implements ILock {
    private String name;
    private StringRedisTemplate stringRedisTemplate;


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

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

    @Override
    public boolean tryLock(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的分布式锁后存在问题分析

锁判断和锁释放不是原子性

Redis的Lua脚本

Redist提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:
https://www.runoob.com/lua/lua-tutorial.html

这里重点介绍Redis提供的调用函数,语法如下:

例如我们要执行set name jack,则脚本是这样:

例如,我们要先执行set name Rose,再执行get name,则脚本如下

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

例如,我们要执行redis.call(‘set’,‘name’,‘jack’)这个脚本,语法如下:

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其他参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

释放锁的业务流程是这样的:

1.获取锁中的线程标示

2.判断是否与指定的标示(当前线标示)一致

3.如果一致则释放锁(删除)

4.如果不一致则什么都不做

利用Lua脚本来表示则是这样的:

再次改进Redis的分布式锁

需求:基于Lua脚本实现分布式锁的释放逻辑

提示:RedisTemplate调用Lua脚本的API如下:

unlock.lua

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

SimpleRedisLock

public class SimpleRedisLock implements ILock {
    private String name;
    private StringRedisTemplate stringRedisTemplate;


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

    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);
    }

    @Override
    public boolean tryLock(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() {
        //调用lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}

基于Redis实现的分布式锁总结

1.基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示

  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

2.特性:

  • 利用set nx满足互斥性

  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

  • 利用Redis集群保证高可用和高并发性

基于Redis的分布式锁优化

基于setnx实现的分布式锁存在下面问题:

Redisson

Redisson是一个在Redis的基础上实现java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

官网地址:https://redisson.org

GitHub地址:https://github.com/redisson/redisson

Redisson入门

1.引入依赖

2.配置Redisson客户端

3.使用Redisson的分布式锁

Redisson可重入锁原理

获取锁的Lua脚本

释放锁的Lua脚本

Redisson分布式锁原理

1.可重入:利用hash结构记录线程id和重入次数

2.可重试:利用信号量和PubSub和功能实现等待、唤醒、获取锁失败的重试机制

3.超时续约:利用watchDog,每隔一段时间,重置超时时间

Redisson分布式锁主从一致性问题

分布式锁总结

1.不可重入Redis分布式锁:

  • 原理:利用setnx的互斥性,利用ex避免死锁,释放锁时,判断线程标示

  • 缺陷:不可重入、无法重试,锁超时失效

2.可重入的Redis分布式锁

  • 原理:利用hash结构,记录线程和重入次数,利用watchDog延续时间,利用信号量控制锁重试等待

  • 缺陷:redis宕机引起锁失效问题

3.Redisson的multiLock

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功

  • 缺陷:运维成本高,失效复杂

视频地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/417706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

极光笔记 | 如何在Shopify中使用EngageLab (下)

Sendgird发布的《2022 Global Messaging Engagement Report》中揭示了世界各地的用户更喜欢用哪种方式与品牌互动&#xff0c;结论是&#xff1a;“电子邮件仍然是第一名&#xff08;短信紧随其后&#xff09;”。4800多名受访者中&#xff0c;有18%的人将电子邮件列为他们最常…

普通人是否能从ChatGPT中分一杯羹?

ChatGPT3.0刚刚推出&#xff0c;最开始的时候&#xff0c;人们只是将ChatGPT看作一个很会聊天的机器人&#xff0c;无论问题多么天马行空&#xff0c;它的答案看上去都有理有据。后来&#xff0c;像打开潘多拉魔盒一样&#xff0c;很多人开始拿它编大纲、撰写文案、编代码、创作…

Docker本地推送到hub,以及上传时遇到的问题解决

1.在本地创建一个 Dockerfile FROM ubuntu:latest RUN apt-get update && apt-get install -y curl CMD ["curl", "https://www.baidu.com"]2.在本地构建 Docker 镜像 在创建本地docker镜像的时候[TAG] .和[TAG] /PATH/TO 需要注意dockerfile文件…

ATTCK v12版本战术介绍——防御规避(二)

一、引言 在前几期文章中我们介绍了ATT&CK中侦察、资源开发、初始访问、执行、持久化、提权战术、防御规避&#xff08;一&#xff09;理论知识及实战研究&#xff0c;本期我们为大家介绍ATT&CK 14项战术中防御规避战术&#xff08;二&#xff09;&#xff0c;包括防御…

【数据结构】顺序栈和链栈的基本操作(定义,初始化, 入栈,出栈,取栈顶元素,遍历,置空)

&#x1f38a;专栏【数据结构】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【勋章】 大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 目录 ⭐栈的分类 ✨顺序栈 &#x1f388;优点&#xff1a; &…

离线安装k8sv1.20.5版本并部署服务

注意&#xff1a;我这里的离线安装包是V1.20.5的&#xff0c;单安装一个master节点并部署服务&#xff0c;保证可以使用。如果安装集群也是可以的&#xff0c;但是需要把离线包上传到所有的node节点&#xff0c;导入&#xff0c;最后把node节点接入到K8S集群即可&#xff0c;本…

js flyout 2: VScroll

目录版权描述测试页面showFlyout问题1 - scroll 实现可能不准?问题2 - 容器内容重排可导致浮层错位关于重排小结附录 - 完整代码版权 本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/130101031. 文中代码属于 pu…

【致敬未来的攻城狮计划】学习总结

文章目录【致敬未来的攻城狮计划】学习总结前言学习总结一、RT-Thread二、RA2E1开发板三、学习移植RT-Thread四、学习RT-Thread设备五、其他收获六、总结【致敬未来的攻城狮计划】学习总结 &#x1f680;&#x1f680;开启攻城狮的成长之旅&#xff01;这是我参与的由 CSDN博客…

【RabbitMQ学习日记】——死信队列与延迟队列

一、死信队列 1.1 相关概念 死信&#xff0c;顾名思义就是无法被消费的消息&#xff0c;字面意思可以这样理解&#xff0c;一般来说&#xff0c;producer 将消息投递到 broker 或者直接到 queue 里了&#xff0c;consumer 从 queue 取出消息进行消费&#xff0c;但某些时候由…

云擎未来,智信天下 | 2023移动云大会来了!

新三年&#xff0c;新征程 2023年作为新三年开局之年 移动云又将以怎样的 全新品牌形象、全新战略规划 向“一流云服务商”战略目标勇毅前行&#xff1f; 答案就在这里&#xff1a; 2023移动云大会&#xff0c;官宣定档&#xff01; 2023.4.25 - 4.26 苏州金鸡湖国际会…

MATLAB配置C/C++库(Visual Studio,MinGW-w64 C/C++ 编译器)问题(包括低版本matlab配置高版本VS)

问题描述 使用matlab加载C语言的库函数时&#xff0c;需要提前配置好C/C编译器&#xff0c;否则在matlab中使用 loadlibrary 加载C /C库中的函数时候&#xff0c;会报错&#xff1a; “未找到支持的编译器或 SDK。您可以安装免费提供的 MinGW-w64 C/C 编译器&#xff1b;请参…

软考第三章 广域通信网

广域通信网 1.公共交换电话网 公共交换电话网PSTN&#xff1a;是为了话音通信而建立的网络&#xff0c;在有些地方用户仍然通过电话线拨号上网 1.1 电话系统的结构 电话系统是一个高度冗余的分级网络。用户电话通过一对铜线连接到最近的端局。 公共电话网由本地网和长途网组…

一文速学数模-最优化算法(二)梯度下降算法一文详解+Python代码

目录 前言 一、梯度下降法简述 二、梯度下降算法原理理解 1.梯度 2.梯度定义 3.梯度下降 4.损失函数(loss function) 5.学习率(步长) 三、梯度下降算法代码展示 消失和爆炸梯度 前言 最近会不断更新深度学习系列文章(全实战性可运行代码)加入到我的一文速学-数学建模…

Git项目同时推送到GitHub和Gitee详细操作

文章目录前言一、创建仓库【Create a new repository】二、初始化三、配置公钥四、密钥验证五、代码推送总结前言 将Git项目同时推送到GitHub和Gitee的好处如下&#xff1a; 提高代码可见性和协作性&#xff1a;GitHub和Gitee都是知名的代码托管平台&#xff0c;推送代码到这两…

大数据能力提升项目|学生成果展系列之五

导读为了发挥清华大学多学科优势&#xff0c;搭建跨学科交叉融合平台&#xff0c;创新跨学科交叉培养模式&#xff0c;培养具有大数据思维和应用创新的“π”型人才&#xff0c;由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…

JavaScript【十】JavaScript事件

文章目录&#x1f31f;前言&#x1f31f;事件&#x1f31f;绑定事件的方式&#xff1a;&#x1f31f;标签绑定事件&#xff1a;&#x1f31f;Document对象来绑定事件&#xff1a;on事件type&#x1f31f; 事件监听:使同一个对象的同一事件绑定多个事件处理程序。兼容IE9及以上。…

Zephyr RTOS应用开发(nrf5340)

目录 概述 开发环境安装 创建一个新的Zephyr应用 构建应用并刷写到开发板 概述 Zephyr™项目是一个采用Apache 2.0协议许可&#xff0c;Linux基金会托管的协作项目。针对低功耗、小型内存微处理器设备开发的物联网嵌入式小型、可扩展的实时操作系统&#xff0c;支持多种硬件…

redis哨兵机制详解

文章目录前言监控&#xff08;Monitoring&#xff09;自动故障转移&#xff08;Automatic failover&#xff09;配置提供者&#xff08;Configuration provider&#xff09;通知&#xff08;Notification&#xff09;哨兵集群的组建哨兵监控Redis库主库下线的判定主观下线客观下…

ORB-SLAM2原理分析

原理分析 ORB-SLAM2是一种基于单目、双目和RGB-D相机的实时视觉SLAM系统&#xff0c;用于在无GPS信号或有限的传感器信息情况下&#xff0c;构建三维地图并定位相机的位置和姿态。ORB-SLAM2采用了ORB特征点提取和描述符匹配技术&#xff0c;以及图优化和闭环检测算法&#xff…

分布式系统监控zabbix安装部署及使用

目录 一、zabbix监控 1、什么是zabbix 2、zabbix功能 3、zabbix运行机制 4、zabbix的3种架构 ①C/S架构 ②zabbix-proxy-client架构 ③master-zabbix-client架构 5、zabbix工作原理及数据走向 6、zabbix监控模式 7、zabbix部署 8、zabbix图形化页面显示设置 二、Z…