동시성 플래시 킬을 달성하는 7가지 방법, 글이 너무 좋아서 소장 추천! !

1. 소개

높은 동시성 시나리오는 현장의 일상 업무, 특히 인터넷 회사에서 매우 일반적입니다. 이 기사에서는 몇 초 만에 제품을 판매하여 높은 동시성 시나리오를 시뮬레이션합니다. 기사의 모든 코드, 스크립트 및 테스트 사례는 기사 끝에 첨부되어 있습니다.

  • 이 기사의 환경: SpringBoot 2.5.7 + MySQL 8.0 X + MybatisPlus + Swagger2.9.2
  • 시뮬레이션 도구: Jmeter
  • 시뮬레이션 시나리오: 재고 감소 -> 주문 생성 -> 결제 시뮬레이션

2. 상품 급등 – 과매도

개발 과정에서 다음 코드에 익숙할 수 있습니다. @Transactional서비스에 트랜잭션 주석 및 잠금 잠금을 추가합니다.

Spring Boot의 기본 사항은 소개하지 않으므로 이 무료 자습서를 시청하는 것이 좋습니다.

https://github.com/javastacks/spring-boot-best-practice

제어 계층: 컨트롤러

@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();
}

비즈니스 계층: 서비스

@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 제품 획득.

여기서 비즈니스 방식의 시작 부분에 잠금을 추가하고 비즈니스 방식의 종료 후에 잠금을 해제합니다. 하지만 여기서 거래제출은 그렇지 않고, 거래가 제출되기 전에 잠금이 해제되어 상품이 과매도될 가능성이 있습니다. 그래서 잠그는 타이밍이 매우 중요합니다!

3. 과매도 종목 해결

위의 과매도 현상의 경우 트랜잭션에서 잠금이 해제될 때 주요 문제가 발생하며 트랜잭션이 커밋되기 전에 잠금이 해제됩니다. (트랜잭션 제출은 전체 메서드가 실행된 후에 실행됩니다.) 이 문제를 해결하는 방법은 잠금 단계를 진행하는 것입니다.

  • 컨트롤러 계층에서 잠글 수 있음
  • 비즈니스 메서드가 실행되기 전에 Aop을 사용하여 잠글 수 있습니다.

3.1 방법 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();
}

위의 잠금은 트랜잭션이 커밋되기 전에 잠금 해제 문제를 해결할 수 있으며 스트레스 테스트는 세 가지 상황에서 수행할 수 있습니다.

  • 동시수 1000, 상품 100
  • 동시수 1000, 상품 1000
  • 동시수 2000, 상품 1000

동시성 수가 제품 수보다 많은 경우 제품 세킬은 일반적으로 과소 판매로 보이지 않지만 동시성 수가 제품 수보다 적거나 같을 때 더 적을 수 있습니다. 매우 이해하기 쉬운 제품 판매.

문제가 없으면 텍스처가 없을 것입니다. 방법이 많고 텍스처가 너무 많기 때문입니다.

3.2 방법 2(AOP 버전 잠금)

제어 계층에서 위의 잠금 방법의 경우 우아하지 않은 것처럼 보일 수 있지만 트랜잭션 전에 잠그는 또 다른 방법, 즉 AOP가 있습니다.

오픈 소스 및 무료 Spring Boot의 가장 완벽한 튜토리얼을 추천합니다.

https://github.com/javastacks/spring-boot-best-practice

사용자 정의 AOP 주석

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public  @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();
}

이 방법은 자물쇠를 사용하면 더욱 발전되고 아름답습니다!

3.3 방법 3(비관적 잠금 1)

위의 비즈니스 코드 수준에서 잠그는 것 외에도 동시성 제어를 위해 데이터베이스와 함께 제공되는 잠금을 사용할 수도 있습니다.

비관적 잠금, 비관적 잠금이란? 일반적으로 어떤 작업을 수행하기 전에 잠금 확인을 수행해야 합니다. 이 데이터베이스 수준 잠금 작업은 덜 효율적입니다.

for update 사용 시 트랜잭션을 추가해야 하며 트랜잭션이 처리된 후 for update는 행 수준 잠금을 해제합니다.

요청 건수가 플래시 상품 건수와 같으면 판매량이 줄어듭니다.

@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);
}

도층

@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);

}

위는 업데이트에 사용하여 쿼리 데이터를 잠그고 행 잠금을 추가하는 것입니다.

3.4 방법 4(비관적 잠금 2)

비관적 잠금의 두 번째 방법은 업데이트 업데이트 명령을 사용하여 테이블 잠금을 추가하는 것입니다.

/**
 * UPDATE锁表
 * @param skgId  商品id
 * @param userId    用户id
 * @return
 */
@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);
}

도층

@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);

    @Update(value = "UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
    int updateSecondKillById(@Param("skgId") long skgId);
}

3.5 방법 5(낙관적 잠금)

낙관적 잠금은 이름에서 알 수 있듯이 버전 필드를 사용하여 데이터가 수정되었는지 여부를 확인함으로써 작업 결과에 대해 매우 낙관적입니다.

낙관적 잠금, 재고 수량을 확인하지 않고 직접 재고 공제

여기에 사용된 낙관적 잠금은 많은 수의 데이터 업데이트 예외를 발생시킵니다(예외를 발생시키면 구매가 실패함) 구성된 스냅업 수가 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);
}
@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);

    @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 방법 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();
    }
}

소비 seckill 대기열: ApplicationRunner 인터페이스 구현

// 消费秒杀队列
@Slf4j
@Component
public class TaskRunner implements 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할 수 없으며 throw new RuntimeException()과 같은 예외를 throw하는 코드는 주석 처리해야 합니다. 프로그램에서 예외가 발생하면 중지되어 소비 seckill 대기열 프로세스가 종료됩니다!

급증을 구현하기 위해 차단 대기열을 사용할 때 주의해야 할 몇 가지 사항이 있습니다.

  • seckillService.startSecondKillByAop()또한 소비 seckill 대기열에서 호출하는 비즈니스 메소드가 잠겨 있고 잠겨 있지 않다는 것 , seckillService.startSecondKillByLock()메소드의 결과가 동일하다는 것을 쉽게 이해할 수 있습니다.
  • 대기열 길이가 상품 수량과 일치하면 판매가 줄어들고 가치가 높아질 수 있습니다.
  • 다음은 대기열 길이가 1000이고 제품 수가 1000이고 동시성 수가 2000일 때 언더셀입니다.

3.7 방법 7(Disruptor queue)

Disruptor는 고성능 대기열입니다.연구 개발의 원래 의도는 메모리 대기열의 지연 문제를 해결하는 것입니다.성능 테스트에서 I/O 작업과 동일한 규모임을 알 수 있습니다.단일 Disruptor를 기반으로 개발된 시스템의 스레드는 초당 600만 주문을 지원할 수 있습니다.

// 事件生成工厂(用来初始化预分配事件对象)
public class SecondKillEventFactory implements EventFactory<SecondKillEvent> {

    @Override
    public SecondKillEvent newInstance() {
        return new SecondKillEvent();
    }
}
// 事件对象(秒杀事件)
public class SecondKillEvent implements Serializable {
    private static final long serialVersionUID = 1L;
    private long seckillId;
    private long userId;

 // set/get方法略

}
// 使用translator方式生产者
public class SecondKillEventProducer {

    private final static EventTranslatorVararg<SecondKillEvent> translator = (seckillEvent, seq, objs) -> {
        seckillEvent.setSeckillId((Long) objs[0]);
        seckillEvent.setUserId((Long) objs[1]);
    };

    private final RingBuffer<SecondKillEvent> ringBuffer;

    public SecondKillEventProducer(RingBuffer<SecondKillEvent> ringBuffer){
        this.ringBuffer = ringBuffer;
    }

    public void secondKill(long seckillId, long userId){
        this.ringBuffer.publishEvent(translator, seckillId, userId);
    }
}
// 消费者(秒杀处理器)
@Slf4j
public class SecondKillEventConsumer implements EventHandler<SecondKillEvent> {

    private SecondKillService secondKillService = (SecondKillService) SpringUtil.getBean("secondKillService");

    @Override
    public void onEvent(SecondKillEvent seckillEvent, long seq, boolean bool) {
        Result result = secondKillService.startSecondKillByAop(seckillEvent.getSeckillId(), seckillEvent.getUserId());
        if(result.equals(Result.ok(SecondKillStateEnum.SUCCESS))){
            log.info("用户:{}{}",seckillEvent.getUserId(),"秒杀成功");
        }
    }
}
public class DisruptorUtil {

    static Disruptor<SecondKillEvent> disruptor;

    static{
        SecondKillEventFactory factory = new SecondKillEventFactory();
        int ringBufferSize = 1024;
        ThreadFactory threadFactory = runnable -> new Thread(runnable);
        disruptor = new Disruptor<>(factory, ringBufferSize, threadFactory);
        disruptor.handleEventsWith(new SecondKillEventConsumer());
        disruptor.start();
    }

    public static void producer(SecondKillEvent kill){
        RingBuffer<SecondKillEvent> ringBuffer = disruptor.getRingBuffer();
        SecondKillEventProducer producer = new SecondKillEventProducer(ringBuffer);
        producer.secondKill(kill.getSeckillId(),kill.getUserId());
    }
}
@ApiOperation(value="秒杀实现方式七——Disruptor队列")
@PostMapping("/start/disruptor")
public Result startDisruptor(long skgId){
    try {
        log.info("开始秒杀方式七...");
        final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
        SecondKillEvent kill = new SecondKillEvent();
        kill.setSeckillId(skgId);
        kill.setUserId(userId);
        DisruptorUtil.producer(kill);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return Result.ok();
}

테스트 후 Disruptor 대기열을 사용하는 것은 사용자 지정 대기열과 동일한 문제가 있으며 과매도 상황도 있지만 효율성이 향상되었습니다.

4. 요약

동시성을 달성하는 위의 7가지 방법에 대해 요약하면 다음과 같습니다.

  • 첫 번째와 두 번째 방법은 동시성 문제를 해결하기 위해 코드에서 잠금과 트랜잭션을 사용하는 것입니다 주요 솔루션은 트랜잭션 전에 잠금을 로드해야 한다는 것입니다
  • 방법 3, 4, 5는 동시성 문제를 해결하기 위해 주로 데이터베이스 잠금을 사용하는 방법 3은 업데이트를 사용하여 테이블에 행 잠금을 추가하는 방법 4는 업데이트를 사용하여 테이블을 잠그는 방법 5는 버전 필드를 추가하여 데이터베이스를 제어 업데이트 작업, 방법 5가 최악의 효과
  • 방법 6과 7은 대기열을 사용하여 동시성 문제를 해결합니다.여기서 특별한 주의가 필요한 것은 코드에서 throw를 통해 예외를 throw할 수 없으며, 그렇지 않으면 소비 스레드가 종료되고 대기열에 들어가고 나가는 시간 간격으로 인해 더 적은 제품이 판매됩니다

위의 모든 상황은 코드로 테스트되었으며 테스트는 세 가지 상황으로 나뉩니다.

  • 동시성 수는 1000개이고 제품 수는 100개입니다.
  • 동시성 수는 1000이고 제품 수는 1000입니다.
  • 동시성 수는 2000이고 제품 수는 1000입니다.

생각: 분산 상황에서 동시성 문제를 해결하는 방법은 무엇입니까? 다음에 실험을 계속하십시오.

저작권 진술: 이 기사는 CC 4.0 BY-SA 저작권 계약에 따라 CSDN 블로거 "Stop Going Forward"의 원본 기사입니다. 재인쇄를 위해 원본 소스 링크와 이 설명을 첨부하십시오. 원본 링크: https://blog.csdn.net/zxd1435513775/article/details/122643285

최근 핫한 기사 추천:

1. 1,000개 이상의 Java 인터뷰 질문 및 답변(2022 최신 버전)

2. 브릴리언트! Java 코 루틴이오고 있습니다. . .

3. Spring Boot 2.x 튜토리얼, 너무 포괄적입니다!

4. 폭발과 폭발로 화면을 채우지 말고 데코레이터 모드를 사용해보십시오. 이것이 우아한 방법입니다! !

5. "Java Development Manual (Songshan Edition)"의 최신 릴리스를 빠르게 다운로드하십시오!

기분좋게 좋아요+포워드 잊지마세요!

추천

출처blog.csdn.net/youanyyou/article/details/130626223