您现在的位置是:首页 >其他 >如何设计一个秒杀系统网站首页其他

如何设计一个秒杀系统

code.song 2023-07-02 20:00:03
简介如何设计一个秒杀系统

设计一个秒杀系统一般需要考虑以下几个方面:

  1. 高并发处理能力:秒杀活动往往会吸引大量用户的参与,需要设计支持高并发处理的架构。可采用分布式系统架构、消息队列、缓存等技术来提高请求处理能力。

  2. 队列处理:利用消息队列来实现请求的异步处理,减低直接请求数据库等核心资源的负载。

  3. 限流控制:对于并发量较大的情况,需要采取限流控制策略,如抛弃过多请求、等待处理后再响应等方式。

  4. 数据库优化:针对高并发的场景,需要进行数据库的优化和设计,如合理的分库分表,优化SQL查询性能等。

  5. 分布式锁:秒杀系统需要使用分布式锁来保证数据的一致性和并发控制。

  6. 支付和物流系统:对于秒杀成功的订单,需要有完善的支付和物流支持,可以考虑与第三方支付和物流公司合作。

  7. 安全性处理:秒杀活动容易引发网络攻击,需要采取严格的安全性处理措施,如防刷、验证码等方式来保障系统的安全性。

  8. 页面静态化:秒杀活动的瞬间爆发力极强,如果每个用户都请求原始页面,会导致服务器负载过高。因此需要将页面静态化,使用CDN缓存,提高系统响应速度。

  9. 事务管理:在用户抢购成功后,需要进行下单、减库存、修改订单状态等操作。这些操作需要保证事务性,要么全部成功,要么全部失败,保证数据一致性。

    10.缓存优化:抢购场景下,大量的用户同时请求购买某个商品,会导致缓存命中率降低,从而增加数据库压力。因此需要考虑缓存优化,使用Redis等缓存工具,提高读取效率。

实现A方式:

创建一个商品表:

CREATE TABLE t_goods (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品ID',
name VARCHAR(32) NOT NULL COMMENT '商品名称',
stock INT DEFAULT 0 COMMENT '商品库存',
PRIMARY KEY (id)
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4;

创建一个订单表:

CREATE TABLE t_order (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
goods_id BIGINT NOT NULL COMMENT '商品ID',
amount INT DEFAULT 0 COMMENT '购买数量',
unit_price DECIMAL(10,2) DEFAULT 0.00 COMMENT '商品单价',
total_price DECIMAL(10,2) DEFAULT 0.00 COMMENT '商品总价',
status INT DEFAULT 0 COMMENT '订单状态',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id)
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4;

创建一个秒杀服务类:

@Service
public class SeckillService {
    @Autowired
    private GoodsMapper goodsMapper;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    private static final String GOODS_CACHE_PREFIX = "seckill:goods:";

    private static final String STOCK_CACHE_PREFIX = "seckill:stock:";

    @Transactional
    public void seckill(long userId, long goodsId, int amount, BigDecimal price) {
        // 判断库存是否充足
        if (!checkStock(goodsId, amount)) {
            throw new RuntimeException("Stock is not enough for goods " + goodsId);
        }
        // 减库存
        reduceStock(goodsId, amount);
        // 下单
        createOrder(userId, goodsId, amount, price);
    }

    /**
     * 检查库存是否充足
     */
    private boolean checkStock(long goodsId, int amount) {
        Integer stock = (Integer)redisTemplate.opsForValue().get(STOCK_CACHE_PREFIX + goodsId);
        if (stock != null && stock >= amount) {
            return true;
        }
        stock = goodsMapper.getStockById(goodsId);
        if (stock >= amount) {
            redisTemplate.opsForValue().set(STOCK_CACHE_PREFIX + goodsId, stock);
            return true;
        } else {
            redisTemplate.opsForValue().set(STOCK_CACHE_PREFIX + goodsId, 0);
            return false;
        }
    }

    /**
     * 减库存
     */
    private void reduceStock(long goodsId, int amount) {
        redisTemplate.opsForValue().decrement(STOCK_CACHE_PREFIX + goodsId, amount);
        goodsMapper.reduceStock(goodsId, amount);
    }

    /**
     * 创建订单
     */
    private void createOrder(long userId, long goodsId, int amount, BigDecimal price) {
        Order order = new Order();
        order.setUserId(userId);
        order.setGoodsId(goodsId);
        order.setAmount(amount);
        order.setUnitPrice(price);
        order.setTotalPrice(price.multiply(new BigDecimal(amount)));
        order.setStatus(0); // 订单状态:0-待支付
        orderMapper.insert(order);
    }

    /**
     * 加载商品列表
     */
    public List<Goods> loadGoodsList() {
        List<Goods> goodsList = (List<Goods>)redisTemplate.opsForValue().get(GOODS_CACHE_PREFIX + "list");
        if (goodsList == null) {
            goodsList = goodsMapper.getGoodsList();
            redisTemplate.opsForValue().set(GOODS_CACHE_PREFIX + "list", goodsList);
        }
        return goodsList;
    }

    /**
     * 加载商品详情
     */
    public Goods loadGoodsDetails(long goodsId) {
        Goods goods = (Goods)redisTemplate.opsForValue().get(GOODS_CACHE_PREFIX + goodsId);
        if (goods == null) {
            goods = goodsMapper.getGoodsById(goodsId);
            redisTemplate.opsForValue().set(GOODS_CACHE_PREFIX + goodsId, goods);
        }
        return goods;
    }
}

编写一个Controller类,处理抢购请求:

@RestController
@RequestMapping("/seckill")
public class SeckillController {
    @Autowired
    private SeckillService seckillService;

    @PostMapping("/goods/{goodsId}")
    public ResponseEntity<Void> seckill(@PathVariable("goodsId") long goodsId,
                                        @RequestParam("userId") long userId,
                                        @RequestParam("amount") int amount,
                                        @RequestParam("price") BigDecimal price) {
        try {
            seckillService.seckill(userId, goodsId, amount, price);
            return ResponseEntity.ok(null);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @GetMapping("/goods")
    public ResponseEntity<List<Goods>> loadGoodsList() {
        try {
            List<Goods> goodsList = seckillService.loadGoodsList();
            return ResponseEntity.ok(goodsList);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @GetMapping("/goods/{goodsId}")
    public ResponseEntity<Goods> loadGoodsDetails(@PathVariable("goodsId") long goodsId) {
        try {
            Goods goods = seckillService.loadGoodsDetails(goodsId);
            return ResponseEntity.ok(goods);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

实现B方式

首先,我们需要定义一个秒杀商品的实体类,包括商品ID、名称、描述、价格、库存等字段。

public class SeckillProduct {
    private int id; // 商品ID
    private String name; // 商品名称
    private String description; // 商品描述
    private BigDecimal price; // 商品价格
    private int stock; // 商品库存

    // 省略getter和setter方法
}

 

对于高并发的请求处理,我们可以使用分布式锁来保证同一时刻只有一个请求可以访问库存信息,从而避免多个请求同时减少库存导致超卖的问题。这里我们使用Redis作为分布式锁的实现。

// 获取分布式锁
public boolean tryLock(String key) {
    // 使用Redis的setnx命令获取锁,如果返回1表示获取成功,否则获取失败
    return jedis.setnx(key, "locked") == 1;
}

// 释放分布式锁
public void releaseLock(String key) {
    jedis.del(key);
}

在进行抢购操作前,我们需要先检查商品的库存是否充足。如果库存不足,则返回错误信息,否则扣减库存并生成订单。为了避免恶意刷单,我们可以针对每个用户限制抢购次数,并且限制抢购的时间窗口。具体实现如下:

public class SeckillService {
    private static final String KEY_PREFIX = "seckill:product:";

    // 检查并扣减库存
    public boolean seckill(String userId, int productId) {
        String key = KEY_PREFIX + productId;

        // 判断用户之前是否已经抢购过
        String userKey = KEY_PREFIX + productId + ":user:" + userId;
        if (jedis.get(userKey) != null) {
            return false;
        }

        // 获取分布式锁
        boolean locked = false;
        try {
            locked = redisLock.tryLock(key);
            if (!locked) {
                return false;
            }

            // 判断库存是否足够
            SeckillProduct product = getProduct(productId);
            if (product == null || product.getStock() < 1) {
                return false;
            }

            // 扣减库存并生成订单
            int stock = product.getStock();
            product.setStock(stock - 1);
            boolean updated = updateProduct(product);
            if (updated) {
                String orderNo = generateOrderNo();
                createOrder(orderNo, userId, productId);
                jedis.setex(userKey, 600, "lock");
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (locked) {
                redisLock.releaseLock(key);
            }
        }
        return false;
    }

    // 检查是否已经到达抢购时间,防止恶意刷单
    public boolean checkTime(String productId) {
        long currentTime = System.currentTimeMillis();
        Date startTime = getStartTime(productId);
        Date endTime = getEndTime(productId);
        if (currentTime < startTime.getTime() || currentTime > endTime.getTime()) {
            return false;
        }
        return true;
    }
}

最后,为了防止数据篡改,我们可以对关键数据进行加密签名,从而保证数据的完整性和可靠性。

public class SignUtil {
    private static final String SALT = "seckill@123456";

    // 生成签名
    public static String generateSign(String data) {
        String sign = null;
        try {
            String source = data + "|" + SALT;
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] md5Bytes = md5.digest(source.getBytes("UTF-8"));
            sign = DatatypeConverter.printHexBinary(md5Bytes).toLowerCase();
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return sign;
    }

    // 验证签名
    public static boolean verifySign(String data, String sign) {
        String expectedSign = generateSign(data);
        return expectedSign.equals(sign);
    }
}

业务手段

有的时候,我们不能只想着用技术手段解决所有问题,其实,如果在业务上能做点事情的话,如果这些做法并不影响用户体验,那么就可能让技术实现上大大简化方案,整个系统的成本和稳定性也会有大大的提高。

比如我们前面说的限购。

再比如说,在有些秒杀业务中,需要先预约,预约通过后才能参与秒杀,这就大大的降低了秒杀时的请求量了。

再比如说很多电商最近再搞一些预售的功能,其实也是有这方面的考虑的。

还有就是秒杀时通过一些验证码、问题等,也可以降低瞬时的高并发流量,以及降低被脚本刷单的风险。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。