秒杀业务:秒杀就是在同一个时间段有大量请求争抢购买同一商品并完成交易的过程,秒杀系统本质是一个高性能、高一致、高可用的系统。
1.秒杀功需要注意的问题
- 超卖问题:多个用户同时发起对同一个商品的下单请求时,先查询商品库存,再修改商品库存,会出现资源竞争问题,导致库存的最终结果出现异常。
- 高并发的问题:秒杀活动通常会吸引大量用户同时进行抢购,这可能会导致系统并发量过高,应该减少对数据库的操作。
- 用户体验:当用户发起抢购时时,应该及时给用户反馈当前订单的一个状态。
- 限购问题:防止用户恶意抢购,可以对每个用户进行限购。
- 库存控制:对于秒杀活动的库存控制,需要保证库存的强一致性。
- 保证用户先进先出问题
2.实现思路
2.1.开启定时任务:
1.将管理员发布参与秒杀的商品存入到redis中
2.根据商品的库存数创建数量队列防止超卖
2.2在业务层 创建订单的方法里:
1.利用redis的increment计数器防止用户恶意下单
2.创建用户队列来保证用户的先进先出
3.给每个用户创建一个状态及时返回给用户,提高用户的体验
2.3在秒杀的方法里:
1.需要开启异步这是一个高并发操作
2.获取商品数量队列的信息,如果没有队列就代表商品已被抢完,改用户状态信息,返回,程序结束,否则就代表还有库存继续执行。
3.创建订单改变用户状态,判断库存队列的长度是否为0,是就去redis删除活动商品,不是0
就改redis的库存
4.下单完成发送给把订单信息发送给mq,由mq监听用户支付。可以用mq的死性队列监控用户支付情况
5.定义两个方法监控,一个监控用户抢购到商品需要删除该用户redis中的信息,一个监控死性队列,代表用户在指定的时间未支付订单,商品的信息需要回滚。
3.代码实现
商品类
/**
* @ClassName TbSeckillGoodsModel
* @Description 模型对象
* @Author xm
* @Date 2023/10/30 08:57
**/
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName("tb_seckill_goods")
public class TbSeckillGoodsModel implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long goodsId;
private Long itemId;
private String title;
private String smallPic;
private BigDecimal price;
private BigDecimal costPrice;
private String sellerId;
private Date createTime;
private Date checkTime;
private String status;
private Date startTime;
private Date endTime;
private Integer num;
private Integer stockCount;
private String introduction;
}
订单类
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName("tb_seckill_order")
public class TbSeckillOrderModel implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.UUID)
private Long id;
private Long seckillId;
private BigDecimal money;
private String userId;
private String sellerId;
private Date createTime;
private Date payTime;
private String status;
private String receiverAddress;
private String receiverMobile;
private String receiver;
private String transactionId;
}
处理时间工具类
/**
* 处理日期类型变量的工具类
* @author donghai
* @version v1.0
* @since 2017/04/20
*/
public class DateUtil {
/**
* 获取两个日期之间的日期
* @param start 开始日期
* @param end 结束日期
* @return 日期字符串格式的集合
*/
public static List<Date> getBetweenDates(Date start, Date end) {
List<Date> result = new ArrayList<Date>();
Calendar tempStart = Calendar.getInstance();
tempStart.setTime(start);
Calendar tempEnd = Calendar.getInstance();
tempEnd.setTime(end);
while (tempStart.before(tempEnd) || tempStart.equals(tempEnd)) {
result.add(tempStart.getTime());
tempStart.add(Calendar.DAY_OF_YEAR, 1);
}
return result;
}
/**
* 根据日期字符串返回日期
* @param source
* @param format
* @return
* @throws ParseException
*/
public static final Date parse(String source,String format) throws ParseException {
DateFormat df = new SimpleDateFormat(format);
return df.parse(source);
}
/**
* 根据日期获取格式化的日期字符串
* @param date
* @param format
* @return
* @throws ParseException
*/
public static final String format(Date date,String format) throws ParseException {
DateFormat df = new SimpleDateFormat(format);
return df.format(date);
}
//时间格式
public static final String PATTERN_YYYYMMDDHH = "yyyyMMddHH";
public static final String PATTERN_YYYY_MM_DDHHMM = "yyyy-MM-dd HH:mm";
/***
* 从yyyy-MM-dd HH:mm格式转成yyyyMMddHH格式
* @param dateStr
* @return
*/
public static String formatStr(String dateStr,String opattern,String npattern){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(opattern);
try {
Date date = simpleDateFormat.parse(dateStr);
simpleDateFormat = new SimpleDateFormat(npattern);
return simpleDateFormat.format(date);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
/***
* 获取指定日期的凌晨
* @return
*/
public static Date toDayStartHour(Date date){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Date start = calendar.getTime();
return start;
}
/***
* 时间增加N分钟
* @param date
* @param minutes
* @return
*/
public static Date addDateMinutes(Date date,int minutes){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minutes);// 24小时制
date = calendar.getTime();
return date;
}
/***
* 时间递增N小时
* @param hour
* @return
*/
public static Date addDateHour(Date date,int hour){
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR, hour);// 24小时制
date = calendar.getTime();
return date;
}
/***
* 获取时间菜单
* @return
*/
public static List<Date> getDateMenus(){
//定义一个List<Date>集合,存储所有时间段
List<Date> dates = getDates(12);
//判断当前时间属于哪个时间范围
Date now = new Date();
for (Date cdate : dates) {
//开始时间<=当前时间<开始时间+2小时
if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){
now = cdate;
break;
}
}
//当前需要显示的时间菜单
List<Date> dateMenus = new ArrayList<Date>();
for (int i = 0; i <5 ; i++) {
dateMenus.add(addDateHour(now,i*2));
}
return dateMenus;
}
/***
* 指定时间往后N个时间间隔
* @param hours
* @return
*/
public static List<Date> getDates(int hours) {
List<Date> dates = new ArrayList<Date>();
//循环12次
Date date = toDayStartHour(new Date()); //凌晨
for (int i = 0; i <hours ; i++) {
//每次递增2小时,将每次递增的时间存入到List<Date>集合中
dates.add(addDateHour(date,i*2));
}
return dates;
}
/***
* 时间转成yyyyMMddHH
* @param date
* @param pattern
* @return
*/
public static String data2str(Date date, String pattern){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
return simpleDateFormat.format(date);
}
}
定时任务
//定时任务注解,每30秒调用一次
@Scheduled(cron = "0/10 * * * * ?")
public void run() {
try {
System.out.println("开启定时任务....");
List<Date> dateMenus = DateUtil.getDateMenus();
for (Date date : dateMenus) {
System.out.println(date);
String extName = DateUtil.format(date, DateUtil.PATTERN_YYYYMMDDHH);
QueryWrapper<TbSeckillGoodsModel> qw = new QueryWrapper<>();
String start = DateUtil.format(date, "yyyy-MM-dd HH:mm:ss");
String end = DateUtil.format(DateUtil.addDateHour(date, 2), "yyyy-MM-dd HH:mm:ss");
Set keys = redisTemplate.boundHashOps("goods:" + extName).keys();
qw.lambda().eq(TbSeckillGoodsModel::getStatus, "1")
.eq(TbSeckillGoodsModel::getStartTime, start)
.eq(TbSeckillGoodsModel::getEndTime, end)
.gt(TbSeckillGoodsModel::getStockCount, 0);
if (keys != null && keys.size() > 0) {
qw.lambda().notIn(TbSeckillGoodsModel::getId, keys);
}
List<TbSeckillGoodsModel> list = tbSeckillGoodsService.list(qw);
if (list == null || list.size() <= 0) {
continue;
}
for (TbSeckillGoodsModel goodsModel : list) {
//商品的信息存储到redis
redisTemplate.boundHashOps("goods:" + extName).put(goodsModel.getId().toString(), goodsModel);
Long[] ids = this.ids(goodsModel.getId(), goodsModel.getStockCount());
//创建商品数量的队列
redisTemplate.boundListOps("goodsCountList:" + goodsModel.getId()).leftPushAll(ids);
}
}
} catch (ParseException e) {
e.printStackTrace();
}
}
private Long[] ids(Long id, Integer num) {
Long[] arr = new Long[num];
for (Integer i = 0; i < num; i++) {
arr[i] = id;
}
return arr;
}
服务层定义创建订单方法
@Override
public Integer create(String id, String time, String username) {
SeckillStatus seckillStatus = new SeckillStatus(id, username, time, "1");
Long userClickCount = redisTemplate.boundHashOps("userClickCount").increment(username, 1);
if (userClickCount>1){
return 2;
}
redisTemplate.boundListOps("userSeckillQueue").leftPush(seckillStatus);
redisTemplate.boundHashOps("userSeckillStatus").put(username,seckillStatus);
multiThreadingCreateOrder.asycnCreateOrder();
return 1;
}
秒杀方法
@Async
public void asycnCreateOrder() {
//获取队列中的信息
SeckillStatus userQueue = (SeckillStatus) redisTemplate.boundListOps("userSeckillQueue").rightPop();
String id = userQueue.getSkuId();
String time = userQueue.getTime();
String username = userQueue.getUsername();
//获取用户的状态, 用户状态变化
SeckillStatus userSeckillStatus = (SeckillStatus) redisTemplate.boundHashOps("userSeckillStatus").get(username);
//获取商品数量队列中的信息,进入程序内的令牌
Object obj = redisTemplate.boundListOps("goodsCountList:" + id).rightPop();
if (ObjectUtils.isEmpty(obj)) {
System.out.println("商品没有库存");
//没有秒杀到的状态变化
userSeckillStatus.setStatus("4");
redisTemplate.boundHashOps("userSeckillStatus").put(username, userSeckillStatus);
return;
}
try {
while (!lockUtil.lock("lock:" + id)) {
Thread.sleep(500);
}
//获取秒杀的商品信息
TbSeckillGoodsModel goodsModel = (TbSeckillGoodsModel) redisTemplate.boundHashOps("goods:" + time).get(id);
System.out.println("2.异步开始");
if (ObjectUtils.isEmpty(goodsModel) || goodsModel.getStockCount() <= 0) {
System.out.println("商品不存在,或者是没有库存");
//没有秒杀到的状态变化
userSeckillStatus.setStatus("4");
redisTemplate.boundHashOps("userSeckillStatus").put(username, userSeckillStatus);
return;
}
TbSeckillOrderModel orderModel = new TbSeckillOrderModel();
orderModel.setId(IdWorker.getId());
orderModel.setSeckillId(Long.parseLong(id));
orderModel.setMoney(goodsModel.getPrice());
orderModel.setUserId(username);
orderModel.setCreateTime(new Date());
orderModel.setStatus("1");
//创建用户订单
redisTemplate.boundHashOps("order").put(username, orderModel);
//状态变化成带支付状态
userSeckillStatus.setStatus("2");
redisTemplate.boundHashOps("userSeckillStatus").put(username, userSeckillStatus);
goodsModel.setStockCount(goodsModel.getStockCount() - 1);
if (redisTemplate.boundListOps("goodsCountList:" + id).size() <= 0) {
goodsModel.setStockCount(0);
//同步数据库
goodsService.updateById(goodsModel);
redisTemplate.boundHashOps("goods:" + time).delete(id);
} else {
redisTemplate.boundHashOps("goods:" + time).put(id, goodsModel);
}
rabbitTemplate.convertAndSend(MQConfig.oneSeckillExchange, MQConfig.oneSeckillRouting, JSON.toJSONString(userQueue), new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockUtil.unlock("lock:" + id);
}
System.out.println("3.异步结束");
}
加锁
@Component
public class LockUtil {
@Autowired
private RedisTemplate redisTemplate;
public boolean lock(String key) {
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
RedisConnection connection = connectionFactory.getConnection();
byte[] keyBytes = key.getBytes();
byte[] valBytes = "lock".getBytes();
Boolean aBoolean = connection.setNX(keyBytes, valBytes);
if (aBoolean) {
//防止出现死锁
connection.expire(keyBytes, 60);
}
return aBoolean;
}
public void unlock(String key) {
redisTemplate.delete(key);
}
}
mq监控用户支付情况
@Component
public class OrderMessage {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TbSeckillOrderService tbSeckillOrderService;
@Autowired
private TbSeckillGoodsService tbSeckillGoodsService;
@RabbitListener(queues = "seckillPaySuccessQu")
public void paySuccessMsg(Map<String, String> map) {
String name = map.get("attach").split("-")[1];
TbSeckillOrderModel order = (TbSeckillOrderModel) redisTemplate.boundHashOps("order").get(name);
order.setStatus("2");
tbSeckillOrderService.save(order);
redisTemplate.boundHashOps("order").delete(name);
redisTemplate.boundHashOps("userClickCount").delete(name);
SeckillStatus userSeckillStatus = (SeckillStatus) redisTemplate.boundHashOps("userSeckillStatus").get(name);
userSeckillStatus.setStatus("5");
redisTemplate.boundHashOps("userSeckillStatus").put(name, userSeckillStatus);
System.out.println("支付成功完成!");
}
// @RabbitListener(queues = "twoSeckillQueue")
public void twoSeckillQueue(String json){
System.out.println("秒杀订单数据回滚");
SeckillStatus seckillStatus = JSON.parseObject(json, SeckillStatus.class);
TbSeckillGoodsModel goodsModel = (TbSeckillGoodsModel) redisTemplate.boundHashOps("goods:" + seckillStatus.getTime()).get(seckillStatus.getSkuId());
if (ObjectUtils.isEmpty(goodsModel)) {
System.out.println("修改数据库");
TbSeckillGoodsModel goodsServiceById = tbSeckillGoodsService.getById(seckillStatus.getSkuId());
goodsServiceById.setStockCount(1);
tbSeckillGoodsService.updateById(goodsServiceById);
//存储存储到redis 中
redisTemplate.boundHashOps("goods:" + seckillStatus.getTime()).put(seckillStatus.getSkuId(), goodsServiceById);
} else {
//修改
System.out.println("修改redis...");
goodsModel.setStockCount(goodsModel.getStockCount() + 1);
//存储存储到redis 中
redisTemplate.boundHashOps("goods:" + seckillStatus.getTime()).put(seckillStatus.getSkuId(), goodsModel);
tbSeckillGoodsService.updateById(goodsModel);
}
//添加队列
redisTemplate.boundListOps("goodsCountList:" + seckillStatus.getSkuId()).leftPush(seckillStatus.getSkuId());
//修改订单
TbSeckillOrderModel orderModel = (TbSeckillOrderModel) redisTemplate.boundHashOps("order").get(seckillStatus.getUsername());
orderModel.setStatus("3");
redisTemplate.boundHashOps("order").put(seckillStatus.getUsername(), orderModel);
//删除点击次数
redisTemplate.boundHashOps("userClickCount").delete(seckillStatus.getUsername());
//修改订单状态为超时
SeckillStatus userStatus = (SeckillStatus) redisTemplate.boundHashOps("userSeckillStatus").get(seckillStatus.getUsername());
userStatus.setStatus("3");
redisTemplate.boundHashOps("userSeckillStatus").put(seckillStatus.getUsername(), userStatus);
System.out.println("数据回滚完毕!");
}