需要考虑的事项
如何防止超卖
如何基于redis与MQ实现秒杀下单
如何快速相应用户请求
如何将多个商品库存均匀的放在不同的redis集群中,并发下单时如何正确的读取对应缓存
如何避免将单个秒杀商品的数量放在同一个redis中
如何保证MQ消息的可靠性
如何保证消息的幂等性
下单后的消息,大量堆积在MQ中怎么办
如何防止超卖
- 使用分布式锁(如 Redis 的分布式锁)来控制并发下的库存扣减,防止超卖。
- 使用 Redis 进行库存的预减操作,因为 Redis 是内存数据库,读写速度极快,可以有效减轻数据库的压力。
@GetMapping("/start")
public String startSeckill() {
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "locked", 10, TimeUnit.SECONDS);
if (locked!= null && locked) {
try {
// 从 Redis 中获取库存
String stockStr = redisTemplate.opsForValue().get(STOCK_KEY);
int stock = Integer.parseInt(stockStr);
if (stock > 0) {
// 库存减 1
redisTemplate.opsForValue().decrement(STOCK_KEY);
return "秒杀成功";
} else {
return "秒杀失败,库存不足";
}
} finally {
// 释放锁
redisTemplate.delete(LOCK_KEY);
}
} else {
return "系统繁忙,请稍后再试";
}
}
如何基于redis与MQ实现秒杀下单
从 Redis 中获取库存减一之后,立马将消息写入到MQ中,直接返回抢购成功
// 库存减 1
redisTemplate.opsForValue().decrement(STOCK_KEY);
// 发送消息到 RabbitMQ 队列
rabbitTemplate.convertAndSend(QUEUE_NAME, requestId);
如何快速相应用户请求
成功扣减库存并将订单消息写入MQ之后。立马提示用户并在前端引导用户去付款页面,出现“付款页面”需要等MQ中的消息入到DB库之后,用户才能点击付款,否则还是抢购失败。在这一步可能有大量的消息堆积到MQ中
如何将多个商品库存均匀的放在不同的redis集群中,并发下单时如何正确的读取对应缓存
解决思路:
-
自定义 Redis Key 分配策略:
- 为不同商品生成唯一的 Redis Key,并根据一定的规则将这些 Key 分配到不同的 Redis 集群节点上。例如,可以使用商品 ID 的哈希值对 Redis 集群节点数量取模,将不同商品的库存信息分散存储到不同节点,避免热点数据集中在一个节点。
- 可以使用 Redis 的
Hash Tag机制,确保相关数据存储在同一个 Redis 节点,以提高事务操作的效率(如库存扣减和分布式锁操作)。
-
并发下单时读取缓存:
- 当用户发起秒杀请求时,根据商品 ID 计算出对应的 Redis 节点。
- 通过 Spring Boot 的 Redis 集群配置,将请求路由到正确的 Redis 节点读取库存信息。
- 使用分布式锁确保并发情况下库存扣减的一致性。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final int REDIS_CLUSTER_SIZE = 3; // 假设 Redis 集群有 3 个节点
@GetMapping("/start")
public String startSeckill(Long productId) {
// 计算商品库存存储的 Redis 节点
int nodeIndex = (int) (productId % REDIS_CLUSTER_SIZE);
String stockKey = getStockKey(productId, nodeIndex);
String lockKey = getLockKey(productId, nodeIndex);
String requestId = UUID.randomUUID().toString();
// 获取分布式锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (locked!= null && locked) {
try {
String stockStr = redisTemplate.opsForValue().get(stockKey);
int stock = Integer.parseInt(stockStr);
if (stock > 0) {
// 库存减 1
redisTemplate.opsForValue().decrement(stockKey);
// 发送消息到 RabbitMQ 队列
rabbitTemplate.convertAndSend(QUEUE_NAME, requestId);
return "秒杀成功";
} else {
return "秒杀失败,库存不足";
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
return "系统繁忙,请稍后再试";
}
}
private String getStockKey(Long productId, int nodeIndex) {
return "{seckill_stock}" + productId + "_node_" + nodeIndex;
}
private String getLockKey(Long productId, int nodeIndex) {
return "{seckill_lock}" + productId + "_node_" + nodeIndex;
}
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class RedisClusterConfig {
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
List<String> nodes = Arrays.asList(clusterNodes.split(","));
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(nodes);
return new JedisConnectionFactory(redisClusterConfiguration);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
}
# application.properties 中的 Redis 集群配置示例
spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002
如何保证MQ消息的可靠性
- 可以利用消息的事务例如rocketMq的事务
- 利用消息的持久化,和确认机制
如何保证消息的幂等性
解决思路:
-
生成唯一消息标识:
- 在将消息发送到 MQ 时,为每条消息生成一个唯一的标识符,该标识符可以是一个 UUID 或者基于业务规则生成的唯一值,如订单号、请求 ID 等。
- 确保这个唯一标识符可以唯一标识这个消息,并且在后续的消息处理过程中可以方便地使用。
-
使用 Redis 存储已处理消息:
- 在消息消费者端,使用 Redis 存储已经处理过的消息的唯一标识符。
- 当收到一条消息时,首先检查该消息的唯一标识符是否已经存在于 Redis 中。
- 如果存在,说明该消息已经被处理过,直接丢弃该消息,避免重复处理;如果不存在,说明该消息是新消息,继续处理并将其唯一标识符存储到 Redis 中。
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
@RabbitListener(queues = "seckill_queue")
public class SeckillMessageConsumer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String PROCESSED_MESSAGE_KEY = "seckill_processed_message:";
@RabbitHandler
public void processMessage(String message) {
// 假设 message 是一个包含了唯一标识符的消息,这里可以是一个 JSON 字符串,其中包含了唯一标识符,如 {"messageId":"unique_id", "content":"seckill_request"}
String messageId = extractMessageId(message);
String processedKey = PROCESSED_MESSAGE_KEY + messageId;
Boolean processed = redisTemplate.opsForValue().setIfAbsent(processedKey, "processed", 1, TimeUnit.DAYS);
if (processed!= null && processed) {
// 消息未处理过,进行处理
try {
System.out.println("Processing message: " + message);
// 这里添加具体的业务逻辑,如订单处理、库存更新等操作
// 处理完成后,消息被标记为已处理,存储在 Redis 中
} catch (Exception e) {
// 处理异常情况,例如记录日志或回滚操作
System.err.println("Error processing message: " + e.getMessage());
}
} else {
System.out.println("Message already processed, skipping: " + message);
}
}
private String extractMessageId(String message) {
// 这里需要根据消息的具体格式提取唯一标识符
// 假设消息是一个 JSON 字符串,可以使用 JSON 解析库(如 Jackson)提取 messageId
// 这里简单示例,假设 messageId 是字符串的前部分
return message.split(":")[0];
}
}
下单后的消息,大量堆积在MQ中怎么办
解决思路:
-
扩容消费者:
- 增加消息队列消费者的数量,让更多的消费者同时处理消息,提高消息的处理速度,从而减少消息堆积。
- 可以通过动态调整消费者的数量,根据消息堆积的程度和系统的处理能力进行灵活配置。
-
丢弃消息: 当监测到消息超过1分钟没有被消费时,就证明此时有大量的消息堆积在MQ中,可以采用将消息丢弃,并将库存还原,提示用户付款失败
-
写入redis缓存: 当监测到消息超过1分钟没有被消费时,就证明此时有大量的消息堆积在MQ中,此时可以将消息先写入到缓存中,并开启一个定时任务再去消费