Redis - 实战之 全局 ID 生成器 RedisIdWorker
概述
-
定义
:一种分布式系统下用来生成全局唯一 ID 的工具 -
特点
- 唯一性,满足优惠券需要唯一的 ID 标识用于核销
- 高可用,随时能够生成正确的 ID
- 高性能,生成 ID 的速度很快
- 递增性,生成的 ID 是逐渐变大的,有利于数据库形成索引
- 安全性,生成的 ID 无明显规律,可以避免间接泄露信息
- 生成量大,可满足优惠券订单数据量大的需求
-
ID 组成部分
- 符号位:1bit,永远为0
- 时间戳:31bit,以秒为单位,可以使用69年
- 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
代码实现
-
目标
:手动实现一个简单的全局 ID 生成器 -
实现流程
- 创建生成器:在 utils 包下创建 RedisIdWorker 类,作为 ID 生成器
- 创建时间戳:创建一个时间戳,即 RedisId 的高32位
- 获取当前日期:创建当前日期对象 date,用于自增 id 的生成
- count:设置 Id 格式,保证 Id 严格自增长
- 拼接 Id 并将其返回
-
代码实现
@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 工具类
定义
:信号枪,用于同步多线程的等待与唤醒功能
:- 同步多线程的等待与唤醒
- 在异步程序中,确保分线程全部走完之后,主线程再继续往下执行
- (如果不用 countdownlatch 则可能分线程还没结束时主线程已经执行完毕)
常用方法
- await:阻塞方法,用于主线程中,可以让 main 线程阻塞,直至 CountDownLatch 内部维护的变量为 0 时再放行
- countDown:计数操作,用于分线程中,可以让 CountDownLatch 内部变量 -1 操作
二、ExecutorService & Executors
-
定义
:Java JDK 提供的接口类 -
功能
- 简化异步模式下任务的执行
- 自动提供线程池和相关 API,执行 Runnable 和 Callable 方法
-
常用方法
方法 说明 Executors.newFixedThreadPool(xxxThreads) Executors 提供的工厂方法,用于创建 ExecutorService 实例 execute(functionName) 调用线程执行 functionName 任务,无返回值 ⭐ submit(functionName) 调用线程执行 functionName 任务,返回一个 Future 类 invokeAny(functionName) 调用线程执行一组 functionName 任务,返回首成功执行的任务的结果 invokeAll(functionName) 调用线程执行一组 functionName 任务,返回所有任务执行的结果 ⭐ shutdown() 停止接受新任务,并在所有正在运行的线程完成当前工作后关闭 ⭐ awaitTermination() 停止接受新任务,在指定时间内等待所有任务完成 -
参考资料:一文秒懂 Java ExecutorService
-
代码实现
- 目标:测试 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)); // 打印任务执行的总耗时 }
超卖问题
一、乐观锁
-
定义
:不加锁,在更新时判断是否有其他线程修改过数据 -
优点
:性能较高 -
常见的乐观锁
:CAS (Compare and Swap) -
添加库存判断
(分布式环境下仍然存在超卖问题)boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update().gt("stock", 0);
二、悲观锁
定义
:添加同步锁,使线程串行执行优点
:实现简单缺点
:性能一般
一人一单问题
一、单服务器系统解决方案
需求
:每个人只能抢购一张大额优惠券,避免同一用户购买多张优惠券重点
- 事务:库存扣减操作必须在事务中执行
- 粒度:事务粒度必须够小,避免影响性能
- 锁:事务开启时必须确保拿到当前下单用户的订单,并依据用户 Id 加锁
- 找到事务的代理对象,避免 Spring 事务注解失效 (需要给启动类加
@EnableAspectJAutoProxy(exposeProxy = true)
注解)
实现逻辑
- 获取优惠券 id、当前登录用户 id
- 查询数据库的优惠券表(voucher_order)
- 如果存在优惠券 id 和当前登录用户 id 都匹配的 order 则拒绝创建订单,返回 fail()
- 如果不存在则创建订单 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 脚本
RedisScript 接口
:用于绑定一个具体的 Lua 脚本DefaultRedisScript 实现类
-
定义:RedisScript 接口的实现类
-
功能:提前加载 Lua 脚本
-
示例
// 创建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 脚本
调用Lua脚本 API
:StringRedisTemplate.execute( RedisScript<T> script, List<K> keys, Object… args )示例
-
执行 ”下单脚本” (此时不需要 key,因为下单时只需要用 userId 和 voucherId 查询是否有锁)
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT, // 要执行的脚本Collections.emptyList(), // KEYvoucherId.toString(), userId.toString(), String.valueOf(orderId) // VALUES );
-
执行 “unlock脚本”
-
实战:添加优惠券 & 单服务器创建订单
添加优惠券
目标
:商家在主页上添加一个优惠券的抢购链接,可以点击抢购按钮抢购优惠券
一、普通优惠券
-
定义
:日常可获取的资源 -
代码实现
@PostMapping public Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId()); }
二、限量优惠券
定义
:限制数量,需要设置时间限制、面对高并发请求的资源下单流程
- 查询优惠券:通过 voucherId 查询优惠券
- 时间判断:判断是否在抢购优惠券的固定时间范围内
- 库存判断:判断优惠券库存是否 ≥ 1
- 扣减库存
- 创建订单:创建订单 VoucherOrder 对象,指定订单号,指定全局唯一 voucherId,指定用户 id
- 保存订单:保存订单到数据库
- 返回结果:Result.ok(orderId)
代码实现
-
VoucherController
@PostMapping("seckill") public Result addSeckillVoucher( @RequestBody Voucher voucher ){voucherService.addSeckillVoucher(voucher);return Result.o(voucher.getId()); }
-
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()); }
-
(缺陷) 优惠券下单功能
一、功能说明
目标
:用户抢购代金券,保证用户成功获得优惠券,保证效率并且避免超卖工作流程
- 提交优惠券 ID
- 查询优惠券信息 (下单时间是否合法,下单时库存是否充足)
- 扣减库存,创建订单
- 返回订单 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
概述 定义:一种分布式系统下用来生成全局唯一 ID 的工具 特点 唯一性,满足优惠券需要唯一的 ID 标识用于核销高可用,随时能够生成正确的 ID高性能,生成 ID 的速度很快递增性,生成的 ID 是逐渐变大的,有利于…...

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

在服务器自主选择GPU使用
比如说,程序使用第 2 张显卡(从 0 开始计数)。它的作用是告诉系统和深度学习框架(如 PyTorch 或 TensorFlow)只可见某些 GPU。 export CUDA_VISIBLE_DEVICES1 然后再查看当前使用的显卡: echo $CUDA_VIS…...
【设计模式】享元模式(Flyweight Pattern)
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享尽可能多的对象来有效支持大量细粒度的对象。这个模式主要用于减少内存使用和提高性能,特别是在需要创建大量相似对象的场景中。享元模式的核心思想是将对象的状态分为…...

题目 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 代码和手动设置参数以及获取结果集的操作,使开发人员能够更专注于 SQL 语句的编写和业务逻辑的处理。 2. 核心组件 SqlSessionFactoryB…...

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

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

AI大模型学习笔记|多目标算法梳理、举例
多目标算法学习内容推荐: 1.通俗易懂讲算法-多目标优化-NSGA-II(附代码讲解)_哔哩哔哩_bilibili 2.多目标优化 (python pyomo pareto 最优)_哔哩哔哩_bilibili 学习笔记: 通过网盘分享的文件:多目标算法学习笔记 链接: https://pan.baidu.com…...
蓝桥杯刷题——day3
蓝桥杯刷题——day3 题目一题干题目解析代码 题目二题干题目解析代码 题目一 题干 每张票据有唯一的 ID 号,全年所有票据的 ID 号是连续的,但 ID 的开始数码是随机选定的。因为工作人员疏忽,在录入 ID 号的时候发生了一处错误,造…...

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

【开源项目】经典开源项目数字孪生体育馆—开源工程及源码
飞渡科技数字孪生体育馆管理平台,融合物联网IOT、BIM数据模型、三维GIS等技术,实现体育馆的全方位监控和实时全局掌握,同时,通过集成设备设施管理、人员管理等子系统,减少信息孤岛,让场馆“可视、可控、可管…...

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

解决MAC装win系统投屏失败问题(AMD显卡)
一、问题描述 电脑接上HDMI线后,电脑上能显示有外部显示器接入,但是外接显示器无投屏画面 二、已测试的方法 1 更改电脑分辨,结果无效 2 删除BootCamp,结果无效 3更新电脑系统,结果无效 4 在设备管理器中&#…...
网易游戏分享游戏场景中MongoDB运行和分析实践
在游戏行业中,数据库的稳定和性能直接影响了游戏质量和用户满意度。在竞争激烈的游戏市场中,一个优秀的数据库产品无疑能为游戏的开发和后期的运营奠定良好的基础。伴随着MongoDB在不同类型游戏场景中的应用越来越广泛,许多知名的游戏公司都在…...
Android14 AOSP 允许system分区和vendor分区应用进行AIDL通信
在Android14上,出于种种原因,system分区的应用无法和vendor分区的应用直接通过AIDL的方法进行通信,但是项目的某个功能又需要如此。 好在Binder底层其实是支持的,只是在上层进行了屏蔽。 修改 frameworks/native/libs/binder/Bp…...

R学习——因子
目录 1 定义因子(factor函数) 2因子的作用 一个数据集中的 只需要考虑可以用哪个数据来进行分类就可以了,可以用来分类就可以作为因子。 Cy1这个因子对应的水平level是4 6 8: 1 定义因子(factor函数) 要…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...

初学 pytest 记录
安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

免费数学几何作图web平台
光锐软件免费数学工具,maths,数学制图,数学作图,几何作图,几何,AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...

【UE5 C++】通过文件对话框获取选择文件的路径
目录 效果 步骤 源码 效果 步骤 1. 在“xxx.Build.cs”中添加需要使用的模块 ,这里主要使用“DesktopPlatform”模块 2. 添加后闭UE编辑器,右键点击 .uproject 文件,选择 "Generate Visual Studio project files",重…...
命令行关闭Windows防火墙
命令行关闭Windows防火墙 引言一、防火墙:被低估的"智能安检员"二、优先尝试!90%问题无需关闭防火墙方案1:程序白名单(解决软件误拦截)方案2:开放特定端口(解决网游/开发端口不通)三、命令行极速关闭方案方法一:PowerShell(推荐Win10/11)方法二:CMD命令…...