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函数) 要…...
pytest入门三:setup、teardown
https://zhuanlan.zhihu.com/p/623447031 function对应类外的函数,每个函数调用一次 import pytest def setup_module():print(开始 module)def teardown_module():print(结束 module)def setup_function():print(开始 function)def teardown_function():print(结…...
前端面试准备问题2
1.防抖和节流分别是什么,应用场景 防抖:在事件被触发后,只有在指定的延迟时间内没有再次触发,才执行事件处理函数。 在我的理解中,简单的说就是在一个指定的时间内,仅触发一次,如果有多次重复触…...
web前端sse封装
这是一个基于microsoft/fetch-event-source包封装的sse函数,包含开始、停止功能; 可传更多参数、使用非常简单。 使用前: 安装 microsoft/fetch-event-source 代码: // sse import { fetchEventSource } from microsoft/fetch-event-source import { …...
智能家居WTR096-16S录放音芯片方案,实现语音播报提示及录音留言功能
前言: 在当今社会的高速运转之下,夜幕低垂之时,许多辛勤工作的父母尚未归家。对于肩负家庭责任的他们而言,确保孩童按时用餐与居家安全成为心头大事。此时,家居留言录音提示功能应运而生,恰似家中的一位无形…...
【创建模式-蓝本模式(Prototype Pattern)】
目录 Overview应用场景代码演示JDK Prototype pattern 更优实践泛型克隆接口 https://doc.hutool.cn/pages/Cloneable/#%E6%B3%9B%E5%9E%8B%E5%85%8B%E9%9A%86%E7%B1%BB The prototype pattern is a creational design pattern in software development. It is used when the t…...
Spring Boot应用开发深度解析与实战案例
Spring Boot应用开发深度解析与实战案例 在当今快速发展的软件开发领域,Spring Boot凭借其“约定优于配置”的理念,极大地简化了Java应用的开发、配置和部署过程,成为了微服务架构下不可或缺的技术选型。本文将深入探讨Spring Boot的核心特性、最佳实践,并通过一个具体的…...
优化Go语言中的网络连接:设置代理超时参数
网络连接优化的重要性 在分布式系统和微服务架构中,网络请求的效率直接影响到整个系统的响应速度。合理的超时设置可以防止系统在等待网络响应时陷入无限期的阻塞,从而提高系统的吞吐量和用户体验。特别是在使用代理服务器时,由于增加了网络…...
《神经网络与深度学习》(邱锡鹏) 内容概要【不含数学推导】
第1章 绪论 基本概念:介绍了人工智能的发展历程及不同阶段的特点,如符号主义、连接主义、行为主义等。还阐述了深度学习在人工智能领域的重要地位和发展现状,以及其在图像、语音、自然语言处理等多个领域的成功应用。术语解释 人工智能&…...
原创 传奇996_55——后端如何点击npc隐藏主界面
点击图片退出,举例: |linkexit Img|ax0.5|ay0.5|percentx50|percenty50|imgpublic/touming2.png|hideMain1|linkexit <Img|x0|y0|esc1|show4|bg1|move0|imgcustom/new/longhun/bg.png|loadDelay0|reset1|hideMain1>...
RabbitMQ中的Work Queues模式
在现代分布式系统中,消息队列(Message Queue)是实现异步通信和解耦系统的关键组件之一。RabbitMQ 是一个广泛使用的开源消息代理软件,支持多种消息传递模式。其中,Work Queues(工作队列)模式是一…...
培训学校如何做网站宣传/最近国际新闻
最初的,是机器语言,它是直接面向计算机底层的语言,就是笨拙的机器能直接识别的语言。 然后是汇编,它整合了1和0,将一串数字变换为一个或几个单词,算是面向对象的初步实现。 后来的B、C等语言,又…...
嘉兴丝绸大厦做网站的公司/手机导航下载2022新版
在Android中,项目目录下的res\drawable用来放置该项目的图片资源。 Android中提供了Bitmap类来获取图像文件信息,进行图像的平移、旋转及缩放等操作,并可以指定格式保存图像文件。 1.图像绘制 在绘制图像之前,需要从项目目录下的r…...
彩票网站建设教程/百度seo优化服务
静态方法: 与类无关,不能访问类里的任何属性和方法 类方法: 只能访问类变量 属性property 把一个方法变成一个静态属性 反射 getattr(obj,str) setattr(obj,str,func) hasattr(obj,str) delattr(obj,str) 异常:自定义异常 raise&a…...
公安机关网站备案怎么做/长沙专业网站制作
原文地址为: jquery 设置style:display 其实很方便的哦("#id").css(display,none); $("#id").css(display,block); 或 $("#id")[0].style.display none; $("#id")返回的是JQuery 它是个集合肯定有display属性$("#id&…...
淘宝做网站设计/长尾关键词查询工具
多屏协同是在华为笔记本上推出的一个非常实用的功能,顾名思义,就是手机,平板,电脑可以在一个屏幕上操作。而且这个功能可以将手机等其他智能设备的文件、应用直接分享到电脑上。华为的多屏协同是通过NFC功能来实现的,华…...
建设银行公积金预约网站/网络营销方式对比分析
$svcid$_GET[svcid]; //通过get获得php的url中的参数 $my_date$_POST[my_date1]; //post提交表单,my_date1是表单域的name。 if(!isset($my_date)) $_POST 变量是一个数组,内容是由 HTTP POST 方法发送的变量名称和值。$_POST 变量用于收集来自 method&q…...