↑↑↑下载测试项目原代码↑↑↑
文章目录
- 前言
- 4.3 优惠券秒杀
- 4.3.1 数据表与实体类
- 4.3.2 添加优惠券
- 4.3.2.1 添加普通券代码
- 4.3.2.2 添加秒杀券代码
- 4.3.3 实现秒杀下单
- 4.3.3.1 秒杀下单逻辑分析
- 4.3.3.2 获取秒杀订单ID
- 4.3.3.3 获取用户ID
- 4.3.3.4 实现秒杀下单
前言
Redis实战系列文章:
Redis从入门到精通(四)Redis实战(一)短信登录
Redis从入门到精通(四)Redis实战(二)商户查询缓存
4.3 优惠券秒杀
每个商户都可以发布优惠券,分为普通券和秒杀券。普通券优惠力度较低,所以可以任意购买;而秒杀券优惠力度较大,需要限时限量抢购。例如:
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.将秒杀优惠券的库存保存到RedisString 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.RedisIdWorkerpublic 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 设置订单IDLong orderId = RedisIdWorker.nextId(stringRedisTemplate, "voucher_order");log.info("get orderId = {}", orderId);voucherOrder.setId(orderId);// 5.2 设置用户IDvoucherOrder.setUserId(userId);// 5.3 设置订单其他信息voucherOrder.setVoucherId(voucherId);voucherOrder.setPayTime(new Date());voucherOrderService.save(voucherOrder);// 6 返回订单IDreturn 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为面试赋能(持续更新中…)