java单机秒杀扛1万并发方案和代码

我们先来看普通的加锁加事务秒杀性能,

说明:

1.这里的秒杀业务执行一次耗时100毫秒

2.电脑配置16g内存 4核8线程 cpu i7 7代,数据库连接池max=20 


    @RequestMapping("/purchase2")
    public ResultJson purchase2( Long productId){
        int userId = new Random().nextInt(10000);
        UserInfo.setUserId(userId);
        RLock lock = redissonClient.getLock("LOCK");
        lock.lock();
        OrderVO purchase = null;
        try {
            purchase = tOrderService.purchase2(productId);
            if(purchase.isSuccess()){
                return new ResultJson<>(200, "成功下单", purchase);
            }else{
                return new ResultJson<>(500, purchase.getMsg(), purchase);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        lock.unlock();
        return new ResultJson<>(500, "下单失败", null);
    }

100个商品 8000并发秒杀

1. 普通加锁加事务 100个商品8000并发,入库100条需要13秒,吞吐量一秒9次,这里jmeter只跑了2000个线程就停止了因为实在是太慢了不想等了

 

1000个商品,8000并发秒杀

1. 普通加锁加事务 1000个商品8000并发,入库1000条需要199秒这里jmeter只跑了2000个线程就停止了因为实在是太慢了不想等了

以下是我优化的方案,自定义了Seckill注解和ProductIdMark注解采用aop处理

    @RequestMapping("/purchase")
    @Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
    public ResultJson purchase(@ProductIdMark Long productId){
        OrderVO purchase = null;
        try {
            purchase = tOrderService.purchase(productId);
            if(purchase.isSuccess()){
                return new ResultJson<>(200, "成功下单", purchase);
            }else{
                return new ResultJson<>(500, purchase.getMsg(), purchase);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ResultJson<>(500, "下单失败", null);
}

100个商品 8000并发秒杀

1. 普通加锁加事务 100个商品8000并发,入库100条需要4秒,吞吐量一秒1034次

 

1000个商品 8000并发秒杀

1. 普通加锁加事务 1000个商品8000并发,入库1000条需要7秒,吞吐量一秒615次

 

分析

100商品优化前: 入库13秒,吞吐9/秒

100商品优化后: 入库4秒,吞吐1034/秒

1000商品优化前:入库199秒,吞吐9/秒

1000商品优化后:入库7秒,吞吐615/秒

可以看得出差别还是很大的

实现思路:所有请求被aop拦截,aop将用户存储到redis中,1000商品购买率是70%所以redis只要存储1500个用户的id即可,多余的直接返回商品售空,接下来轮到1500个用户竞争1000个商品,预热时候把1000个商品分成20份也就是每份50个商品,将20份商品存入库中作为lockName锁使用,同步轮训获取数据库中的lockName,获取到对应的lockName即可对该记录的库存进行扣减操作大致流程就是这样

下面是aop的主要实现:

@Aspect
@Configuration
public class SeckillAspect {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ProductsSubsectionService productsSubsectionService;

    @Pointcut("@annotation(seckill)")
    public void pointCut(Seckill seckill) {
    }

    private final String infoKey= "info:productId:";
    private final String indexKey= "index:productId:";
    private final Long time=60*10L;
    @Around("pointCut(seckill)")
    public Object around(ProceedingJoinPoint joinPoint,Seckill seckill) throws Throwable {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        String appName = className +":"+ methodName;
        Long productId = getProductId(joinPoint);
        //模拟用户
        int userId = new Random().nextInt(100000);
        UserInfo.setUserId(userId);
        //初始化商品数据
        init(appName,productId);

        if(stopRun(appName,productId)){
            return new ResultJson<>(500, "商品售空", null);
        }

        //防止同一个用户使用外挂疯狂点击
        RMap<Integer, Integer> map = redissonClient.getMap(appName+":userClicks");
        RLock lockClicks = redissonClient.getLock("LOCK:"+appName+":5");
        try {
            lockClicks.lock();
            map.put(userId,map.get(userId)+1);
        }finally {
            lockClicks.unlock();
        }

        if(map.get(userId) > 1){
            return new ResultJson<>(500, "请勿重复提交", null);
        }

        RLock lock = redissonClient.getLock("LOCK:"+appName+":2");
        lock.lock();
        if(isUpdatePrimaryTable(appName,productId)){
            updatePrimaryTable(seckill,productId);
        }
        String lockName = getLockName(appName, productId);
        RLock lock4 = redissonClient.getLock(lockName);
        try {
            lock4.lock();
            lock.unlock();
            ProductsSubsection productsSubsection = productsSubsectionService.queryByLockMark(lockName);
            if(productsSubsection.getNumber()==0){
                return new ResultJson<>(500, "商品售空", null);
            }
            Object proceed = joinPoint.proceed();
            productsSubsection.setNumber(productsSubsection.getNumber()-1);
            productsSubsectionService.update(productsSubsection);
            return proceed;
        }finally {
            lock4.unlock();
        }
    }

    private Long getProductId(ProceedingJoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //一维数组是注解位置,二维数组是注解个数
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Long productId=null;
        boolean noProductId = true;
        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotation = parameterAnnotations[i];
            for (Annotation var : annotation) {
                if (var.annotationType().equals(ProductIdMark.class)) {
                    productId= (Long) args[i];
                    noProductId = false;
                    break;
                }
            }
        }
        if(noProductId){
            throw new RuntimeException("形参未指定注解:ProductId");
        }
        return productId;
    }

    private boolean init(String appName, Long productId){

        if(!redissonClient.getMap(appName).isEmpty()){
            //已初始化
            return false;
        }
        RLock lock = redissonClient.getLock("LOCK:"+appName+":2");
        try {
            lock.lock();
            if(redissonClient.getMap(appName).isEmpty()){
                List<ProductsSubsection> list = productsSubsectionService.queryByProdId(productId);
                int inventory =0 ;
                LinkedHashSet<String> locks = new LinkedHashSet<>();
                for (ProductsSubsection subsection : list) {
                    inventory+=subsection.getNumber();
                    locks.add(subsection.getLockMark());
                }
                CommodityInfo info = new CommodityInfo();
                info.setLockNames(locks);
                info.setInventory(inventory);
                RMap<String, Object> map = redissonClient.getMap(appName);
                map.expire(time, TimeUnit.SECONDS);
                String key= infoKey+ productId;
                String key2= indexKey+ productId;
                map.put(key,info);
                map.put(key2,0);
            }
        }finally {
            lock.unlock();
        }
        return true;
    }

    //统计用户点击数
    private Integer userClicks(String appName,int size){
        RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");
        user.expire(time, TimeUnit.SECONDS);

        RLock lock = redissonClient.getLock("LOCK:"+appName+":3");
        int userLength = user.size();
        if(userLength >= size){
            return user.size();
        }
        try {
            lock.lock();
            if(user.size() < size){
                //初始化用户点击次数
                int userId = UserInfo.getUserId();
                Integer clicks = user.get(userId);
                if( clicks == null){
                    user.put(userId,0);
                }else{
                    user.put(userId,clicks+1);
                }
            }
        } finally {
            lock.unlock();
        }
        return user.size();
    }

    private boolean stopRun(String appName,Long productId){
        RMap<String, Object> map = redissonClient.getMap(appName);
        map.expire(time, TimeUnit.SECONDS);
        CommodityInfo info = (CommodityInfo) map.get( infoKey+ productId);
        double size = info.getProbability() *  info.getInventory();
        RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");
        user.expire(time, TimeUnit.SECONDS);

        if(userClicks(appName, (int) size) >= size && !user.containsKey(UserInfo.getUserId())){
            return true;
        }
        return false;
    }

    private String getLockName(String appName,Long productId){
        String key= infoKey+ productId;
        String key2= indexKey+ productId;
        RMap<Object, Object> map = redissonClient.getMap(appName);
        map.expire(time, TimeUnit.SECONDS);
        CommodityInfo  info = (CommodityInfo) map.get(key);
        Integer index = (Integer) map.get(key2);
        List<String> lockNamesList = new ArrayList<>(info.getLockNames());
        map.put(key2,index+1);
        return lockNamesList.get(index % lockNamesList.size());
    }



    private boolean isUpdatePrimaryTable(String appName,Long productId){
        String key= infoKey+ productId;
        String key2= indexKey+ productId;
        RMap<Object, Object> map = redissonClient.getMap(appName);
        map.expire(time, TimeUnit.SECONDS);
        CommodityInfo  info = (CommodityInfo) map.get(key);
        Integer index = (Integer) map.get(key2);
        return index==info.getInventory();
    }

    //更新主表
    private void updatePrimaryTable(Seckill seckill,Long productId){
        // 获取注解的值
        String tableName = seckill.tableName();
        String inventoryColumn = seckill.inventoryColumn();
        String productIdColumn = seckill.productIdColumn();
        System.out.println("开始更新"+tableName+"主表数据");
        productsSubsectionService.updatePrimaryTable(tableName,inventoryColumn,productIdColumn,productId);
    }
}

项目地址:

通用所有秒杀业务,只要商品表中有库存,商品id即可

使用规则:

1.创建分段表

CREATE TABLE `products_subsection` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `prod_id` bigint(20) DEFAULT NULL,
  `number` int(11) DEFAULT NULL,
  `lock_mark` varchar(20) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4

2.在Controller的方法中使用Seckill注解和@ProductIdMark

//商品表名,库存字段名,商品id名
@Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
//用于表示商品id
@ProductIdMark

访问后的效果

架构图

项目链接:redisson-demo: 使用所有秒杀场景,高效,方便,只需要使用注解即可生效 (gitee.com)

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java并发编程是指在多个线程同时运行时,对共享资源的访问和修改进行协调和管理,以确保程序的正确性和性能。 下面是几个常见的Java并发场景和相应的实现代码: 1. 生产者-消费者模型 生产者-消费者模型是一种常见的并发场景,它涉及到一个或多个生产者线程向一个共享的缓冲区中生产数据,同时一个或多个消费者线程从缓冲区中消费数据。为了协调生产者和消费者线程之间的访问和修改,需要使用锁、条件变量等并发控制机制。 以下是一个简单的生产者-消费者模型的Java实现代码: ```java import java.util.LinkedList; import java.util.Queue; import java.util.Random; public class ProducerConsumerExample { public static void main(String[] args) { Queue<Integer> buffer = new LinkedList<>(); // 缓冲区 int maxSize = 10; // 缓冲区最大容量 Thread producerThread = new Thread(new Producer(buffer, maxSize), "Producer"); Thread consumerThread = new Thread(new Consumer(buffer), "Consumer"); producerThread.start(); consumerThread.start(); } static class Producer implements Runnable { private Queue<Integer> buffer; private int maxSize; public Producer(Queue<Integer> buffer, int maxSize) { this.buffer = buffer; this.maxSize = maxSize; } @Override public void run() { while (true) { synchronized (buffer) { while (buffer.size() == maxSize) { try { buffer.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Random random = new Random(); int number = random.nextInt(100); buffer.add(number); System.out.println("Produced " + number); buffer.notifyAll(); } } } } static class Consumer implements Runnable { private Queue<Integer> buffer; public Consumer(Queue<Integer> buffer) { this.buffer = buffer; } @Override public void run() { while (true) { synchronized (buffer) { while (buffer.isEmpty()) { try { buffer.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } int number = buffer.poll(); System.out.println("Consumed " + number); buffer.notifyAll(); } } } } } ``` 2. 线程池 线程池是一种管理线程的机制,它通过在应用程序启动时创建一定数量的线程并将它们放入池中,然后在应用程序运行期间重复使用这些线程,以避免因频繁创建和销毁线程而导致的性能问题。Java中提供了Executor和ThreadPoolExecutor两个类来实现线程池。 以下是一个简单的线程池的Java实现代码: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { Runnable worker = new WorkerThread("Task " + i); executor.execute(worker); } executor.shutdown(); while (!executor.isTerminated()) {} System.out.println("All tasks completed"); } static class WorkerThread implements Runnable { private String taskName; public WorkerThread(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " " + taskName + " is running"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + taskName + " is completed"); } } } ``` 3. CountDownLatch CountDownLatch是一种同步工具,它允许一个或多个线程等待一组事件的完成。当CountDownLatch的计数器变为0时,等待线程可以继续执行。Java中提供了CountDownLatch类来实现这种同步机制。 以下是一个简单的CountDownLatch的Java实现代码: ```java import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { int n = 5; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(n); for (int i = 0; i < n; i++) { new Thread(new Worker(startSignal, doneSignal)).start(); } System.out.println("Workers are waiting for the start signal"); Thread.sleep(1000); startSignal.countDown(); // 送开始信号 System.out.println("Workers are started"); doneSignal.await(); // 等待所有任务完成 System.out.println("All tasks are completed"); } static class Worker implements Runnable { private final CountDownLatch startSignal; private final CountDownLatch doneSignal; public Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { this.startSignal = startSignal; this.doneSignal = doneSignal; } @Override public void run() { try { startSignal.await(); // 等待开始信号 System.out.println(Thread.currentThread().getName() + " is working"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { doneSignal.countDown(); // 送完成信号 } } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值