当前位置: 首页 > 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; 要…...

C++初阶-list的底层

目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

如何将联系人从 iPhone 转移到 Android

从 iPhone 换到 Android 手机时&#xff0c;你可能需要保留重要的数据&#xff0c;例如通讯录。好在&#xff0c;将通讯录从 iPhone 转移到 Android 手机非常简单&#xff0c;你可以从本文中学习 6 种可靠的方法&#xff0c;确保随时保持连接&#xff0c;不错过任何信息。 第 1…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现

摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

EtherNet/IP转DeviceNet协议网关详解

一&#xff0c;设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络&#xff0c;本网关连接到EtherNet/IP总线中做为从站使用&#xff0c;连接到DeviceNet总线中做为从站使用。 在自动…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

c#开发AI模型对话

AI模型 前面已经介绍了一般AI模型本地部署&#xff0c;直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型&#xff0c;但是目前国内可能使用不多&#xff0c;至少实践例子很少看见。开发训练模型就不介绍了&am…...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...