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

Redis优惠券秒杀企业实战_Redis_

2023-05-27 600人已围观

简介 Redis优惠券秒杀企业实战_Redis_

一、全局唯一ID

1. 全局ID生成器

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

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

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

所以tb_voucher_order表的主键不能用自增ID:

create table tb_voucher_order ( id bigint not null comment '主键' primary key, user_id bigint unsigned not null comment '下单的用户id', voucher_id bigint unsigned not null comment '购买的代金券id', pay_type tinyint(1) unsigned default 1 not null comment '支付方式 1:余额支付;2:支付宝;3:微信', status tinyint(1) unsigned default 1 not null comment '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款', create_time timestamp default CURRENT_TIMESTAMP not null comment '下单时间', pay_time timestamp null comment '支付时间', use_time timestamp null comment '核销时间', refund_time timestamp null comment '退款时间', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' ); 

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

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

D的组成部分:

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

编写全局ID生成器代码:

@Component public class RedisIdWorker { /** * 开始时间戳,以2022.1.1为基准计算时间差 */ private static final long BEGIN_TIMESTAMP = 1640995200L; /** * 序列号的位数 */ private static final int COUNT_BITS = 32; private StringRedisTemplate stringRedisTemplate; public RedisIdWorker(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 生成带有业务前缀的redis自增id * @param keyPrefix * @return */ 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.拼接并返回 // 用于是数字类型的拼接,所以不能像拼接字符串那样处理,而是通过位运算将高32位存 符号位+时间戳,低32位存 序列号 return timestamp << COUNT_BITS | count; } public static void main(String[] args) { LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0); long second = time.toEpochSecond(ZoneOffset.UTC); System.out.println(second);// 1640995200 } } 

测试全局ID生成器:

@SpringBootTest class HmDianPingApplicationTests { @Resource private RedisIdWorker redisIdWorker; private ExecutorService executorService = Executors.newFixedThreadPool(500); @Test void testIdWorker() throws InterruptedException { CountDownLatch latch = new CountDownLatch(300); // 每个线程生成100个id Runnable task = () -> { for (int i = 0; i < 100; i++) { long id = redisIdWorker.nextId("order"); System.out.println("id = " + id); } latch.countDown(); }; // 300个线程 long begin = System.currentTimeMillis(); for (int i = 0; i < 300; i++) { executorService.submit(task); } latch.await(); long end = System.currentTimeMillis(); System.out.println("time = " + (end - begin)); } } 

测试结果:

2. 全局唯一ID生成策略

  • UUID(不是递增的)
  • Redis自增
  • 雪花算法(snowflake)
  • 数据库自增(单独建一张表存自增id,分配到分库分表后的表中)

3. Redis自增ID策略

  • 以日期作为前缀的key,方便统计订单量
  • 自增ID的结构:时间戳 + 计数器

二、实现优惠券秒杀下单

1. 添加优惠券

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:

优惠券表信息:

  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等(tb_voucher表的type字段区分是普通券还是秒杀券)
  • tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间(秒杀券才需要填写这些信息),同时秒杀券拥有普通券的基本信息(秒杀券表tb_seckill_voucher的主键id绑定的是普通券表tb_voucher的id)
create table tb_voucher ( id bigint unsigned auto_increment comment '主键' primary key, shop_id bigint unsigned null comment '商铺id', title varchar(255) not null comment '代金券标题', sub_title varchar(255) null comment '副标题', rules varchar(1024) 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 default 0 not null comment '0,普通券;1,秒杀券', status tinyint(1) unsigned default 1 not null comment '1,上架; 2,下架; 3,过期', create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' ); 
create table tb_seckill_voucher ( voucher_id bigint unsigned not null comment '关联的优惠券的id' primary key, stock int(8) not null comment '库存', create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间', begin_time timestamp default CURRENT_TIMESTAMP not null comment '生效时间', end_time timestamp default CURRENT_TIMESTAMP not null comment '失效时间', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' ) comment '秒杀优惠券表,与优惠券是一对一关系'; 

2. 编写添加秒杀券的接口

主要代码:

@RestController @RequestMapping("/voucher") public class VoucherController { @Resource private IVoucherService voucherService; /** * 新增秒杀券 * @param voucher 优惠券信息,包含秒杀信息 * @return 优惠券id */ @PostMapping("seckill") public Result addSeckillVoucher(@RequestBody Voucher voucher) { voucherService.addSeckillVoucher(voucher); return Result.ok(voucher.getId()); } } 
@Service public class VoucherServiceImpl extends ServiceImpl implements IVoucherService { @Resource private ISeckillVoucherService seckillVoucherService; @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); } } 

测试添加:

测试结果:

三、实现秒杀下单

下单时需要判断两点:

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

主要代码:

@RestController @RequestMapping("/voucher-order") public class VoucherOrderController { @Resource private IVoucherOrderService voucherOrderService; @PostMapping("seckill/{id}") public Result seckillVoucher(@PathVariable("id") Long voucherId) { return voucherOrderService.seckillVoucher(voucherId); } } 
@Service public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService { @Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // 2.判断秒杀是否开始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀尚未开始!"); } // 3.判断秒杀是否已经结束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀已经结束!"); } // 4.判断库存是否充足 if (voucher.getStock() < 1) { // 库存不足 return Result.fail("库存不足!"); } //5,扣减库存 boolean success = seckillVoucherService.update()
                
                

-六神源码网