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

news2024/12/14 19:08:54

概述

  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;
        }
    
    		// 获取下一个自动生成的 id
        public 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 ) then
	return 1
end
--3.2. 判断用户是否下单 SISMEMBER orderKey userId
if( redis.call( 'sismember', orderKey, userId ) == 1 ) then
	return 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(),                                               // KEY
              voucherId.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{
    	
    	@Resource
    	private ISeckillVoucherService seckillVoucherService;
    	
    	@Override
    	public 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);
    		}
    	}
    		
    	@Transactional
    	public 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);
    		
    		// 返回订单id
    		return Result.ok(orderId);
    }
    

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

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

相关文章

arXiv-2024 | VLM-GroNav: 基于物理对齐映射视觉语言模型的户外环境机器人导航

作者&#xff1a; Mohamed Elnoor, Kasun Weerakoon, Gershom Seneviratne, Ruiqi Xian, Tianrui Guan, Mohamed Khalid M Jaffar, Vignesh Rajagopal, and Dinesh Manocha单位&#xff1a;马里兰大学学院公园分校原文链接&#xff1a;VLM-GroNav: Robot Navigation Using Phys…

华为无线AC、AP模式与上线解析(Huawei Wireless AC, AP Mode and Online Analysis)

华为无线AC、AP模式与上线解析 为了实现fit 瘦AP的集中式管理&#xff0c;我们需要统一把局域网内的所有AP上线到AC&#xff0c;由AC做集中式管理部署。这里我们需要理解CAPWAP协议&#xff0c;该协议分为两种报文&#xff1a;1、管理报文 2、数据报文。管理报文实际在抓包过程…

简单vue3前端打包部署到服务器,动态配置http请求头后端ip方法教程

vue3若依框架前端打包部署到服务器&#xff0c;需要部署到多个服务器上&#xff0c;每次打包会很麻烦&#xff0c;今天教大家一个动态配置请求头api的方法&#xff0c;部署后能动态获取(修改)对应服务器的请求ip 介绍两种方法&#xff0c;如有需要可以直接尝试步骤一&#xff…

Java-DataX 插件机制示例

示例代码 DataXPluginExample: DataX 项目的plugin 机制学习https://gitee.com/wendgit/data-xplugin-example/ 摘要 DataXPluginExample 是一个我编写的专门解读DataX插件机制的示例项目&#xff0c;旨在深入解析和掌握DataX的插件机制。本示例通过简洁明了的实现方式&#…

基于AI网关的风电系统在线监测

风力发电是典型的清洁能源之一&#xff0c;也是我国能源结构转型的重要组成。近年来我国大力发展风能、水能、光伏等清洁能源&#xff0c;加快创造人与生态友好和谐的人居社会。由于风电机组通常部署于偏远的野外&#xff0c;经常面临狂风、暴雨、日晒等严苛工作形势&#xff0…

[Unity] Text文本首行缩进两个字符

Text文本首行缩进两个字符的方法比较简单。通过代码把"\u3000\u3000"加到文本字符串前面即可。 比如&#xff1a; 效果&#xff1a; 代码&#xff1a; TMPtext1.text "\u3000\u3000" "选择动作类型&#xff1a;";

实时日志与发展:Elasticsearch 推出全新专用的 logsdb 索引模式

作者&#xff1a;来自 Elastic Mark Settle, George Kobar 及 Amena Siddiqi Elastic 最新发布的 logsdb 索引模式是专为日志管理优化的功能&#xff0c;旨在提升日志数据的存储效率、查询性能以及整体可用性。这个模式专注于满足现代日志处理需求&#xff0c;提供更高效的日志…

可视化报表如何制作?一文详解如何用报表工具开发可视化报表

在如今这个数据驱动的商业时代&#xff0c;众多企业正如火如荼地推进数字化转型&#xff0c;力求在激烈的市场竞争中占据先机。然而&#xff0c;随着业务规模的扩大和运营复杂度的提升&#xff0c;企业的数据量爆炸式增长&#xff0c;传统报表格式单一、信息呈现密集且不易解读…

在CentOS中安装和卸载mysql

在CentOS7中安装和卸载mysql 卸载mysql1、查看是否安装过mysql2、查看mysql服务状态3、关闭mysql服务4、卸载mysql相关的rpm程序5、删除mysql相关的文件6、删除mysql的配置文件my.cnf 安装mysql1、下载mysql相关的rpm程序2、检查/tmp临时目录权限3、安装mysql前的依赖检查3、安…

【EthIf-03】 EthernetInterface软件栈的文件组织结构

上图为《AUTOSAR_SWS_EthernetInterface【v2.2.0 】》给出的EthernetInterface软件栈的文件组织结构,本文主要关注arccore代码中已存在的文件的功能和作用,不知道的小伙伴可以查看🔗EthIf的文件结构中的src和inc目录下的文件有哪些: 1. 文件结构 1.1 EthIf_Cbk.h 头文…

Java基础知识(四) -- 面向对象(上)

1.概述 Java语言是一种面向对象的程序设计语言&#xff0c;而面向对象思想(OOP)是一种程序设计思想&#xff0c;在面向对象思想的指引下&#xff0c;使用Java语言去设计、开发计算机程序。这里的对象泛指现实中一切事物&#xff0c;每种事物都具备自己的属性和行为。 面向对象思…

国内Chrome浏览器下载安装教程,谷歌浏览器最新下载教程

今天主要讲解的是国内Chrome浏览器下载安装教程&#xff0c;谷歌浏览器最新下载教程&#xff0c;包括确认浏览器版本、ChromeDriver 驱动的下载&#xff0c;同理&#xff0c;这个教程同样适用于windows版本的&#xff0c;linux 版本的&#xff0c; mac 版本的。 众所周知&…

【KodExplorer】可道云KodExplorer-个人网盘安装使用

说明&#xff1a;安装kodExplorer &#xff08;不是Kodbox&#xff09;&#xff1b;Kodbox需求服务器至少2核4G内存&#xff0c;要求环境具备php/redis/mysql/。安装kodExplorer 就是比较方便简单部署&#xff0c;个人版免费。 一、安装环境需求 服务器: Windows&#xff0c;…

nVisual 定制化APP打包流程

一、下载打包软件 HBuilder X 下载地址&#xff1a;https://dcloud.io/hbuilderx.html 安装:此软件为绿色软件&#xff0c;解压即可使用。进入目录&#xff0c;双击exe启动。 此软件需要注册&#xff0c;打开时会提供跳转链接&#xff0c;通过邮箱注册账号。 注册成功后&#…

pytest -s执行的路径

pytest -s执行的路径&#xff1a; 直接写pytest -s&#xff0c;表示从当前路径下开始执行全部.py的文件。 执行具体指定文件&#xff1a;pytest -s .\testXdist\test_dandu.py 下面这样执行pytest -s 会报找不到文件或没权限访问&#xff0c; 必须要加上具体文件路径pytest -s…

Bootstrap-HTML(六)Bootstrap按钮

Bootstrap按钮与按钮组 前言一、Bootstrap按钮&#xff08;一&#xff09;、内置按钮样式&#xff08;二&#xff09;、按钮边框设置&#xff08;三&#xff09;、按钮尺寸调整&#xff08;四&#xff09;、块级按钮创建&#xff08;五&#xff09;、活动 / 禁用按钮设置 二、B…

HTMLCSS:3D卡片翻转悬停效果

这段HTML、CSS代码定义了页面的背景、卡片的3D翻转效果、内容的布局和样式&#xff0c;以及伪元素的视觉效果。通过这些样式&#xff0c;可以实现一个在鼠标悬停时翻转显示另一面内容的3D卡片。 演示效果 HTML&CSS <!DOCTYPE html> <html lang"en">…

Apache APISIX快速入门

本文将介绍Apache APISIX&#xff0c;这是一个开源API网关&#xff0c;可以处理速率限制选项&#xff0c;并且可以轻松地完全控制外部流量对内部后端API服务的访问。我们将看看是什么使它从其他网关服务中脱颖而出。我们还将详细讨论如何开始使用Apache APISIX网关。 在深入讨…

对象键值对的修改

一&#xff1a;一个对象&#xff0c;过滤掉键对应的值是空数组的键&#xff0c;保留值不是空数组的键值对 const obj {a: [1, 2, 3],b: [],c: [4, 5],d: [],e: [6] };// 过滤掉值为空数组的键值对 const filteredObj Object.fromEntries(Object.entries(obj).filter(([key, v…

【专题】2024年中国新能源汽车用车研究报告汇总PDF洞察(附原数据表)

原文链接&#xff1a; https://tecdat.cn/?p38564 本年度&#xff0c;国家及地方政府持续发力&#xff0c;推出诸多政策组合拳&#xff0c;全力推动汽车产业向更高质量转型升级&#xff0c;积极鼓励消费升级&#xff0c;并大力推行以旧换新等惠民生、促发展举措。尤为引人注目…