【Redis】4、全局唯一 ID生成、单机(非分布式)情况下的秒杀和一人一单

news/2024/11/20 13:38:01/

目录

  • 一、利用 Redis 实现全局唯一 ID 生成
    • (1) 为啥要用全局唯一 ID 生成
    • (2) 全局唯一 ID 生成器
    • (3) 全局 ID 的结构
    • (4) 代码实现
      • ① RedisIdWorker
      • ② Test
    • (5) 全局唯一 ID 其他生成策略
  • 二、添加优惠券
    • (1) 数据库
    • (2) 添加优惠券接口
  • 三、优惠券秒杀下单功能
    • (1) 超卖问题
    • (2) 乐观锁(版本号和 CAS)
    • (3) 乐观锁解决超卖问题
  • 四、一人一单功能【☆】
  • 五、并发情况下的线程安全问题

一、利用 Redis 实现全局唯一 ID 生成

(1) 为啥要用全局唯一 ID 生成

CREATE TABLE `tb_voucher_order` (`id` bigint(20) NOT NULL COMMENT '主键',`user_id` bigint(20) unsigned NOT NULL COMMENT '下单的用户id',`voucher_id` bigint(20) unsigned NOT NULL COMMENT '购买的代金券id',`pay_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',`status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',`pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',`use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',`refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT

🍀 id 字段不是自增 AUTO_INCREMENT


  • 每个店铺都可以发布优惠券:

在这里插入图片描述

  • 用户抢购的时候会生成订单并保存到 tb_voucher_order 这张表中
  • 如订单 id 使用数据库自增 ID 会出现以下问题:

🍀 id 规律性太明显(可能会被用户猜测到优惠券的 id)
🍀 受单表数据量的限制(优惠券订单可能很多,当分库分表的时候,每张表的 id 各自递增)

(2) 全局唯一 ID 生成器

🍀 全局 ID 生成器:一种在分布式系统下用来生成全局唯一 ID 的工具。一般要满足下列特性:

在这里插入图片描述

🍀 ① 唯一性:一个 ID 只能对应数据库中的一条记录
🍀 ② 高可用:生成 ID 的功能在高并发情况下也要能够提供服务
🍀 ③ 高性能:生成 ID 的速度要足够快(否则会影响其他业务的功能)
🍀 ④ 递增性:ID 必须递增才能让 MySQL 为表创建索引(提高数据库表查询效率的实现)
🍀 ⑤ 安全性:ID 不能过于简单,让用户猜测到

(3) 全局 ID 的结构

🍀 可使用 Redis 的 incr 实现自增
🍀 为了增加 ID 的安全性,不直接使用 Redis 自增的数值,而是拼接一些其它信息

在这里插入图片描述

ID 的组成部分:
🍀 符号位:1bit,永远为 0【ID 永远是正数】
🍀 时间戳:31bit,以为单位,可以使用69年
🍀 序列号:32bit,秒内的计数器,支持每秒产生 2^32 个不同 ID

(4) 代码实现

① RedisIdWorker

@Component
@SuppressWarnings("all")
public class RedisIdWorker {// 开始时间(秒)private static final long BEGIN_DAY_SECONDS;// 序列号的长度private static final long NO_BITS = 32;@Resourceprivate StringRedisTemplate stringRedisTemplate;static {BEGIN_DAY_SECONDS = getSecondsOfDate(2020, 5, 20, 5, 20, 20);}/*** @param idPrefix 标识 ID 是哪个业务的* @return ID 值*/public long newId(String idPrefix) {// 1.生成时间戳long seconds = getNowSeconds() - BEGIN_DAY_SECONDS;// 2.生成序列号// 2.1 当前日期String ymd = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2 使用 Redis 生成序列号Long no = stringRedisTemplate.opsForValue().increment("icrId:" + idPrefix + ":" + ymd);// 把时间戳左移32位, 空出序列号的位置return seconds << NO_BITS | no;}/*** 获取某个日期的秒数*/private static long getSecondsOfDate(int y, int month, int d, int h, int min, int sec) {LocalDateTime time = LocalDateTime.of(y, month, d, h, min, sec);return time.toEpochSecond(ZoneOffset.UTC);}/*** 获取此时此刻的秒数*/private static long getNowSeconds() {LocalDateTime curTime = LocalDateTime.now();return curTime.toEpochSecond(ZoneOffset.UTC);}
}

② Test

@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate RedisIdWorker redisIdWorker;// 线程池private ExecutorService executorService = Executors.newFixedThreadPool(520);// 计数器private CountDownLatch latch = new CountDownLatch(300);@Testpublic void testReidIdWorker() throws InterruptedException {Runnable task = () -> {for (int i = 0; i < 100; i++) {long orderId = redisIdWorker.newId("order");System.out.println("orderId = " + orderId);}latch.countDown(); // 任务执行完就递减};long begin = System.currentTimeMillis();for (int i = 0; i < 300; i++) {executorService.submit(task);}latch.await();long end = System.currentTimeMillis();System.out.println("duration: " + (end - begin));}}

(5) 全局唯一 ID 其他生成策略

全局唯一ID生成策略:
🍀 UUID
🍀 Redis 自增
🍀 snowflake 算法
🍀 数据库自增(专门用一张表自增 ID)

Redis 自增 ID 策略:
🍀 每天一个 key,方便统计订单量
🍀 ID 结构:时间戳 + 计数器

二、添加优惠券

(1) 数据库

普通券表:

CREATE TABLE `tb_voucher` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`shop_id` bigint(20) unsigned DEFAULT NULL COMMENT '商铺id',`title` varchar(255) NOT NULL COMMENT '代金券标题',`sub_title` varchar(255) DEFAULT NULL COMMENT '副标题',`rules` varchar(1024) DEFAULT NULL COMMENT '使用规则',`pay_value` bigint(10) unsigned NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',`actual_value` bigint(10) NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',`type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '0,普通券;1,秒杀券',`status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '1,上架; 2,下架; 3,过期',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT

```秒杀券表:`

CREATE TABLE `tb_seckill_voucher` (`voucher_id` bigint(20) unsigned NOT NULL COMMENT '关联的优惠券的id',`stock` int(8) NOT NULL COMMENT '库存',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`begin_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',`end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '失效时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='秒杀优惠券表,与优惠券是一对一关系'

(2) 添加优惠券接口

📗 优惠券(或秒杀券)增加完毕后会返回券的 ID

    @Override@Transactionalpublic 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);}
{"actualValue": 10000,"rules": "全场通用\\n无需预约\\n可无限叠加\\n不兑现、不找零七\\n仅堂食","updateTime": "2022-05-02T10:10:10","title": "100元代金券(大优惠)","type": 1,"payValue": 8000,"subTitle": "错过再等一年","createTime": "2022-05-10T10:10:10","id": 1,"shopId": 1,"beginTime": "2023-08-20T10:10:10","endTime": "2023-08-21T23:10:10","stock": 100,"status": 1
}

三、优惠券秒杀下单功能

📖 ① 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
📖 ② 库存是否充足,库存不足则无法下单

在这里插入图片描述

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactional // 事务public Result seckillVoucher(Long voucherId) {SeckillVoucher voucherById = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始或结束LocalDateTime beginTime = voucherById.getBeginTime();LocalDateTime endTime = voucherById.getEndTime();LocalDateTime nowTime = LocalDateTime.now();if (nowTime.isBefore(beginTime)) {return Result.fail("秒杀未开始(no start)");}if (nowTime.isAfter(endTime)) {return Result.fail("秒杀已结束(finish)");}// 判断库存是否充足Integer stock = voucherById.getStock();if (stock < 1) {return Result.fail("库存不足");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (success) {VoucherOrder voucherOrder = new VoucherOrder();long seckillOrderId = redisIdWorker.newId("seckillOrder");voucherOrder.setId(seckillOrderId); // 订单 IDvoucherOrder.setUserId(UserHolder.getUser().getId()); // 用户 IDvoucherOrder.setVoucherId(voucherId); // 优惠券 IDif (save(voucherOrder)) {return Result.ok(seckillOrderId);}return Result.fail("服务器忙, 请稍后再秒杀下单");}return Result.fail("库存不足");}}

(1) 超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁: 超卖问题:

悲观锁
📖 认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行
📖 如 Synchronized、Lock 都属于悲观锁

乐观锁
📖 认为线程安全问题不一定会发生,因此不加锁
📖 在更新数据时去判断有没有其它线程对数据做了修改。
📖 如果没有修改则认为是安全的,自己才更新数据
📖 如果已经被其它线程修改,说明发生了安全问题,此时可以重试或异常

(2) 乐观锁(版本号和 CAS)

乐观锁的关键是判断之前查询得到的数据是否有被修改过

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(3) 乐观锁解决超卖问题

在这里插入图片描述

四、一人一单功能【☆】

📖 修改秒杀业务,要求同一个优惠券,同一个用户只能下一单

在这里插入图片描述

在这里插入图片描述

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {SeckillVoucher voucherById = seckillVoucherService.getById(voucherId);// 判断秒杀是否开始或结束LocalDateTime beginTime = voucherById.getBeginTime();LocalDateTime endTime = voucherById.getEndTime();LocalDateTime nowTime = LocalDateTime.now();if (nowTime.isBefore(beginTime)) {return Result.fail("秒杀未开始(no start)");}if (nowTime.isAfter(endTime)) {return Result.fail("秒杀已结束(finish)");}// 判断库存是否充足Integer stock = voucherById.getStock();if (stock < 1) {return Result.fail("库存不足");}// 一人一单Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 获取事务的代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(userId, voucherId);}}@Transactional // 事务public Result createVoucherOrder(Long userId, Long voucherId) {Integer 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").eq("voucher_id", voucherId).ge("stock", 0) // 保证库存大于零(CAS 乐观锁).update();if (success) {VoucherOrder voucherOrder = new VoucherOrder();long seckillOrderId = redisIdWorker.newId("seckillOrder");voucherOrder.setId(seckillOrderId); // 订单 IDvoucherOrder.setUserId(userId); // 用户 IDvoucherOrder.setVoucherId(voucherId); // 优惠券 IDif (save(voucherOrder)) {return Result.ok(seckillOrderId);}return Result.fail("服务器忙, 请稍后再秒杀下单");}return Result.fail("库存不足");}}

五、并发情况下的线程安全问题

在这里插入图片描述

在这里插入图片描述

worker_processes  1;events {worker_connections  1024;
}http {include       mime.types;default_type  application/json;sendfile        on;keepalive_timeout  65;server {listen       8080;server_name  localhost;# 指定前端项目所在的位置location / {root   html/hmdp;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}location /api {  default_type  application/json;#internal;  keepalive_timeout   30s;  keepalive_requests  1000;  #支持keep-alive  proxy_http_version 1.1;  rewrite /api(/.*) $1 break;  proxy_pass_request_headers on;#more_clear_input_headers Accept-Encoding;  proxy_next_upstream error timeout;  #proxy_pass http://127.0.0.1:8081;proxy_pass http://backend;}}upstream backend {server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;}  
}

在这里插入图片描述

在这里插入图片描述

集群模式下,每个 JVM 都有自己的锁监视器
每个 JVM 的锁监视器互相不可见


http://www.ppmy.cn/news/834973.html

相关文章

在 TypeScript 中 interface 和 type 的区别

在 TypeScript 中&#xff0c;interface 和 type 都用于定义自定义类型&#xff0c;但它们有一些区别&#xff1a; 语法风格&#xff1a;interface 使用关键字 interface 开头&#xff0c;而 type 使用关键字 type 开头。例如&#xff1a; interface Person {name: string;age:…

4k对齐 diskgenius修复分区表 ubuntu安装

最近被500G 日历硬盘折腾了很久&#xff0c;今天终于解决问题。 问题起源&#xff1a; 前两天打算从fedora转向ubuntu。原来的fedora装在硬盘的最后一个分区&#xff08;G盘&#xff0c;为一主分区&#xff09;&#xff0c;可万万没想到&#xff0c;当我删除该分区的逻辑驱动器…

磁盘分区4K未对齐的解决方案

磁盘分区4K未对齐的解决方案 后续在遇到用户反馈电脑运行慢&#xff0c;不区分机型&#xff0c;先使用 AS SSD benchmark工具&#xff08;请查看附件&#xff09;检测分区对齐是否正常&#xff0c;华为办公区访问服务器\\szxems12-fs\drivers_for_IT下载工具。 如果显示为10…

怎么看ssd有没有4k对齐?3分钟包教包会!

固态硬盘成为许多电脑配置不够的标配&#xff0c;但是许多小伙伴告诉快启动小编&#xff1a;自己的电脑装上了固态硬盘之后并没有太大的变化。其实大家在使用固态硬盘是有盲区的&#xff0c;首先我们需要将其设置为4k对齐才能发挥到最佳性能&#xff0c;所以大家在分区时一定要…

4K 对齐与固态硬盘检测工具

0. 硬盘扇区 当前电脑传统机械硬盘的每个扇区一般大小为 512 字节&#xff08;512B&#xff09;&#xff1b;当使用某一文件系统将硬盘格式化时&#xff0c;文件系统会将硬盘扇区、磁道与柱面统计整理并定义一个簇为多少扇区方便快速存储。 现时 windows 中常见使用的 NTFS 文…

[Windows] 4k对齐(无损对齐) [ 技术分享 ]

4K对齐介绍&#xff1a; 点击查看 4K对齐检查&#xff1a; 点击查看 4K对齐操作&#xff1a; 一是“无损对齐”&#xff1a;无损就是不需要重新格式化磁盘、重新分区&#xff0c;免去重装系统、备份的麻烦。 二是“有损对齐”&#xff1a;有损就是需要重新分区、划分磁盘、备份…

Keil代码一键对齐工具

1. 下载AStyle工具2.Keil中配置3.效果展示4.扩展参数4.1 只格式化当前文件4.2 格式化整个工程4.3 参数说明 1. 下载AStyle工具 下载链接。 下载后将其放在合适的位置&#xff0c;不用安装。我放在了keil安装目录下。 2.Keil中配置 打开tools下的Customize Tools Menu。添加新…

linux ssd 4k对齐工具下载,4k对齐检测工具(as ssd benchmark)

AS SSD Benchmark是一个SSD(固态硬盘)的传输速度测试工具&#xff0c;AS SSD Benchmark软件操作简单&#xff0c;我们下载解压后直接打开就可以使用。AS SSD Benchmark软件功能也很强大&#xff0c;可以帮助大家测试固态硬盘持续读、写等的性能&#xff0c;软件体积小巧&#x…