当前位置: 首页 > news >正文

Redis - 实战之 全局 ID 生成器 RedisIdWorker

概述

  1. 定义:一种分布式系统下用来生成全局唯一 ID 的工具

  2. 特点

    1. 唯一性,满足优惠券需要唯一的 ID 标识用于核销
    2. 高可用,随时能够生成正确的 ID
    3. 高性能,生成 ID 的速度很快
    4. 递增性,生成的 ID 是逐渐变大的,有利于数据库形成索引
    5. 安全性,生成的 ID 无明显规律,可以避免间接泄露信息
    6. 生成量大,可满足优惠券订单数据量大的需求
  3. ID 组成部分

    1. 符号位:1bit,永远为0
    2. 时间戳:31bit,以秒为单位,可以使用69年
    3. 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

代码实现

  1. 目标:手动实现一个简单的全局 ID 生成器

  2. 实现流程

    1. 创建生成器:在 utils 包下创建 RedisIdWorker 类,作为 ID 生成器
    2. 创建时间戳:创建一个时间戳,即 RedisId 的高32位
    3. 获取当前日期:创建当前日期对象 date,用于自增 id 的生成
    4. count:设置 Id 格式,保证 Id 严格自增长
    5. 拼接 Id 并将其返回
  3. 代码实现

    @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;}// 获取下一个自动生成的 idpublic long nextId(String keyPrefix){// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;// 3.获取当前日期String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 4.获取自增长值:生成一个递增计数值。每次调用 increment 方法时,它会在这个key之前的自增值的基础上+1(第一次为0)long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 5.拼接并返回return timestamp << COUNT_BITS | count;}
    }
    

测试

一、CountDownLatch 工具类

  1. 定义:信号枪,用于同步多线程的等待与唤醒
  2. 功能
    1. 同步多线程的等待与唤醒
    2. 在异步程序中,确保分线程全部走完之后,主线程再继续往下执行
    3. (如果不用 countdownlatch 则可能分线程还没结束时主线程已经执行完毕)
  3. 常用方法
    1. await:阻塞方法,用于主线程中,可以让 main 线程阻塞,直至 CountDownLatch 内部维护的变量为 0 时再放行
    2. countDown:计数操作,用于分线程中,可以让 CountDownLatch 内部变量 -1 操作

二、ExecutorService & Executors

  1. 定义:Java JDK 提供的接口类

  2. 功能

    1. 简化异步模式下任务的执行
    2. 自动提供线程池和相关 API,执行 Runnable 和 Callable 方法
  3. 常用方法

    方法说明
    Executors.newFixedThreadPool(xxxThreads)Executors 提供的工厂方法,用于创建 ExecutorService 实例
    execute(functionName)调用线程执行 functionName 任务,无返回值
    ⭐ submit(functionName)调用线程执行 functionName 任务,返回一个 Future 类
    invokeAny(functionName)调用线程执行一组 functionName 任务,返回首成功执行的任务的结果
    invokeAll(functionName)调用线程执行一组 functionName 任务,返回所有任务执行的结果
    ⭐ shutdown()停止接受新任务,并在所有正在运行的线程完成当前工作后关闭
    ⭐ awaitTermination()停止接受新任务,在指定时间内等待所有任务完成
  4. 参考资料:一文秒懂 Java ExecutorService

  5. 代码实现

    1. 目标:测试 redisIdWorker 在高并发场景下的表现(共生成 30000 个 id)
    private ExecutorService es = Executors.newFixedThreadPool(500);     // 创建一个含有 500 个线程的线程池@Test
    void testIdWorker() throws InterruptedException {CountDownLatch latch = new CountDownLatch(300);           // 定义一个工具类,统计线程执行300次task的进度// 创建函数,供线程执行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();                                          // 主线程等待,直到 CountDownLatch 的计数归long end = System.currentTimeMillis();System.out.println("time = " + (end - begin));          // 打印任务执行的总耗时
    }
    

超卖问题

一、乐观锁

  1. 定义:不加锁,在更新时判断是否有其他线程修改过数据

  2. 优点:性能较高

  3. 常见的乐观锁:CAS (Compare and Swap)

  4. 添加库存判断 (分布式环境下仍然存在超卖问题)

    boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update().gt("stock", 0);
    

二、悲观锁

  1. 定义:添加同步锁,使线程串行执行
  2. 优点:实现简单
  3. 缺点:性能一般

一人一单问题

一、单服务器系统解决方案

  1. 需求:每个人只能抢购一张大额优惠券,避免同一用户购买多张优惠券
  2. 重点
    1. 事务:库存扣减操作必须在事务中执行
    2. 粒度:事务粒度必须够小,避免影响性能
    3. 锁:事务开启时必须确保拿到当前下单用户的订单,并依据用户 Id 加锁
    4. 找到事务的代理对象,避免 Spring 事务注解失效 (需要给启动类加 @EnableAspectJAutoProxy(exposeProxy = true) 注解)
  3. 实现逻辑
    1. 获取优惠券 id、当前登录用户 id
    2. 查询数据库的优惠券表(voucher_order)
      1. 如果存在优惠券 id 和当前登录用户 id 都匹配的 order 则拒绝创建订单,返回 fail()
      2. 如果不存在则创建订单 voucherOrder 并保存至 voucher_order 表中,返回 ok()

二、分布式系统解决方案 (通过 Lua 脚本保证原子性)

一、优惠券下单逻辑

二、代码实现 (Lua脚本)

--1. 参数列表
--1.1. 优惠券id
local voucherId = ARGV[1]
--1.2. 用户id
local userId = ARGV[2]
--1.3. 订单id
local orderId = ARGV[3]--2. 数据key
--2.1. 库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2. 订单key
local orderKey = 'seckill:order' .. voucherId--3. 脚本业务
--3.1. 判断库存是否充足 get stockKey
if( tonumber( redis.call('get', stockKey) ) <= 0 ) thenreturn 1
end
--3.2. 判断用户是否下单 SISMEMBER orderKey userId
if( redis.call( 'sismember', orderKey, userId ) == 1 ) thenreturn 2
end
--3.4 扣库存: stockKey 的库存 -1
redis.call( 'incrby', stockKey, -1 )
--3.5 下单(保存用户): orderKey 集合中添加 userId
redis.call( 'sadd', orderKey, userId )
-- 3.6. 发送消息到队列中
redis.call( 'xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId )

三、加载 Lua 脚本

  1. RedisScript 接口:用于绑定一个具体的 Lua 脚本
  2. DefaultRedisScript 实现类
    1. 定义:RedisScript 接口的实现类

    2. 功能:提前加载 Lua 脚本

    3. 示例

      // 创建Lua脚本对象
      private static final DefaultRedisScript<Long> SECKILL_SCRIPT;// Lua脚本初始化 (通过静态代码块)
      static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("/path/to/lua_script.lua"));SECKILL_SCRIPT.setResultType(Long.class);
      }
      

四、执行 Lua 脚本

  1. 调用Lua脚本 API :StringRedisTemplate.execute( RedisScript<T> script, List<K> keys, Object… args )
  2. 示例
    1. 执行 ”下单脚本” (此时不需要 key,因为下单时只需要用 userId 和 voucherId 查询是否有锁)

      Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,                                                        // 要执行的脚本Collections.emptyList(),                                               // KEYvoucherId.toString(), userId.toString(), String.valueOf(orderId)       // VALUES
      );
      
    2. 执行 “unlock脚本”


实战:添加优惠券 & 单服务器创建订单

添加优惠券

  1. 目标:商家在主页上添加一个优惠券的抢购链接,可以点击抢购按钮抢购优惠券

一、普通优惠券

  1. 定义:日常可获取的资源

  2. 代码实现

    @PostMapping
    public Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());
    }
    

二、限量优惠券

  1. 定义:限制数量,需要设置时间限制、面对高并发请求的资源
  2. 下单流程
    1. 查询优惠券:通过 voucherId 查询优惠券
    2. 时间判断:判断是否在抢购优惠券的固定时间范围内
    3. 库存判断:判断优惠券库存是否 ≥ 1
    4. 扣减库存
    5. 创建订单:创建订单 VoucherOrder 对象,指定订单号,指定全局唯一 voucherId,指定用户 id
    6. 保存订单:保存订单到数据库
    7. 返回结果:Result.ok(orderId)
  3. 代码实现
    1. VoucherController

      @PostMapping("seckill")
      public Result addSeckillVoucher( @RequestBody Voucher voucher ){voucherService.addSeckillVoucher(voucher);return Result.o(voucher.getId());
      }
      
    2. VoucherServiceImpl

      @Override
      @Transactional
      public void addSeckillVoucher(Voucher voucher) {// 保存优惠券到数据库save(voucher);// 保存优惠券信息SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存优惠券到Redis中stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
      }
      

(缺陷) 优惠券下单功能

一、功能说明

  1. 目标:用户抢购代金券,保证用户成功获得优惠券,保证效率并且避免超卖
  2. 工作流程
    1. 提交优惠券 ID
    2. 查询优惠券信息 (下单时间是否合法,下单时库存是否充足)
    3. 扣减库存,创建订单
    4. 返回订单 ID

四、代码实现

  • VoucherOrderServiceImpl (下述代码在分布式环境下仍然存在超卖问题)

    @Service
    public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService{@Resourceprivate ISeckillVoucherService seckillVoucherService;@Overridepublic Result seckillVoucher(Long voucherId) {// 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 优惠券抢购时间判断if(voucher.getBeginTime().isAfter(LocalDateTime.now) || voucher.getEndTime().isBefore(LocalDateTime.now()){return Result.fail("当前不在抢购时间!");}// 库存判断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);}}@Transactionalpublic Result createVoucherOrder(Long userId) {Long userId = UserHolder.getUser().getId();// 查询当前用户是否已经购买过优惠券int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if( count > 0 ) {return Result.fail("当前用户不可重复购买!");// !!! 实现乐观锁 !!!// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1")                       // set stock = stock - 1;.eq("voucher_id", voucherId).gt("stock", 0)        // where voucher_id = voucherId and stock > 0;.update();if(!success) {return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(redisIdWorker.nextId("order"));voucherOrder.setUserId(UserHolder.getUser().getId());voucherOrder.setVoucherId(voucherId);save(voucherOrder);// 返回订单idreturn Result.ok(orderId);
    }
    

相关文章:

Redis - 实战之 全局 ID 生成器 RedisIdWorker

概述 定义&#xff1a;一种分布式系统下用来生成全局唯一 ID 的工具 特点 唯一性&#xff0c;满足优惠券需要唯一的 ID 标识用于核销高可用&#xff0c;随时能够生成正确的 ID高性能&#xff0c;生成 ID 的速度很快递增性&#xff0c;生成的 ID 是逐渐变大的&#xff0c;有利于…...

matlab 连接远程服务器

通过matlab 控制远程服务器 查看 matlab 中 python 接口脚本 对于 matlab 2010b 兼容的 最高 Python版本是 3.10 安装 3.10 版本的Python&#xff0c;并安装 paramiko 库 pip install paramikomatlab 中设置 Python的环境 例如 pyversion&#xff08;D:/Anaconda3/python.e…...

在服务器自主选择GPU使用

比如说&#xff0c;程序使用第 2 张显卡&#xff08;从 0 开始计数&#xff09;。它的作用是告诉系统和深度学习框架&#xff08;如 PyTorch 或 TensorFlow&#xff09;只可见某些 GPU。 export CUDA_VISIBLE_DEVICES1 然后再查看当前使用的显卡&#xff1a; echo $CUDA_VIS…...

【设计模式】享元模式(Flyweight Pattern)

享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;它通过共享尽可能多的对象来有效支持大量细粒度的对象。这个模式主要用于减少内存使用和提高性能&#xff0c;特别是在需要创建大量相似对象的场景中。享元模式的核心思想是将对象的状态分为…...

题目 1688: 数据结构-字符串插入

第一种方式字符串 #include<iostream> #include<cstring> #include<algorithm> using namespace std; int main(){string s1,s2;int n;cin>>s1>>s2>>n;s1.insert(n-1,s2);cout<<s1<<endl;return 0; } 第二种方式字符数组 …...

28.攻防世界PHP2

进入场景 扫描目录 [04:12:32] 403 - 303B - /.ht_wsr.txt [04:12:32] 403 - 306B - /.htaccess.bak1 [04:12:32] 403 - 308B - /.htaccess.sample [04:12:…...

QML QT6 WebEngineView 、Echarts使用和数据交互

QML 中的 WebEngineView 是用于显示网页内容的组件,它基于 Qt WebEngine,支持现代网页渲染和与 JavaScript 的交互。它通常用来在 QML 应用中嵌入浏览器或加载在线资源。WebEngineView 可以展示 HTML、CSS、JavaScript 等网页内容,并提供多种属性和方法来控制其行为。 如下…...

SpringBoot 整合 Mail 轻松实现邮件自动推送

简单使用 1、pom 包配置 pom 包里面添加 spring-boot-starter-mail 包引用 <dependencies><dependency> <groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency> </de…...

MyBatis 核心知识与实践

一、MyBatis 概述 1. 框架简介 MyBatis 是一款支持自定义 SQL、存储过程以及高级映射的持久层框架。它避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的操作&#xff0c;使开发人员能够更专注于 SQL 语句的编写和业务逻辑的处理。 2. 核心组件 SqlSessionFactoryB…...

机器学习期末速成

文章目录 一、机器学习分类二、逻辑回归三、决策树四、集成学习算法五、支持向量机六、聚类七、特征工程和指标 文章参考自B站机器学习期末速成课 本文仅作者个人复习使用 一、机器学习分类 聚类和分类的区别&#xff1a; 分类&#xff1a;一开始就知道有哪些类别 聚类&#…...

Linux中的线程

目录 线程的概念 进程与线程的关系 线程创建 线程终止 线程等待 线程分离 原生线程库 线程局部存储 自己实现线程封装 线程的优缺点 多线程共享与独占资源 线程互斥 互斥锁 自己实现锁的封装 加锁实现互斥的原理 死锁 线程同步 线程的概念 回顾进程相关概念 …...

AI大模型学习笔记|多目标算法梳理、举例

多目标算法学习内容推荐&#xff1a; 1.通俗易懂讲算法-多目标优化-NSGA-II(附代码讲解)_哔哩哔哩_bilibili 2.多目标优化 (python pyomo pareto 最优)_哔哩哔哩_bilibili 学习笔记&#xff1a; 通过网盘分享的文件&#xff1a;多目标算法学习笔记 链接: https://pan.baidu.com…...

蓝桥杯刷题——day3

蓝桥杯刷题——day3 题目一题干题目解析代码 题目二题干题目解析代码 题目一 题干 每张票据有唯一的 ID 号&#xff0c;全年所有票据的 ID 号是连续的&#xff0c;但 ID 的开始数码是随机选定的。因为工作人员疏忽&#xff0c;在录入 ID 号的时候发生了一处错误&#xff0c;造…...

企业级日志分析系统ELK之ELK概述

ELK 概述 ELK 介绍 什么是 ELK 早期IT架构中的系统和应用的日志分散在不同的主机和文件&#xff0c;如果应用出现问题&#xff0c;开发和运维人员想排 查原因&#xff0c;就要先找到相应的主机上的日志文件再进行查找和分析&#xff0c;所以非常不方便&#xff0c;而且还涉及…...

【开源项目】经典开源项目数字孪生体育馆—开源工程及源码

飞渡科技数字孪生体育馆管理平台&#xff0c;融合物联网IOT、BIM数据模型、三维GIS等技术&#xff0c;实现体育馆的全方位监控和实时全局掌握&#xff0c;同时&#xff0c;通过集成设备设施管理、人员管理等子系统&#xff0c;减少信息孤岛&#xff0c;让场馆“可视、可控、可管…...

C++多线程实战:掌握图像处理高级技巧

文章结尾有最新热度的文章,感兴趣的可以去看看。 本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身 导读 在当今的计算世界中,…...

解决MAC装win系统投屏失败问题(AMD显卡)

一、问题描述 电脑接上HDMI线后&#xff0c;电脑上能显示有外部显示器接入&#xff0c;但是外接显示器无投屏画面 二、已测试的方法 1 更改电脑分辨&#xff0c;结果无效 2 删除BootCamp&#xff0c;结果无效 3更新电脑系统&#xff0c;结果无效 4 在设备管理器中&#…...

网易游戏分享游戏场景中MongoDB运行和分析实践

在游戏行业中&#xff0c;数据库的稳定和性能直接影响了游戏质量和用户满意度。在竞争激烈的游戏市场中&#xff0c;一个优秀的数据库产品无疑能为游戏的开发和后期的运营奠定良好的基础。伴随着MongoDB在不同类型游戏场景中的应用越来越广泛&#xff0c;许多知名的游戏公司都在…...

Android14 AOSP 允许system分区和vendor分区应用进行AIDL通信

在Android14上&#xff0c;出于种种原因&#xff0c;system分区的应用无法和vendor分区的应用直接通过AIDL的方法进行通信&#xff0c;但是项目的某个功能又需要如此。 好在Binder底层其实是支持的&#xff0c;只是在上层进行了屏蔽。 修改 frameworks/native/libs/binder/Bp…...

R学习——因子

目录 1 定义因子&#xff08;factor函数&#xff09; 2因子的作用 一个数据集中的 只需要考虑可以用哪个数据来进行分类就可以了&#xff0c;可以用来分类就可以作为因子。 Cy1这个因子对应的水平level是4 6 8&#xff1a; 1 定义因子&#xff08;factor函数&#xff09; 要…...

Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?

Golang 面试经典题&#xff1a;map 的 key 可以是什么类型&#xff1f;哪些不可以&#xff1f; 在 Golang 的面试中&#xff0c;map 类型的使用是一个常见的考点&#xff0c;其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练

前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1)&#xff1a;从基础到实战的深度解析-CSDN博客&#xff0c;但实际面试中&#xff0c;企业更关注候选人对复杂场景的应对能力&#xff08;如多设备并发扫描、低功耗与高发现率的平衡&#xff09;和前沿技术的…...

代理篇12|深入理解 Vite中的Proxy接口代理配置

在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

智能AI电话机器人系统的识别能力现状与发展水平

一、引言 随着人工智能技术的飞速发展&#xff0c;AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术&#xff0c;在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?

现有的 Redis 分布式锁库&#xff08;如 Redisson&#xff09;相比于开发者自己基于 Redis 命令&#xff08;如 SETNX, EXPIRE, DEL&#xff09;手动实现分布式锁&#xff0c;提供了巨大的便利性和健壮性。主要体现在以下几个方面&#xff1a; 原子性保证 (Atomicity)&#xff…...

push [特殊字符] present

push &#x1f19a; present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中&#xff0c;push 和 present 是两种不同的视图控制器切换方式&#xff0c;它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

STM32HAL库USART源代码解析及应用

STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...

给网站添加live2d看板娘

给网站添加live2d看板娘 参考文献&#xff1a; stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下&#xff0c;文章也主…...

Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?

Pod IP 的本质与特性 Pod IP 的定位 纯端点地址&#xff1a;Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址&#xff08;如 10.244.1.2&#xff09;无特殊名称&#xff1a;在 Kubernetes 中&#xff0c;它通常被称为 “Pod IP” 或 “容器 IP”生命周期&#xff1a;与 Pod …...