每个商户都可以发布优惠券,分为普通券和秒杀券。普通券优惠力度较低,所以可以任意购买;而秒杀券优惠力度较大,需要限时限量抢购。例如:
4.3.1 数据表与实体类
在数据库中,tb_voucher表用于保存优惠券的信息,包括优惠券的基本信息、优惠金额、使用规则等:
tb_seckill_voucher表用于保存秒杀券的扩展信息,包括秒杀券的库存、开始抢购时间、结束抢购时间等:
tb_voucher_order表用于保存优惠券购买订单信息,包括购买用户的ID、优惠券的ID、购买时间等:
根据以上三个数据表在项目中创建三个对应的实体类,如下:
// tb_voucher、tb_seckill_voucher
// com.star.redis.dzdp.pojo.Voucher
/***
* 优惠券
* @author hsgx
* @since 2024/4/5 10:26
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_voucher")
public class Voucher implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商铺id
*/
private Long shopId;
/**
* 代金券标题
*/
private String title;
/**
* 副标题
*/
private String subTitle;
/**
* 使用规则
*/
private String rules;
/**
* 支付金额
*/
private Long payValue;
/**
* 抵扣金额
*/
private Long actualValue;
/**
* 优惠券类型
*/
private Integer type;
/**
* 优惠券类型
*/
private Integer status;
/**
* 库存
*/
@TableField(exist = false)
private Integer stock;
/**
* 生效时间
*/
@TableField(exist = false)
private Date beginTime;
/**
* 失效时间
*/
@TableField(exist = false)
private Date endTime;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
// tb_seckill_voucher
// com.star.redis.dzdp.pojo.SeckillVoucher
/***
* 秒杀优惠券表
* @author hsgx
* @since 2024/4/5 10:26
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_seckill_voucher")
public class SeckillVoucher implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 关联的优惠券的id
*/
@TableId(value = "voucher_id", type = IdType.INPUT)
private Long voucherId;
/**
* 库存
*/
private Integer stock;
/**
* 创建时间
*/
private Date createTime;
/**
* 生效时间
*/
private Date beginTime;
/**
* 失效时间
*/
private Date endTime;
/**
* 更新时间
*/
private Date updateTime;
}
// tb_voucher_order
// com.star.redis.dzdp.pojo.VoucherOrder
/***
* 优惠券订单
* @author hsgx
* @since 2024/4/5 10:30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_voucher_order")
public class VoucherOrder implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.INPUT)
private Long id;
/**
* 下单的用户id
*/
private Long userId;
/**
* 购买的代金券id
*/
private Long voucherId;
/**
* 支付方式 1:余额支付;2:支付宝;3:微信
*/
private Integer payType;
/**
* 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款
*/
private Integer status;
/**
* 下单时间
*/
private Date createTime;
/**
* 支付时间
*/
private Date payTime;
/**
* 核销时间
*/
private Date useTime;
/**
* 退款时间
*/
private Date refundTime;
/**
* 更新时间
*/
private Date updateTime;
}
4.3.2 添加优惠券
4.3.2.1 添加普通券代码
- 1)接口文档
项目 | 说明 |
---|---|
请求方法 | POST |
请求路径 | /voucher/add |
请求参数 | Voucher |
返回值 | 无 |
- 2)代码实现
首先为普通优惠券业务创建对应的VoucherController类-IVoucherService接口-VoucherServiceImpl实现类-VoucherMapper类;为秒杀优惠券业务创建对应的ISeckillVoucherService接口-SeckillVoucherServiceImpl实现类-SeckillVoucherMapper类。详见测试项目代码。
然后在VoucherController类中编写一个add()
方法,用于新增普通优惠券:
// com.star.redis.dzdp.controller.VoucherController
@Resource
private IVoucherService voucherService;
/**
* 添加普通优惠券
* @author hsgx
* @since 2024/4/5 10:43
* @param voucher
* @return com.star.redis.dzdp.pojo.BaseResult
*/
@PostMapping("/add")
public BaseResult add(@RequestBody Voucher voucher) {
log.info("add {}", voucher.toString());
voucherService.save(voucher);
log.info("add success. id = {}", voucher.getId());
return BaseResult.setOk("添加普通优惠券成功");
}
- 3)功能测试
编写完成后,调用/voucher/add
接口,新增一个普通优惠券:
控制台打印信息如下:
add Voucher(id=null, shopId=1, title=500元代金券, subTitle=周一至周日均可使用, rules=全场通用 无需预约 可无限叠加 不兑现、不找零 仅限堂食, payValue=45000, actualValue=50000, type=null, status=null, stock=null, beginTime=null, endTime=null, createTime=null, updateTime=null)
==> Preparing: INSERT INTO tb_voucher ( shop_id, title, sub_title, rules, pay_value, actual_value ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: 1(Long), 500元代金券(String), 周一至周日均可使用(String), 全场通用 无需预约 可无限叠加 不兑现、不找零 仅限堂食(String), 45000(Long), 50000(Long)
<== Updates: 1
add success. id = 10
4.3.2.2 添加秒杀券代码
- 1)接口文档
项目 | 说明 |
---|---|
请求方法 | POST |
请求路径 | /voucher/seckill/add |
请求参数 | Voucher |
返回值 | 无 |
- 2)代码实现
在VoucherController类中编写一个addSeckill()
方法,调用IVoucherService接口的addSeckillVoucher()
方法,用于新增秒杀优惠券:
// com.star.redis.dzdp.controller.VoucherController
/**
* 添加秒杀优惠券
* @author hsgx
* @since 2024/4/5 11:00
* @param voucher
* @return com.star.redis.dzdp.pojo.BaseResult
*/
@PostMapping("/seckill/add")
public BaseResult addSeckill(@RequestBody Voucher voucher) {
return voucherService.addSeckillVoucher(voucher);
}
// com.star.redis.dzdp.service.impl.VoucherServiceImpl
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public BaseResult addSeckillVoucher(Voucher voucher) {
log.info("add a seckill voucher, {}", voucher.toString());
// 1.保存优惠券信息
save(voucher);
log.info("add voucher success. id = {}", voucher.getId());
// 2.保存秒杀信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
// 3.将秒杀优惠券的库存保存到Redis
String key = "seckill:stock:" + voucher.getId();
stringRedisTemplate.opsForValue().set(key, voucher.getStock().toString());
log.info("set to Redis : Key = {}, Value = {}", key, voucher.getStock().toString());
return BaseResult.setOk("新增秒杀券成功!");
}
- 3)功能测试
编写完成后,调用/voucher/seckill/add
接口,新增一个秒杀优惠券:
控制台打印信息如下:
add a seckill voucher, Voucher(id=null, shopId=1, title=1000元代金券, subTitle=限时秒杀, rules=周一至周日均可使用, payValue=50000, actualValue=100000, type=null, status=null, stock=1000, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Fri Apr 05 18:00:00 CST 2024, createTime=null, updateTime=null)
==> Preparing: INSERT INTO tb_voucher ( shop_id, title, sub_title, rules, pay_value, actual_value ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: 1(Long), 1000元代金券(String), 限时秒杀(String), 周一至周日均可使用(String), 50000(Long), 100000(Long)
<== Updates: 1
add voucher success. id = 11
==> Preparing: INSERT INTO tb_seckill_voucher ( voucher_id, stock, begin_time, end_time ) VALUES ( ?, ?, ?, ? )
==> Parameters: 11(Long), 1000(Integer), 2024-04-05
<== Updates: 1
set to Redis : Key = seckill:stock:11, Value = 1000
此时,在数据库的tb_voucher表有2条记录,tb_seckill_voucher有1条记录:
4.3.3 实现秒杀下单
4.3.3.1 秒杀下单逻辑分析
如上图所示,当用户下单时,会提交优惠券的ID,后台根据该ID查询对应的优惠券信息,并判断秒杀是否开始,如果未开始,则直接返回错误信息;如果已经开始,则再次判断库存是否充足,如果不充足,则直接返回错误信息;如果充足,则扣减库存,并创建订单,并返回订单ID。
4.3.3.2 获取秒杀订单ID
每个商户都可以发布订单ID,并且保存到tb_voucher_order表中,如果这个表的ID使用自增ID,则是存在一些问题的:ID的规律太明显,且受到单表数据量的限制。
如果ID具有太明显的规律,用户或者商业对手就很容易猜测出商户的一些敏感信息,比如商户在一天时间内卖出了多少单,这明显不合适。
同时,随着商户规模的扩大,需要存入数据库的数据量也在变大,而MySQL的单表容量不宜超过500W。数据量过大之后,**就要进行拆库拆表,拆分之后,从逻辑上讲它们仍然是同一张表,所以ID是也不能是一样的。
综上,需要保证订单ID的唯一性。本项目使用全局ID生成器来解决这个问题。 全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具,满足唯一性、高可用、高性能、递增型和安全性等。
为了增加ID的安全性,使用以下编码方式生成全局唯一ID:
- 符号位:永远是0
- 时间戳:31bit,以秒为单位,可以使用69年
- 序列号:32bit,支持每秒产生2^32个不同的ID
下面就创建一个工具类RedisIdWorker,用于生成全局唯一ID:
// com.star.redis.dzdp.utils.RedisIdWorker
public class RedisIdWorker {
/**
* 获取全局唯一ID
* @author hsgx
* @since 2024/4/5 12:37
* @param stringRedisTemplate
* @param keyPrefix
* @return long
*/
public static long nextId(StringRedisTemplate stringRedisTemplate, String keyPrefix) {
// 1.生成时间戳
long nowSec = System.currentTimeMillis() / 1000;
// 2.生成序列号
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + new Date());
// 3.拼接并返回
return nowSec << 32 | count;
}
}
4.3.3.3 获取用户ID
在tb_voucher_order表中,有一个user_id字段,该字段保存了下单用户的ID。下单用户即当前登录的用户,因此要想办法在业务层拿到当前登录用户的ID。
我们在登录拦截器中已经拿到了当前登录用户的信息,因此可以将这里拿到的信息继续向下游传递:
// com.star.redis.dzdp.interceptor.LoginInterceptor#preHandle()
// 5.存在,放行
// 将用户ID向下游传递
request.setAttribute("userId", user.getId());
return true;
如此,后续在Controller方法中,就可以使用request.getAttribute("userId")
方法获取用户ID。
4.3.3.4 实现秒杀下单
为优惠券订单业务创建对应的IVoucherOrderService接口-VoucherOrderServiceImpl实现类-VoucherOrderMapper类。详见测试项目代码。
在IVoucherOrderService接口中定义一个seckillVoucher()
方法,并在VoucherOrderServiceImpl实现类中具体实现,作用是秒杀下单。
// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private IVoucherOrderService voucherOrderService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public BaseResult<Long> seckillVoucher(Long voucherId, Long userId) {
log.info("开始秒杀下单...voucherId = {}, userId = {}", voucherId, userId);
// 1.查询秒杀优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀活动是否开启或结束
if(seckillVoucher == null) {
// 秒杀活动不存在
return BaseResult.setFail("秒杀活动不存在!");
} else if(seckillVoucher.getBeginTime().after(new Date())) {
// 秒杀活动未开始
log.info("beginTime = {}", seckillVoucher.getBeginTime());
return BaseResult.setFail("秒杀尚未开始!");
} else if(seckillVoucher.getEndTime().before(new Date())) {
// 秒杀活动已结束
log.info("endTime = {}", seckillVoucher.getEndTime());
return BaseResult.setFail("秒杀尚已结束!");
}
log.info("{}", seckillVoucher.toString());
// 3.判断库存是否充足
if(seckillVoucher.getStock() < 1) {
// 库存不足
return BaseResult.setFail("库存不足,抢券失败!");
}
// 4.扣减库存
boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
log.info("update result = {}", update);
if(!update) {
// 扣减库存失败,返回抢券失败
return BaseResult.setFail("库存不足,抢券失败!");
}
// 5.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 5.1 设置订单ID
Long orderId = RedisIdWorker.nextId(stringRedisTemplate, "voucher_order");
log.info("get orderId = {}", orderId);
voucherOrder.setId(orderId);
// 5.2 设置用户ID
voucherOrder.setUserId(userId);
// 5.3 设置订单其他信息
voucherOrder.setVoucherId(voucherId);
voucherOrder.setPayTime(new Date());
voucherOrderService.save(voucherOrder);
// 6 返回订单ID
return BaseResult.setOkWithData(orderId);
}
接着在VoucherOrderMapper类中编写一个seckillOrder()
方法,作为秒杀下单的接口。
- 1)接口文档
项目 | 说明 |
---|---|
请求方法 | POST |
请求路径 | /voucher/seckill/order |
请求参数 | Voucher |
返回值 | 订单ID |
- 2)代码实现
/**
* 秒杀下单
* @author hsgx
* @since 2024/4/5 12:41
* @param voucherOrder
* @return com.star.redis.dzdp.pojo.BaseResult
*/
@PostMapping("/seckill/order")
public BaseResult seckillOrder(@RequestBody VoucherOrder voucherOrder, HttpServletRequest request) {
return voucherOrderService.seckillVoucher(voucherOrder.getVoucherId(), (Long)request.getAttribute("userId"));
}
- 3)功能测试
编写完成后,调用/voucher/seckill/order
接口,新增一个秒杀订单:
控制台打印信息如下:
开始秒杀下单...voucherId = 11, userId = 1012
==> Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
==> Parameters: 11(Long)
<== Total: 1
SeckillVoucher(voucherId=11, stock=999, createTime=Fri Apr 05 11:25:00 CST 2024, beginTime=Fri Apr 05 06:00:00 CST 2024, endTime=Fri Apr 05 18:00:00 CST 2024, updateTime=Fri Apr 05 12:56:15 CST 2024)
==> Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ?)
==> Parameters: 11(Long)
<== Updates: 1
update result = true
get orderId = 7354246043942256641
==> Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
==> Parameters: 7354246043942256641(Long), 1012(Long), 11(Long), 2024-04-05 13:10:40.332(Timestamp)
<== Updates: 1
至此,秒杀下单完成。
…
本节完,更多内容请查阅分类专栏:Redis从入门到精通
感兴趣的读者还可以查阅我的另外几个专栏:
- SpringBoot源码解读与原理分析(已完结)
- MyBatis3源码深度解析(已完结)
- 再探Java为面试赋能(持续更新中…)