如何设计一个商品秒杀接口

131 阅读4分钟

衡量一个秒杀系统的好坏,通常会同时考虑QPS和TPS这两个指标。因为秒杀系统需要处理大量的请求,所以QPS是一个重要的指标,它可以反映系统的查询处理能力。同时,秒杀系统也需要保证交易的正确性和一致性,因此TPS也是一个重要的指标,它可以反映系统的事务处理能力。因此,我们需要综合考虑QPS和TPS这两个指标来评估一个秒杀系统的好坏。在软件方面,QPS和TPS的概念和意义:

  • QPS(Queries Per Second)指的是每秒查询数,通常用于衡量数据库或缓存系统的性能。它表示在一秒钟内系统能够处理的查询请求数量。
  • TPS(Transactions Per Second)指的是每秒事务数,通常用于衡量交易系统的性能。它表示在一秒钟内系统能够处理的事务数量,其中事务可以是一次数据库操作、一次网络请求或一次业务逻辑处理等。

设计思路

  1. 使用Redis作为缓存,将商品信息和库存信息存储在Redis中,Redis的原子性操作来保证不会超卖。
  2. 使用消息队列来处理秒杀请求,将请求放入消息队列中,使用多个消费者来处理请求,提高并发处理能力。
  3. 使用分布式锁来保证同一时刻只有一个请求能够进入秒杀处理流程,避免重复秒杀。
  4. 使用限流算法来控制QPS和TPS,避免系统崩溃。
  5. 使用异步处理来提高系统的并发处理能力,将秒杀请求的处理和返回结果的处理分离开来,提高系统的吞吐量。

代码实现

  1. 商品信息和库存信息存储在Redis中
public class RedisUtil {
    private static JedisPool jedisPool;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(200);
        config.setMaxIdle(8);
        config.setMaxWaitMillis(1000 * 100);
        config.setTestOnBorrow(true);
        jedisPool = new JedisPool(config, "localhost", 6379, 3000);
    }

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }

    public static void returnResource(final Jedis jedis) {
        if (jedis != null) {
            jedisPool.close(jedis);
        }
    }
}

@Data
public class Goods {
    private int id;
    private String name;
    private int stock;
}

public class GoodsDao {
    private static final String GOODS_PREFIX = "goods:";
    private static final String STOCK_PREFIX = "stock:";
    //从缓存中获取商品信息:
    public Goods getGoodsById(int id) {
        Jedis jedis = RedisUtil.getJedis();
        try {
            String key = GOODS_PREFIX + id;
            String name = jedis.hget(key, "name");
            String stockStr = jedis.get(STOCK_PREFIX + id);
            int stock = Integer.parseInt(stockStr);
            return new Goods(id, name, stock);
        } finally {
            RedisUtil.returnResource(jedis);
        }
    }
    //更新缓存中的商品库存:
    public void updateStock(int id, int stock) {
        Jedis jedis = RedisUtil.getJedis();
        try {
            String key = STOCK_PREFIX + id;
            jedis.set(key, String.valueOf(stock));
        } finally {
            RedisUtil.returnResource(jedis);
        }
    }
}
  1. 使用消息队列来处理秒杀请求
public class MQUtil {
    private static final String QUEUE_NAME = "seckill_queue";

    public static void send(String message) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }

    public static void receive(Consumer<String> consumer) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                consumer.accept(message);
            }, consumerTag -> {
            });
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}
  1. 使用分布式锁来保证同一时刻只有一个请求能够进入秒杀处理流程
public class DistributedLock {
    private static final String LOCK_PREFIX = "lock:";
    private static final int LOCK_EXPIRE_TIME = 5000;

    public static boolean tryLock(int id) {
        Jedis jedis = RedisUtil.getJedis();
        try {
            String key = LOCK_PREFIX + id;
            String value = UUID.randomUUID().toString();
            String result = jedis.set(key, value, "NX", "PX", LOCK_EXPIRE_TIME);
            return "OK".equals(result);
        } finally {
            RedisUtil.returnResource(jedis);
        }
    }

    public static void unlock(int id) {
        Jedis jedis = RedisUtil.getJedis();
        try {
            String key = LOCK_PREFIX + id;
            String value = jedis.get(key);
            if (value != null) {
                jedis.del(key);
            }
        } finally {
            RedisUtil.returnResource(jedis);
        }
    }
}
  1. 使用限流算法来控制QPS和TPS
/**
* 这里的限流器可以替换使用Guava的限流器.
*/
public class RateLimiter {
    private static final int MAX_REQUESTS_PER_SECOND = 100;
    private static final int MAX_CONCURRENT_REQUESTS = 10;
    private static final Semaphore SEMAPHORE = new Semaphore(MAX_CONCURRENT_REQUESTS);
    private static final AtomicInteger REQUEST_COUNT = new AtomicInteger(0);
    private static final AtomicLong LAST_REQUEST_TIME = new AtomicLong(0);

    public static boolean tryAcquire() {
        long now = System.currentTimeMillis();
        int count = REQUEST_COUNT.get();
        if (count >= MAX_REQUESTS_PER_SECOND) {
            return false;
        }
        if (now - LAST_REQUEST_TIME.get() > 1000) {
            REQUEST_COUNT.set(0);
            LAST_REQUEST_TIME.set(now);
        }
        if (SEMAPHORE.tryAcquire()) {
            REQUEST_COUNT.incrementAndGet();
            return true;
        } else {
            return false;
        }
    }

    public static void release() {
        SEMAPHORE.release();
    }
}
  1. 使用异步处理来提高系统的并发处理能力
public class SeckillService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SeckillService.class);
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);

    public void seckill(int goodsId, int userId) {
        if (!RateLimiter.tryAcquire()) {
            LOGGER.warn("请求被限流,goodsId={}, userId={}", goodsId, userId);
            return;
        }
        String message = goodsId + ":" + userId;
        //将请求发送到消息队列中:
        MQUtil.send(message);
    }

    public void handleSeckillRequest(String message) {
        String[] parts = message.split(":");
        int goodsId = Integer.parseInt(parts[0]);
        int userId = Integer.parseInt(parts[1]);
        if (!DistributedLock.tryLock(goodsId)) {
            LOGGER.warn("请求被锁定,goodsId={}, userId={}", goodsId, userId);
            return;
        }
        //多个消费者进行消费:
        EXECUTOR_SERVICE.submit(() -> {
            try {
                GoodsDao goodsDao = new GoodsDao();
                Goods goods = goodsDao.getGoodsById(goodsId);
                if (goods.getStock() > 0) {
                    goodsDao.updateStock(goodsId, goods.getStock() - 1);
                    LOGGER.info("秒杀成功,goodsId={}, userId={}", goodsId, userId);
                } else {
                    LOGGER.warn("库存不足,goodsId={}, userId={}", goodsId, userId);
                }
            } finally {
                //释放锁:
                DistributedLock.unlock(goodsId);
                //释放限流信号量:
                RateLimiter.release();
            }
        });
    }
}

用户从秒杀开始到结束的整个过程步骤

  1. 用户发送秒杀请求,请求中包含商品ID和用户ID。

  2. 服务器使用限流算法来控制QPS和TPS,避免系统崩溃。

  3. 服务器将请求放入消息队列中,使用多个消费者来处理请求,提高并发处理能力。

  4. 消费者使用分布式锁来保证同一时刻只有一个请求能够进入秒杀处理流程,避免重复秒杀。

  5. 消费者从Redis中获取商品信息和库存信息,使用Redis的原子性操作来保证不会超卖。

  6. 如果库存充足,消费者更新库存信息,并返回秒杀成功的结果。

  7. 如果库存不足,消费者返回秒杀失败的结果。

  8. 消费者释放分布式锁和限流信号量,处理完成。

  9. 服务器返回秒杀结果给用户。