目录
3、前端应用服务器验证用户身份,检查秒杀活动是否开始,是否已经结束。
前端应用服务器(使用Java + Spring Boot):
7、前端应用服务器根据库存数量判断是否可以参与秒杀,如果可以则生成订单。
9、后端应用服务器消费订单信息,根据订单信息生成订单,并更新库存数量。
11、 分布式锁用于保护对库存数量的操作,确保数据的一致性。
设计高并发秒杀系统是一个挑战性的任务,需要考虑到系统的性能、稳定性和数据一致性。本文将介绍如何设计一个高并发的秒杀系统,使用消息队列和分布式锁来确保系统的稳定性和数据一致性。
我们的高并发秒杀系统将采用以下架构:
- +-------------------------------------+
- | 前端负载均衡器 |
- +-----------+--------------+--------------+
- | |
- +-----------v--------------+--------------+
- | 前端应用服务器 | CDN |
- +-----------+--------------+--------------+
- | |
- +----------------------+---------v----------+--------------+-------v--------+-------------+
- | 缓存层 | | 后端应用服务器 | 数据库集群 |
- +-----------+--------------+ +-----------+--------------+--------------+
- | | |
- +-----------v--------------+ +-----------v--------------+--------------+
- | 缓存服务器 | | 消息队列 | 主数据库 |
- +-----------+--------------+ +-----------+--------------+--------------+
- | | |
- +-----------v--------------+ +-----------v--------------+
- | 缓存存储(Redis) | | 业务数据存储(MySQL) |
- +---------------------------+ +---------------------------+
在这个架构中:
通过以上架构设计,可以实现一个高并发的秒杀系统,保证系统的性能、稳定性和数据一致性,为用户提供良好的秒杀体验。
流程如下:
- <!DOCTYPE html>
- <html>
- <head>
- <title>秒杀页面</title>
- </head>
- <body>
- <h1>欢迎参加秒杀活动!</h1>
- <button id="seckillButton">秒杀按钮</button>
- <div id="result"></div>
-
- <script>
- document.getElementById("seckillButton").addEventListener("click", function() {
- // 模拟用户ID和商品ID
- var userId = 123;
- var productId = 456;
-
- fetch("/seckill", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({ userId: userId, productId: productId })
- })
- .then(response => response.json())
- .then(data => {
- document.getElementById("result").innerText = data.message;
- });
- });
- </script>
- </body>
- </html>
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
-
- @SpringBootApplication
- public class SeckillApplication {
-
- private boolean seckillStarted = true; // 模拟秒杀活动是否开始
- private int stock = 10; // 模拟商品库存
-
- public static void main(String[] args) {
- SpringApplication.run(SeckillApplication.class, args);
- }
-
- @RestController
- public class SeckillController {
-
- @PostMapping("/seckill")
- public String seckill(@RequestBody SeckillRequest request) {
- if (!seckillStarted) {
- return "秒杀活动未开始";
- }
-
- if (stock <= 0) {
- return "商品已售罄";
- }
-
- // 模拟生成订单
- stock--;
- return "秒杀成功";
- }
- }
-
- public static class SeckillRequest {
- private Long userId;
- private Long productId;
-
- // 省略getter和setter方法
- }
- }
前端负载均衡器将请求分发到多个前端应用服务器上,可以通过配置负载均衡器(如Nginx、Apache等)来实现。以下是一个简单的示例:
假设有两台前端应用服务器,分别运行在不同的端口上(假设为8001和8002)。
- upstream frontends {
- server 127.0.0.1:8001;
- server 127.0.0.1:8002;
- }
-
- server {
- listen 80;
- server_name example.com;
-
- location / {
- proxy_pass http://frontends;
- }
- }
在这个示例中,Nginx配置了一个名为frontends的upstream,其中包含两台前端应用服务器的地址和端口。当收到用户请求时,Nginx会根据一定的负载均衡算法(如轮询、权重等)将请求转发到这些服务器上。
- <Proxy balancer://frontends>
- BalancerMember http://127.0.0.1:8001
- BalancerMember http://127.0.0.1:8002
- </Proxy>
-
- <VirtualHost *:80>
- ServerName example.com
-
- ProxyPass / balancer://frontends/
- ProxyPassReverse / balancer://frontends/
- </VirtualHost>
在这个示例中,Apache配置了一个名为frontends的负载均衡器,其中包含两台前端应用服务器的地址和端口。当收到用户请求时,Apache会将请求转发到这些服务器上,实现负载均衡。
以下是一个简单的示例,展示前端应用服务器验证用户身份,检查秒杀活动是否开始或结束的过程:
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
-
- @SpringBootApplication
- public class FrontendApplication {
-
- private boolean seckillStarted = true; // 模拟秒杀活动是否开始
- private boolean seckillEnded = false; // 模拟秒杀活动是否结束
-
- public static void main(String[] args) {
- SpringApplication.run(FrontendApplication.class, args);
- }
-
- @RestController
- public class SeckillController {
-
- @PostMapping("/seckill")
- public String seckill(@RequestBody SeckillRequest request) {
- // 验证用户身份,假设用户身份验证通过
- if (!isUserAuthenticated(request.getUserId())) {
- return "用户身份验证失败";
- }
-
- // 检查秒杀活动是否开始或结束
- if (!isSeckillStarted()) {
- return "秒杀活动未开始";
- }
-
- if (isSeckillEnded()) {
- return "秒杀活动已结束";
- }
-
- // 处理秒杀请求
- return "秒杀请求处理中";
- }
-
- private boolean isUserAuthenticated(Long userId) {
- // 省略实际的用户身份验证逻辑
- return true;
- }
-
- private boolean isSeckillStarted() {
- // 省略实际的秒杀活动开始检查逻辑
- return seckillStarted;
- }
-
- private boolean isSeckillEnded() {
- // 省略实际的秒杀活动结束检查逻辑
- return seckillEnded;
- }
- }
-
- public static class SeckillRequest {
- private Long userId;
- private Long productId;
-
- // 省略getter和setter方法
- }
- }
在这个示例中,前端应用服务器通过Spring Boot框架实现了一个简单的/seckill接口,接收用户的秒杀请求。在处理请求之前,先验证用户身份,然后检查秒杀活动是否开始或结束。如果用户身份验证失败,则返回"用户身份验证失败";如果秒杀活动未开始,则返回"秒杀活动未开始";如果秒杀活动已结束,则返回"秒杀活动已结束";否则处理秒杀请求。
首先,需要在前端应用服务器中配置缓存服务器的地址和端口信息,以便发送请求。
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
- import org.springframework.web.client.RestTemplate;
-
- @SpringBootApplication
- public class FrontendApplication {
-
- private static final String CACHE_SERVER_URL = "http://cache-server:8080"; // 缓存服务器地址
-
- public static void main(String[] args) {
- SpringApplication.run(FrontendApplication.class, args);
- }
-
- @RestController
- public class SeckillController {
-
- private final RestTemplate restTemplate = new RestTemplate();
-
- @PostMapping("/seckill")
- public String seckill(@RequestBody SeckillRequest request) {
- // 请求缓存服务器获取商品信息和库存数量
- String url = CACHE_SERVER_URL + "/product/" + request.getProductId();
- ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
-
- if (productInfo == null) {
- return "获取商品信息失败";
- }
-
- // 处理秒杀请求
- return "请求处理中";
- }
- }
-
- public static class SeckillRequest {
- private Long userId;
- private Long productId;
-
- // 省略getter和setter方法
- }
-
- public static class ProductInfo {
- private Long productId;
- private String productName;
- private int stock;
-
- // 省略getter和setter方法
- }
- }
在这个示例中,前端应用服务器使用RestTemplate发送GET请求到缓存服务器的/product/{productId}路由,获取商品信息和库存数量。如果成功获取到商品信息,则继续处理秒杀请求;否则返回"获取商品信息失败"。
缓存负载均衡器将请求分发到多个缓存应用服务器上,可以通过配置缓存负载均衡器(如Redis Cluster、Memcached Cluster等)来实现。以下是一个简单的示例:
假设有两台缓存应用服务器,分别运行在不同的地址和端口上(假设为cache-server1:6379和cache-server2:6379)。
- # 启动redis-server节点1
- redis-server --port 6379
-
- # 启动redis-server节点2
- redis-server --port 6380
-
- # 创建Redis Cluster
- redis-cli --cluster create cache-server1:6379 cache-server2:6379 --cluster-replicas 1
在这个示例中,我们创建了一个包含两个主节点和一个从节点的Redis Cluster。缓存负载均衡器可以将请求分发到这个Redis Cluster上,实现缓存的负载均衡。
- # 安装memcached
- sudo apt-get update
- sudo apt-get install memcached
-
- # 启动memcached节点1
- memcached -p 11211 -d
-
- # 启动memcached节点2
- memcached -p 11212 -d
-
- # 配置memcached集群
- echo "add 127.0.0.1 11212" | nc 127.0.0.1 11211
在这个示例中,我们创建了一个包含两个节点的Memcached Cluster。缓存负载均衡器可以将请求分发到这个Memcached Cluster上,实现缓存的负载均衡。
在实际生产环境中,需要根据具体需求和负载情况来配置缓存负载均衡器,并保证缓存服务器之间的数据同步和一致性。
6、缓存应用服务器返回商品信息和库存数量给前端应用服务器
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.HashMap;
- import java.util.Map;
-
- @SpringBootApplication
- public class CacheServerApplication {
-
- private static final Map<Long, ProductInfo> productCache = new HashMap<>();
-
- public static void main(String[] args) {
- SpringApplication.run(CacheServerApplication.class, args);
- }
-
- @RestController
- public class CacheController {
-
- @GetMapping("/product/{productId}")
- public ProductInfo getProductInfo(@PathVariable Long productId) {
- // 从缓存中获取商品信息
- return productCache.getOrDefault(productId, new ProductInfo(productId, "Unknown", 0));
- }
- }
-
- public static class ProductInfo {
- private Long productId;
- private String productName;
- private int stock;
-
- public ProductInfo(Long productId, String productName, int stock) {
- this.productId = productId;
- this.productName = productName;
- this.stock = stock;
- }
-
- // 省略getter和setter方法
- }
- }
在这个示例中,缓存应用服务器通过Spring Boot框架实现了一个简单的/cache接口,接收前端应用服务器的商品信息请求。根据商品ID从缓存中获取商品信息,如果缓存中不存在该商品信息,则返回一个未知商品信息。
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.HashMap;
- import java.util.Map;
-
- @SpringBootApplication
- public class CacheServerApplication {
-
- private static final Map<Long, ProductInfo> productCache = new HashMap<>();
-
- public static void main(String[] args) {
- SpringApplication.run(CacheServerApplication.class, args);
- }
-
- @RestController
- public class CacheController {
-
- @GetMapping("/product/{productId}")
- public ProductInfo getProductInfo(@PathVariable Long productId) {
- // 从缓存中获取商品信息
- return productCache.getOrDefault(productId, new ProductInfo(productId, "Unknown", 0));
- }
- }
-
- public static class ProductInfo {
- private Long productId;
- private String productName;
- private int stock;
-
- public ProductInfo(Long productId, String productName, int stock) {
- this.productId = productId;
- this.productName = productName;
- this.stock = stock;
- }
-
- // 省略getter和setter方法
- }
- }
在这个示例中,缓存应用服务器通过Spring Boot框架实现了一个简单的/cache接口,接收前端应用服务器的商品信息请求。根据商品ID从缓存中获取商品信息,如果缓存中不存在该商品信息,则返回一个未知商品信息。
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.HashMap;
- import java.util.Map;
-
- @SpringBootApplication
- public class FrontendApplication {
-
- private static final Map<Long, Integer> stockMap = new HashMap<>(); // 模拟商品库存
- private static final Map<Long, Boolean> seckillMap = new HashMap<>(); // 模拟秒杀活动是否开始
- private static final Map<Long, Boolean> seckillEndMap = new HashMap<>(); // 模拟秒杀活动是否结束
- private static final Map<Long, Long> userOrders = new HashMap<>(); // 模拟用户订单
-
- public static void main(String[] args) {
- // 初始化商品库存
- stockMap.put(1L, 10);
- // 初始化秒杀活动状态
- seckillMap.put(1L, true);
- // 初始化秒杀活动结束状态
- seckillEndMap.put(1L, false);
-
- SpringApplication.run(FrontendApplication.class, args);
- }
-
- @RestController
- public class SeckillController {
-
- @PostMapping("/seckill")
- public String seckill(@RequestBody SeckillRequest request) {
- Long productId = request.getProductId();
- Long userId = request.getUserId();
-
- // 判断秒杀活动是否开始或结束
- if (!seckillMap.getOrDefault(productId, false)) {
- return "秒杀活动未开始";
- }
-
- if (seckillEndMap.getOrDefault(productId, true)) {
- return "秒杀活动已结束";
- }
-
- // 判断库存是否足够
- Integer stock = stockMap.getOrDefault(productId, 0);
- if (stock <= 0) {
- return "商品已售罄";
- }
-
- // 生成订单
- userOrders.put(userId, productId);
- // 更新库存
- stockMap.put(productId, stock - 1);
-
- return "秒杀成功";
- }
- }
-
- public static class SeckillRequest {
- private Long userId;
- private Long productId;
-
- // 省略getter和setter方法
- }
- }
在这个示例中,前端应用服务器根据商品ID获取库存数量,判断秒杀活动是否开始或结束,以及库存是否足够。如果满足条件,则生成订单并更新库存,返回秒杀成功;否则返回相应的错误信息。
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
- import org.springframework.web.client.RestTemplate;
-
- @SpringBootApplication
- public class FrontendApplication {
-
- private static final String LOAD_BALANCER_URL = "http://backend-load-balancer";
-
- public static void main(String[] args) {
- SpringApplication.run(FrontendApplication.class, args);
- }
-
- @RestController
- public class OrderController {
-
- private final RestTemplate restTemplate = new RestTemplate();
-
- @PostMapping("/order")
- public String placeOrder(@RequestBody OrderRequest request) {
- // 向后端负载均衡器发送订单信息
- String url = LOAD_BALANCER_URL + "/order";
- return restTemplate.postForObject(url, request, String.class);
- }
- }
-
- public static class OrderRequest {
- private Long orderId;
- private Long productId;
- private Long userId;
-
- // 省略getter和setter方法
- }
- }
在这个示例中,前端应用服务器通过Spring Boot框架实现了一个简单的/order接口,接收订单信息,并使用RestTemplate将订单信息发送到后端负载均衡器的/order接口。
在实际应用中,后端负载均衡器可以使用诸如Nginx、HAProxy、AWS ELB等工具来实现。这里简化为直接在Java中模拟负载均衡器的行为。
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Random;
-
- public class LoadBalancer {
-
- private List<String> backendServers;
-
- public LoadBalancer() {
- backendServers = new ArrayList<>();
- backendServers.add("http://backend-server1");
- backendServers.add("http://backend-server2");
- // 添加更多后端服务器...
- }
-
- public String selectBackendServer() {
- // 模拟负载均衡算法,这里简单使用随机选择
- Random random = new Random();
- int index = random.nextInt(backendServers.size());
- return backendServers.get(index);
- }
-
- public static void main(String[] args) {
- LoadBalancer loadBalancer = new LoadBalancer();
- // 模拟请求分发给后端服务器
- for (int i = 0; i < 10; i++) {
- String backendServer = loadBalancer.selectBackendServer();
- System.out.println("Request sent to: " + backendServer);
- }
- }
- }
在这个示例中,LoadBalancer
类模拟了一个简单的负载均衡器,它维护了一个后端服务器列表,并实现了一个简单的随机选择算法来选择后端服务器。在实际应用中,需要根据实际情况选择合适的负载均衡算法。
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.HashMap;
- import java.util.Map;
-
- @SpringBootApplication
- public class BackendApplication {
-
- private static final Map<Long, Integer> stockMap = new HashMap<>();
-
- public static void main(String[] args) {
- // 初始化库存数量
- stockMap.put(1L, 10);
-
- SpringApplication.run(BackendApplication.class, args);
- }
-
- @RestController
- public class OrderController {
-
- @PostMapping("/order")
- public String createOrder(@RequestBody OrderRequest request) {
- Long orderId = request.getOrderId();
- Long productId = request.getProductId();
- Long userId = request.getUserId();
-
- // 判断库存是否足够
- Integer stock = stockMap.getOrDefault(productId, 0);
- if (stock <= 0) {
- return "商品已售罄";
- }
-
- // 生成订单
- String orderInfo = "订单信息:订单号-" + orderId + ",商品ID-" + productId + ",用户ID-" + userId;
- // 更新库存数量
- stockMap.put(productId, stock - 1);
-
- return "生成订单成功:" + orderInfo;
- }
- }
-
- public static class OrderRequest {
- private Long orderId;
- private Long productId;
- private Long userId;
-
- // 省略getter和setter方法
- }
- }
在这个示例中,后端应用服务器通过Spring Boot框架实现了一个简单的/order接口,接收订单信息,并根据订单信息生成订单,并更新库存数量。如果库存不足,则返回"商品已售罄"。
将订单信息写入数据库集群是一个关键的步骤,需要考虑到数据的一致性和高可用性。下面是一个详细全面的示例,演示如何将订单信息写入数据库集群:
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.sql.Connection;
- import java.sql.DriverManager;
- import java.sql.PreparedStatement;
- import java.sql.SQLException;
-
- @SpringBootApplication
- public class BackendApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(BackendApplication.class, args);
- }
-
- @RestController
- public class OrderController {
-
- @PostMapping("/order")
- public String createOrder(@RequestBody OrderRequest request) {
- Long orderId = request.getOrderId();
- Long productId = request.getProductId();
- Long userId = request.getUserId();
-
- // 生成订单
- String orderInfo = "订单信息:订单号-" + orderId + ",商品ID-" + productId + ",用户ID-" + userId;
-
- // 将订单信息写入数据库集群
- boolean success = writeToDatabase(orderInfo);
-
- if (success) {
- return "生成订单成功:" + orderInfo;
- } else {
- return "生成订单失败:" + orderInfo;
- }
- }
-
- private boolean writeToDatabase(String orderInfo) {
- // 连接数据库集群 这里为了方便演示没有用yaml进行配置
- String url = "jdbc:mysql://database-cluster:3306/database";
- String user = "user";
- String password = "password";
- try (Connection connection = DriverManager.getConnection(url, user, password)) {
- // 写入订单信息
- String sql = "INSERT INTO orders (order_info) VALUES (?)";
- try (PreparedStatement statement = connection.prepareStatement(sql)) {
- statement.setString(1, orderInfo);
- int rowsAffected = statement.executeUpdate();
- return rowsAffected > 0;
- }
- } catch (SQLException e) {
- e.printStackTrace();
- return false;
- }
- }
- }
-
- public static class OrderRequest {
- private Long orderId;
- private Long productId;
- private Long userId;
-
- // 省略getter和setter方法
- }
- }
分布式锁用于保护对库存数量的操作,确保数据的一致性。下面是一个详细全面的示例,演示如何使用Redis实现分布式锁来保护对库存数量的操作:
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
- import redis.clients.jedis.Jedis;
-
- @SpringBootApplication
- public class BackendApplication {
-
- private static final String REDIS_HOST = "localhost";
- private static final int REDIS_PORT = 6379;
- private static final String LOCK_KEY = "inventory_lock";
- private static final String INVENTORY_KEY = "inventory";
-
- public static void main(String[] args) {
- SpringApplication.run(BackendApplication.class, args);
- }
-
- @RestController
- public class OrderController {
-
- @PostMapping("/order")
- public String createOrder(@RequestBody OrderRequest request) {
- Long orderId = request.getOrderId();
- Long productId = request.getProductId();
- Long userId = request.getUserId();
-
- // 获取分布式锁
- try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
- boolean locked = false;
- while (!locked) {
- locked = jedis.setnx(LOCK_KEY, "locked") == 1;
- if (!locked) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- }
-
- // 获取锁成功,处理订单
- int stock = Integer.parseInt(jedis.get(INVENTORY_KEY));
- if (stock > 0) {
- // 生成订单
- String orderInfo = "订单信息:订单号-" + orderId + ",商品ID-" + productId + ",用户ID-" + userId;
- System.out.println("生成订单成功:" + orderInfo);
-
- // 更新库存数量
- jedis.set(INVENTORY_KEY, String.valueOf(stock - 1));
- } else {
- System.out.println("商品已售罄");
- }
-
- // 释放锁
- jedis.del(LOCK_KEY);
- }
-
- return "订单处理完成";
- }
- }
-
- public static class OrderRequest {
- private Long orderId;
- private Long productId;
- private Long userId;
-
- // 省略getter和setter方法
- }
- }
在这个示例中,后端应用服务器通过Spring Boot框架实现了一个简单的/order接口,接收订单信息,并使用Redis实现分布式锁来保护对库存数量的操作。当多个请求同时到达时,只有一个请求能够获得锁并处理订单,其他请求需要等待锁释放后才能继续处理。这样可以保证对库存数量的操作是原子性的,从而确保数据的一致性。
设计高并发的秒杀系统需要考虑到多个方面,包括系统架构、技术选型、流程设计和代码实现等。通过合理的架构设计和技术选型,可以实现一个稳定高效的秒杀系统,为用户提供良好的购物体验。