实现高并发秒杀的七种方式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){    
 try {        
   log.info("开始秒杀方式一...");        
   final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
   Result result = secondKillService.startSecondKillByLock(skgId, userId);     
   if(result != null){            
   log.info("用户:{}--{}", userId, result.get("msg"));        
 }else{            
   log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");        
 }    
 } catch (Exception e) {        
   e.printStackTrace();    
 } finally {   
}    
   return Result.ok();
}

业务层:Service

@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByLock(long skgId, long userId) {    
  lock.lock();    
  try {        
  //校验库存        
  SecondKill secondKill = secondKillMapper.selectById(skgId);        
  Integer number = secondKill.getNumber();        
  if (number > 0) {            
  //扣库存            
	secondKill.setNumber(number - 1);            
	secondKillMapper.updateById(secondKill);            
	//创建订单            
	SuccessKilled killed = new SuccessKilled();            
	killed.setSeckillId(skgId);            
	killed.setUserId(userId);            
	killed.setState((short) 0);            
	killed.setCreateTime(new Timestamp(System.currentTimeMillis()));            
	successKilledMapper.insert(killed);            
	// 模拟支付            
	Payment payment = new Payment();            
	payment.setSeckillId(skgId);            
	payment.setSeckillId(skgId);            
	payment.setUserId(userId);            
	payment.setMoney(40);            
	payment.setState((short) 1);            
	payment.setCreateTime(new Timestamp(System.currentTimeMillis()));            
	paymentMapper.insert(payment);        
  } else {            
    return Result.error(SecondKillStateEnum.END);        
  }    
} catch (Exception e) {        
  throw new ScorpiosException("异常了个乖乖");    
} finally {        
  lock.unlock();    
}    
return Result.ok(SecondKillStateEnum.SUCCESS);
}

对于上面的代码应该没啥问题吧,业务方法上加事务,在处理业务的时候加锁。
但上面这样写法是有问题的,会出现超卖的情况,
看下测试结果:模拟1000个并发,抢100商品.
Jmeter不了解的,可以参考这篇文章:
https://blog.csdn.net/zxd1435513775/article/details/106372446
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3.1 方式一(改进版加锁)

@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){    
  //在此处加锁    
  lock.lock();    
  try {        
     log.info("开始秒杀方式一...");        
     final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;        
     Result result = secondKillService.startSecondKillByLock(skgId, userId);        
  if(result != null){            
     log.info("用户:{}--{}", userId, result.get("msg"));        
  }else{            
     log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");        
  }   
  } catch (Exception e) {        
     e.printStackTrace();    
  } finally {        
    // 在此处释放锁        
    lock.unlock();    
 }    
 return Result.ok();
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documentedpublic  
@interface ServiceLock {    
    String description()  default "";
}
定义切面类
@Slf4j
@Component
@Scope
@Aspect
@Order(1) 
//order越小越是最先执行,但更重要的是最先执行的最后结束
public class LockAspect {    
/**     
*思考:为什么不用synchronized     
* service默认是单例的,并发下lock只有一个实例     
*/    
private static Lock lock = new ReentrantLock(true); 
//互斥锁参数默认false,不公平锁    
// Service层切点用于记录错误日志    
@Pointcut("@annotation(com.scorpios.secondkill.aop.ServiceLock)")    
public void lockAspect() { }    
@Around("lockAspect()")    
public Object around(ProceedingJoinPoint joinPoint) {        
   lock.lock();        
   Object obj = null;        
   try {            
      obj = joinPoint.proceed();        
   } catch (Throwable e) {            
      e.printStackTrace();   
      throw new RuntimeException();        
  } finally{            
   lock.unlock();        
 }        
  return obj;    
 }
}

在业务方法上添加AOP注解

@Override
@ServiceLock 
//使用Aop进行加锁
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByAop(long skgId, long userId) {    
	try {        
		//校验库存        
		SecondKill secondKill = secondKillMapper.selectById(skgId);        
		Integer number = secondKill.getNumber();        
		if (number > 0) {            
		//扣库存            
		secondKill.setNumber(number - 1);            
		secondKillMapper.updateById(secondKill);            
		//创建订单            
		SuccessKilled killed = new SuccessKilled();            
		killed.setSeckillId(skgId);            
		killed.setUserId(userId);            
		killed.setState((short) 0);            
killed.setCreateTime(new Timestamp(System.currentTimeMillis()));            
		successKilledMapper.insert(killed);            
		//支付            
		Payment payment = new Payment();            
		payment.setSeckillId(skgId);            
		payment.setSeckillId(skgId);            
		payment.setUserId(userId);            
		payment.setMoney(40);            
		payment.setState((short) 1);            
payment.setCreateTime(new Timestamp(System.currentTimeMillis()));            
		   paymentMapper.insert(payment);       
		} else {            
		   return Result.error(SecondKillStateEnum.END);        
		}    
	} catch (Exception e) {        
	   throw new ScorpiosException("异常了个乖乖");    
   }    
return Result.ok(SecondKillStateEnum.SUCCESS);
}

控制层:

@ApiOperation(value="秒杀实现方式二——Aop加锁")
@PostMapping("/start/aop")
public Result startAop(long skgId){    
	try {        
		log.info("开始秒杀方式二...");        
		final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;       
		Result result = secondKillService.startSecondKillByAop(skgId, userId);        
		if(result != null){            
		 log.info("用户:{}--{}", userId, result.get("msg"));        
		}else{            
		 log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");        
		}    
	} catch (Exception e) {        
	e.printStackTrace();    
	}    
  return Result.ok();
}

这种方式在对锁的使用上,更高阶、更美观!
在这里插入图片描述

@ApiOperation(value="秒杀实现方式三——悲观锁")
@PostMapping("/start/pes/lock/one")
public Result startPesLockOne(long skgId){    
	try {        
		log.info("开始秒杀方式三...");        
		final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;        
		Result result = secondKillService.startSecondKillByUpdate(skgId, userId);        
		if(result != null){            
		   log.info("用户:{}--{}", userId, result.get("msg"));        
		}else{            
		   log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");       
		}    
	} catch (Exception e) {        
	 e.printStackTrace();    
	}    
   return Result.ok();
}

业务逻辑

@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdate(long skgId, long userId) {    
	try {        
		// 校验库存-悲观锁        
		SecondKill secondKill = secondKillMapper.querySecondKillForUpdate(skgId);        
		Integer number = secondKill.getNumber();        
		if (number > 0) {            
		//扣库存            
		secondKill.setNumber(number - 1);            
		secondKillMapper.updateById(secondKill);            
		//创建订单            
		SuccessKilled killed = new SuccessKilled();            
		killed.setSeckillId(skgId);            
		killed.setUserId(userId);            
		killed.setState((short) 0);            
		killed.setCreateTime(new Timestamp(System.currentTimeMillis()));            
		successKilledMapper.insert(killed);            
		//支付            
		Payment payment = new Payment();            
		payment.setSeckillId(skgId);            
		payment.setSeckillId(skgId);            
		payment.setUserId(userId);            
		payment.setMoney(40);            
		payment.setState((short) 1);            
		payment.setCreateTime(new Timestamp(System.currentTimeMillis()));            
		paymentMapper.insert(payment);        
		} else {            
		return Result.error(SecondKillStateEnum.END);        
		}    
	} catch (Exception e) {        
	   throw new ScorpiosException("异常了个乖乖");    
	} finally {  }    
   return Result.ok(SecondKillStateEnum.SUCCESS);
}

Dao层

@Repository
public interface SecondKillMapper extends BaseMapper<SecondKill> {    
/**     
* 将此行数据进行加锁,当整个方法将事务提交后,才会解锁     
* @param skgId     
* @return     
*/    
@Select(value = "SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")    
  SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);
}

上面是利用for update进行对查询数据加锁,加的是行锁。
3.4 方式四(悲观锁二)
悲观锁的第二种方式就是利用update更新命令来加表锁

@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdateTwo(long skgId, long userId) {    
	try {        
			// 不校验,直接扣库存更新        
			int result = secondKillMapper.updateSecondKillById(skgId);        
			if (result > 0) {            
			//创建订单            
			SuccessKilled killed = new SuccessKilled();            
			killed.setSeckillId(skgId);            
			killed.setUserId(userId);            
			killed.setState((short) 0);            
		killed.setCreateTime(new Timestamp(System.currentTimeMillis()));            
			successKilledMapper.insert(killed);            
			//支付            
			Payment payment = new Payment();            
			payment.setSeckillId(skgId);            
			payment.setSeckillId(skgId);            
			payment.setUserId(userId);            
			payment.setMoney(40);            
			payment.setState((short) 1);            
		payment.setCreateTime(new Timestamp(System.currentTimeMillis()));            
			paymentMapper.insert(payment);        
		} else {            
		   return Result.error(SecondKillStateEnum.END);        
		}    
	 }catch (Exception e) {       
	    throw new ScorpiosException("异常了个乖乖");    
	 } finally {    }    
return Result.ok(SecondKillStateEnum.SUCCESS);
}

Dao层

/** 
* UPDATE锁表 
* @param skgId  商品id 
* @param userId  用户id 
* @return 
*/
@Repositorypublic interface SecondKillMapper extends BaseMapper<SecondKill> {    
/**     
* 将此行数据进行加锁,当整个方法将事务提交后,才会解锁     
* @param skgId     
* @return     
*/
    
@Select(value = "SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")    
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);
    
@Update(value = "UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")    
int updateSecondKillById(@Param("skgId") long skgId);
}

3.5 方式五(乐观锁)
乐观锁,顾名思义,就是对操作结果很乐观,通过利用version字段来判断数据是否被修改
乐观锁,不进行库存数量的校验,直接做库存扣减
这里使用的乐观锁会出现大量的数据更新异常(抛异常就会导致购买失败)、如果配置的抢购人数比较少、比如120:100(人数:商品) 会出现少买的情况,不推荐使用乐观锁。

@ApiOperation(value="秒杀实现方式五——乐观锁")
@PostMapping("/start/opt/lock")
public Result startOptLock(long skgId){    
try {        
log.info("开始秒杀方式五...");        
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;        
//参数添加了购买数量        
Result result = secondKillService.startSecondKillByPesLock(skgId, userId,1);        
if(result != null){            
log.info("用户:{}--{}", userId, result.get("msg"));        
}else{            
log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");        
}    
} catch (Exception e) {        
e.printStackTrace();   
}    
return Result.ok();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByPesLock(long skgId, long userId, int number) {    
 //乐观锁,不进行库存数量的校验,直接    
 try {        
 SecondKill kill = secondKillMapper.selectById(skgId);        
 //剩余的数量应该要大于等于秒杀的数量        
 if(kill.getNumber() >= number) {            
 int result = 
secondKillMapper.updateSecondKillByVersion(number,skgId,kill.getVersion());            
 if (result > 0) {                
 //创建订单                
 SuccessKilled killed = new SuccessKilled();                
 killed.setSeckillId(skgId);                
 killed.setUserId(userId);                
 killed.setState((short) 0);                
 killed.setCreateTime(new Timestamp(System.currentTimeMillis()));                
 successKilledMapper.insert(killed);                
 //支付                
 Payment payment = new Payment();                
 payment.setSeckillId(skgId);                
 payment.setSeckillId(skgId);                
 payment.setUserId(userId);                
 payment.setMoney(40);                
 payment.setState((short) 1);                
 payment.setCreateTime(new Timestamp(System.currentTimeMillis()));                
 paymentMapper.insert(payment);           
 } else {                
   return Result.error(SecondKillStateEnum.END);            
 }        
 }   
 } catch (Exception e) {        
   throw new ScorpiosException("异常了个乖乖");    
 } finally {    }    
 return Result.ok(SecondKillStateEnum.SUCCESS);}
 
 @Repositorypublic interface SecondKillMapper extends 
BaseMapper<SecondKill> {    

 /**     
 * 将此行数据进行加锁,当整个方法将事务提交后才会解锁     
 * @param skgId     
 * @return     
 */    

 @Select(value = "SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")    
 SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);    
 @Update(value = "UPDATE seckill SET number=number-1 
WHERE seckill_id=#{skgId} AND number > 0")    
 int updateSecondKillById(@Param("skgId") long skgId);    
 @Update(value = "UPDATE seckill  SET number=number-#{number},version=version+1 WHERE seckill_id=#{skgId} AND version = #{version}")    
 int updateSecondKillByVersion(@Param("number") int number, @Param("skgId") long skgId, @Param("version")int version);}

3.6 方式六(阻塞队列)
利用阻塞队类,也可以解决高并发问题。其思想就是把接收到的请求按顺序存放到队列中,消费者线程逐一从队列里取数据进行处理,看下具体代码。
阻塞队列:这里使用静态内部类的方式来实现单例模式,在并发条件下不会出现问题。

//秒杀队列(固定长度为100)
public class SecondKillQueue {    
//队列大小    
static final int QUEUE_MAX_SIZE = 100;    
//用于多线程间下单的队列    
static BlockingQueue<SuccessKilled> blockingQueue = new LinkedBlockingQueue<SuccessKilled>(QUEUE_MAX_SIZE);    
//使用静态内部类,实现单例模式    
private SecondKillQueue(){};    
private static class SingletonHolder{        
//静态初始化器,由JVM来保证线程安全        
private static SecondKillQueue queue = new SecondKillQueue();    
}    

/**

  • 单例队列
  • @return
    */
 public static SecondKillQueue getSkillQueue(){        
 return SingletonHolder.queue;    
 }    

/**
*生产入队
*@param kill
*@throws InterruptedException
*add(e) 队列未满时,返回true;队列满则抛出IllegalStateException(“Queue full”)异常——AbstractQueue

  • put(e) 队列未满时,直接插入没有返回值;队列满时会阻塞等待,一直等到队列未满时再插入。
  • offer(e) 队列未满时,返回true;队列满时返回false。非阻塞立即返回。
  • offer(e, time, unit) 设定等待的时间,如果在指定时间内还不能往队列中插入数据则返回false,插入成功返回true。
    */
 public  Boolean  produce(SuccessKilled kill) {        
 return blockingQueue.offer(kill);    
 }    

/**

  • 消费出队
  • poll() 获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null
  • take() 与带超时时间的poll类似不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒
    */

public SuccessKilled consume() throws InterruptedException {
return blockingQueue.take();

}

 /**     
 * 获取队列大小     
 * @return     
 */    

 public int size() {        
 return blockingQueue.size();    
 }
 }
消费秒杀队列:实现ApplicationRunner接口
@Autowired    
private SecondKillService seckillService;    
@Override public void run(ApplicationArguments var){        
new Thread(() -> {            
log.info("队列启动成功");            
while(true){               
try {                    
//进程内队列                    
SuccessKilled kill = SecondKillQueue.getSkillQueue().consume();                    
if(kill != null){                        
Result result = seckillService.startSecondKillByAop(kill.getSeckillId(), kill.getUserId());                        
if(result != null && result.equals(Result.ok(SecondKillStateEnum.SUCCESS))){                            
log.info("TaskRunner,result:{}",result);                            
log.info("TaskRunner从消息队列取出用户,用户:{}{}",kill.getUserId(),"秒杀成功");                        
}                    
}                
} catch (InterruptedException e) {                   
 e.printStackTrace();                
 }            
 }        
 }).start();    
 }}
 @ApiOperation(value="秒杀实现方式六——消息队列")
 @PostMapping("/start/queue")
 public Result startQueue(long skgId){    
 try {        
 log.info("开始秒杀方式六...");        
 final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;        
 SuccessKilled kill = new SuccessKilled();        
 kill.setSeckillId(skgId);        
 kill.setUserId(userId);        
 Boolean flag = SecondKillQueue.getSkillQueue().produce(kill);        
 //虽然进入了队列,但是不一定能秒杀成功 进队出队有时间间隙        
 if(flag){            
 log.info("用户:{}{}",kill.getUserId(),"秒杀成功");        
 }else{            
 log.info("用户:{}{}",userId,"秒杀失败");       
 }    
 } catch (Exception e) {        
 e.printStackTrace();    
 }    
 return Result.ok();
 }

注意:在业务层和AOP方法中,不能抛出任何异常, throw new RuntimeException()这些抛异常代码要注释掉。因为一旦程序抛出异常就会停止,导致消费秒杀队列进程终止!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值