Java秒杀抢购
需要用到的技术 java多线程 Redis mysql数据库 Quartz定时器
用到的框架: SSM
整体项目结构:
前端页面不用我们写,我这里提源码项目自己下载
链接:https://pan.baidu.com/s/13YHV3lpSMZtf4Tb_A941Qg
提取码:1234
需要的表
链接:https://pan.baidu.com/s/1femCs5bPg7Vj_mrSpT5pyw
提取码:1234
我接下来就开始讲代码了
controller mapper pojo utils 这些里面的代码就没必要讲了 自己看源码
我们讲主要的地方service 和 task 和 thread 这些层下面的类
SeckillGoodsServiceImpl
关键的代码
@Override
public Result saveOrder(Long id, String userId) {
//1.判断用户是否在排队队列 或者 以秒杀过此商品但是没有付款
Boolean isMember = redisTemplate.boundSetOps(StaticResource.SECKILL_USER+id).isMember(userId);
if(isMember){
//获取购买这个商品中 有没有此用户
TbSeckillOrder seckillOrder = (TbSeckillOrder) redisTemplate.boundHashOps(TbSeckillOrder.class.getSimpleName()).get(userId);
//有此用户
if(null != seckillOrder){
//1在订单队列,“您已抢购成功,请支付订单!”异常
return new Result(true, "您已抢购成功此商品,在规定时间内请支付订单 否则商品将从新上架!");
}
//不在订单队列,“您正在排队...”
return new Result(false, "您正在排队,请耐心等待。。。");
}
TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);
//2.判断商品是否售罄 我们在定时器里录入了全部秒杀的商品*他的商品个数的数据 因为key是能重复的所以当获取不到时候就代表商品售空了
BoundListOperations boundListOperations = redisTemplate.boundListOps(StaticResource.SECKILL_PREFIX + id);
Object o = boundListOperations.leftPop();
if(o==null){
System.out.println("对不起,商品已售罄");
//2.售罄
return new Result(false, "对不起,商品已售罄,请查看其他商品!");
}
//注意获取到的值可能会不同 但是结果肯定截止到0
// 因为从使用leftPop()到这里是需要时间的如果在这个时间内其他线程又调用了leftPop()那么这个size()打印的结果就会和调用的这个线程重复
// 比如A线程 原本要打印12 在没打印前 B线程又进行了调用那么结果就是 A=11 B=11 以此类推
// 但是放心数据没问题 到0肯定结束 因为boundListOps是阻塞队列 当没有值了就会返回null了 就不会执行下面的代码了
System.out.println("剩余商品:"+boundListOperations.size());
//为了保证用户秒杀只能购买一次同样的商品 我们使用set集合
redisTemplate.boundSetOps(StaticResource.SECKILL_USER+id).add(userId);
//将用户添加到此商品的集合中 等会在多线程中 进行弹出集合中的数据 入数据库里
redisTemplate.boundListOps(OrderRecord.class.getSimpleName()).leftPush(new OrderRecord(userId, id));
//创建一个线程
executor.execute(createOrderThread);
return new Result(true, "秒杀成功,请您尽快支付!");
}
上面代码整体流程
- 判断用户是否在排队队列 或者 以秒杀过此商品但是没有付款
- 判断商品是否售罄 我们在定时器里录入了全部秒杀的商品*他的商品个数的数据 因为key是能重复的所以当获取不到时候就代表商品售空了
- 为了保证用户秒杀只能购买一次同样的商品 我们使用set集合
- 将用户添加到此商品的集合中 等会在多线程中 进行弹出集合中的数据入数据库里 (相当中介的作用)
- 利用线程池创建一个线程, 此线程内就是用于处理用户购买的商品
CreateOrderThread
@Component
public class CreateOrderThread implements Runnable {
@Resource
private RedisTemplate redisTemplate;
@Resource
private IdWorker idWorker;
@Resource
private TbSeckillGoodsMapper seckillGoodsMapper;
@Override
public void run() {
try {
//从队列尾部弹出一个元素 没有元素时候进入阻塞 等待有数据
OrderRecord orderRecord = (OrderRecord) redisTemplate.boundListOps(OrderRecord.class.getSimpleName()).rightPop();
if(null != orderRecord){
Long id = orderRecord.getId();
String userid = orderRecord.getUserId();
//获取秒杀的商品信息 然后将一些值 配置一下
TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);
//3.未售罄,创建订单,以用户id为key存入redis
TbSeckillOrder seckillOrder = new TbSeckillOrder();
seckillOrder.setId(idWorker.nextId());
seckillOrder.setSeckillId(id);
seckillOrder.setMoney(seckillGoods.getCostPrice()); //秒杀价格
seckillOrder.setUserId(userid);
seckillOrder.setSellerId(seckillGoods.getSellerId());
seckillOrder.setCreateTime(new Date());
seckillOrder.setStatus("0");
//将订单信息存储到Redis里用于模拟数据库存储
redisTemplate.boundHashOps(TbSeckillOrder.class.getSimpleName()).put(userid, seckillOrder);
//需要更新Redis数据 防止多线程操作的影响
synchronized (CreateOrderThread.class){
seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);
//4.更新数据库 剩余的个数,判断库存是否售罄
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
if(seckillGoods.getStockCount() <= 0){
//5.售罄,同步秒杀商品数据库(seckillGoods),将秒杀商品从redis中删除
seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);//修改数据库 数量
redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).delete(seckillGoods.getId()); //删除Rieds值的指定商品
} else {
//6.未售罄,更新redis中秒杀商品库存
redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).put(seckillGoods.getId(), seckillGoods);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 获取刚才用户购买的商品id和用户id
- 通过商品id找到对应的商品
- 创建订单信息 存入Redis 等待用户支付成功后我们在录入数据库里 当支付超时后 我们将订单信息从Redis删除 (这个超时功能我们没有做因为需要查询微信 我们下面会告诉你在哪里添加这个功能日后你做的时候自己补充就行)
- 更新数据库商品剩余的个数,判断库存是否售罄
- 如果售罄,同步秒杀商品数据库,将秒杀商品从redis中删除 ,未售罄,更新redis中秒杀商品库存
重点来了 到这一步 就没有代码了 那么我们可以在这一步添加一个线程沉睡20秒 当20秒过后我们进行访问微信查询订单情况,如果没有付款那么将Reids里的对应用户订单删除, 然后更新数据库商品剩余的个数+1和更新redis中秒杀商品库存
在前端也设置一个定时器 如果跳转到支付页面后 20秒后没有支付那么我们就 提示支付超时 然后返回秒杀页面
SeckillGoodsToRedisTask
//@Component
public class SeckillGoodsToRedisTask {
@Autowired
private TbSeckillGoodsMapper seckillGoodsMapper;
@Autowired
private RedisTemplate redisTemplate;
// @Scheduled( cron="0 0 0 1/1 * ?") // cron 0/10 * * * * ? 我们这里测试数据每分钟10秒执行 之后如果要测试卖空商品的话 修改为 0 0 0 1/1 * ? 每天的0时0秒0分 一天执行一次
public void startSeckill(){ //测试使用xml里的方式进行 初始化完成后执行1次就行
try {
TbSeckillGoodsExample example = new TbSeckillGoodsExample();
TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
//库存数量>0
criteria.andStockCountGreaterThan(0);
//活动开始时间 <=当前时间< 活动结束时间
Date date = new Date();
criteria.andStartTimeLessThanOrEqualTo(date); //活动开始时间<=now()
criteria.andEndTimeGreaterThan(date); //活动结束时间>now()
//批量查询所有缓存数据,增加到Redis缓存中
List goods = seckillGoodsMapper.selectByExample(example);
// System.out.println(goods);
//将商品数据加入到缓存中
for (TbSeckillGoods good : goods) {
//秒杀商品信息加入缓存
redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).put(good.getId(),good);
//将每一个 商品*他的个数 都存入到队列里
pushSeckillGoods(good);
}
System.out.println("录入商品成功");
} catch (Exception e) {
e.printStackTrace();
}
}
private void pushSeckillGoods(TbSeckillGoods goods){
//库存量
Integer stockCount = goods.getStockCount();
System.out.println(goods.getTitle()+"库存剩余"+stockCount);
//循环加入Redis队列
//左压栈方式加入
for (int i = 0; i
在测试的时候 我使用xml配置的定时器 初始化后指执行一次 方便测试
如果项目需要在线上运行那么把applicationContext-task.xml 里面配置的定时器的配置去掉
改为使用注解的 cron=“0 0 0 1/1 * ?” 每天的0时0秒0分 一天执行一次
以上只是关键代码,没有展示部分, 自己看项目源码理解
点赞 -收藏-关注-便于以后复习和收到最新内容
有其他问题在评论区讨论-或者私信我-收到会在第一时间回复
如有侵权,请私信联系我
感谢,配合,希望我的努力对你有帮助^_^



