您现在的位置是:网站首页> 编程资料编程资料

Spring Boot 整合Redis 实现优惠卷秒杀 一人一单功能_Redis_

2023-05-27 548人已围观

简介 Spring Boot 整合Redis 实现优惠卷秒杀 一人一单功能_Redis_

一、什么是全局唯一ID

⛅全局唯一ID

在分布式系统中,经常需要使用全局唯一ID查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。

全局唯一ID在数据库中一般会被设成主键,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。

这样全局唯一ID就需要保证这两个需求:

  • 全局唯一
  • 趋势有序

我们的场景是 优惠卷秒杀抢购, 当用户抢购时,就会生成订单 并保存到 数据库 的订单表中,而订单表 如果使用数据库自增ID就会存在以下问题

  • id的规律性太明显
  • 受单表数据量限制

场景分析:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。

场景分析二: 随着我们商城规模越来越大,MySQL 的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

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

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

ID的组合为

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

⚡Redis实现全局唯一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; } public long nextId(String keyPrefix) { // 1.生成时间戳 LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSecond - BEGIN_TIMESTAMP; // 2.生成序列号 // 2.1.获取当前日期,精确到天 String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); // 2.2.自增长 long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); // 3.拼接并返回 return timestamp << COUNT_BITS | count; } } 

测试存入Redis

@Autowired private RedisIdWorker redisIdWorker; private ExecutorService es = Executors.newFixedThreadPool(500); @Test public void testWorkerId() throws InterruptedException { CountDownLatch latch = new CountDownLatch(300); 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(); long end = System.currentTimeMillis(); System.out.println("times = " + (end- begin)); } 

这里用到了 CountDownlatch,简单的介绍一下:

CountDownLatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题

我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch

CountDownLatch 中有两个最重要的方法

  • countDown
  • await

await 是阻塞方法,我们担心线程没有执行完时,main线程就执行,所以可以使用await就阻塞主线程, 那么什么时候main线程不在阻塞呢? 当 CountDownLatch 内部维护的变量为0时,就不再阻塞,直接放行

什么时候 CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

二、环境准备

需要搭建登录环境,基础环境代码和sql文件均已上传 GitCode 链接:基础环境和SQL

三、实现秒杀下单

添加优惠卷

VoucherServiceImpl 核心代码

@Service public class VoucherServiceImpl extends ServiceImpl implements IVoucherService { // 该类无代码,直接MyBatis-Plus继承实现类 即可,自动完成持久化 @Autowired private ISeckillVoucherService seckillVoucherService; @Override public ResultBean> queryVoucherOfShop(Long shopId) { // 查询优惠券信息 List vouchers = getBaseMapper().queryVoucherOfShop(shopId); // 返回结果 return ResultBean.create(0, "success", vouchers); } @Override 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); } }

VoucherController 接口层

@RestController @CrossOrigin @RequestMapping("/voucher") public class VoucherController { @Autowired private IVoucherService voucherService; /** * 新增秒杀券 * @param voucher 优惠券信息,包含秒杀信息 * @return 优惠券id */ @PostMapping("seckill") public ResultBean addSeckillVoucher(@RequestBody Voucher voucher) { voucherService.addSeckillVoucher(voucher); return Result.ok(voucher.getId()); } } 

编写下单业务

VoucherOrderServiceImpl 优惠卷订单核心业务类

@Service public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService { @Autowired private ISeckillVoucherService seckillVoucherService; @Autowired private RedisIdWorker redisIdWorker; @Override @Transactional public Result seckillVoucher(Long voucherId) { //1. 查询优惠卷 SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId); //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购 if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀尚未开始!"); } //3. 判断秒杀是否结束 if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已经结束!"); } //4. 判断库存是否充足 if (seckillVoucher.getStock() < 1) { return Result.fail("库存不足!"); } Long userId = UserHolder.getUser().getId(); //5. 查询订单 //5.1 查询订单 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); //5.2 判断并返回 if (count > 0) { return Result.fail("用户已经购买过!"); } //6. 扣减库存 boolean success = seckillVoucherService.update().setSql("stock = stock -1") .eq("voucher_id", voucherId).update(); if (!success) { return Result.fail("库存不足!"); } //7. 创建订单 VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8. 返回订单id return Result.ok(orderId); } } 

VoucherOrderController 接口层

@RestController @CrossOrigin @RequestMapping("/voucher_order") public class VoucherOrderController { @Autowired private IVoucherOrderService voucherOrderService; @PostMapping("seckill/{id}") public Result seckillVoucher(@PathVariable("id") Long voucherId) { return voucherOrderService.seckillVoucher(voucherId); } }

测试抢购秒杀优惠卷

ApiFox 新增以下接口

添加秒杀卷

测试返回成功即可。

抢购秒杀优惠卷接口

测试无误,抢购成功!

四、库存超卖问题

⏳问题分析

有关超卖问题分析:在我们原有代码中是这么写的

 if (voucher.getStock() < 1) { // 库存不足 return Result.fail("库存不足!"); } //5,扣减库存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).update(); if (!success) { //扣减库存 return Result.fail("库存不足!"); } 

假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。

超卖问题是典型的多线程安全问题, 这种情况下常见的解决方案就是 加 锁:而对于加锁,我们通常有两种解决方案

悲观锁:

悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等

乐观锁:

会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,**如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,**如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas

乐观锁的典型代表:就是CAS,利用CAS进行无锁化机制加锁,varNum是操作前读取的内存值,while中的var1+var2 是预估值,如果预估

-六神源码网