使用 Redis + MQ 完成抢购活动的最佳实现
·
好记忆不如烂笔头,能记下点东西,就记下点,有时间拿出来看看,也会发觉不一样的感受.
一,概况
1.介绍
有兄弟留言,说让我帮忙设计一个秒杀活动,目的是拉新,大致的情况他是这样告诉我的:
有个活动,用户想参加活动,就需要注册成会员,然后抢购礼品(1000个礼品)到账进行消费。以此来拉取新用户和提高用户的活跃度。针对这样的一个抢购场景,进行抢购活动的代码设计。
2.关键信息
核心信息是:用户(userId),活动(activityId),库存(1000);
3.设计思路
异步,削峰,使用MQ, 可以采用你熟悉的或者系统中已经存在的MQ;
资源池加锁,使用Redis做抢购资源的管控,springboot中推荐使用 Redisson 做分布式锁;
利用redis的流式处理,完成系统的核心代码实现,推荐使用lua脚本实现。
二,核心实现
具体的代码实现,我就用以下方式进行输出,不做过多的阐述,直接看代码就好.
1.添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2.Lua脚本文件
-- KEYS[1] = 库存键名
-- KEYS[2] = 用户已领取集合键名
-- KEYS[3] = Stream队列键名
-- ARGV[1] = userId
-- ARGV[2] = 消息内容(JSON格式)
-- ARGV[3] = 过期时间(秒)
-- 检查库存是否充足
local current_stock = tonumber(redis.call("GET", KEYS[1]))
if not current_stock or current_stock <= 0 then
return {code = 0, message = "库存不足"}
end
-- 检查用户是否已经领取过
if redis.call("SISMEMBER", KEYS[2], ARGV[1]) == 1 then
return {code = 0, message = "您已经参与过此活动"}
end
-- 扣减库存
local new_stock = redis.call("DECR", KEYS[1])
redis.call("EXPIRE", KEYS[1], ARGV[3])
-- 记录用户领取
redis.call("SADD", KEYS[2], ARGV[1])
redis.call("EXPIRE", KEYS[2], ARGV[3])
-- 写入Stream队列
redis.call("XADD", KEYS[3], "*", "userId", ARGV[1], "message", ARGV[2])
return {code = 1, message = "抢购成功", stock = new_stock}
3.消息实体
// GiftPurchaseRequest.java
public class GiftPurchaseRequest {
private Long userId;
private Long activityId;
private String giftName;
// 构造函数、getter和setter
public GiftPurchaseRequest() {}
public GiftPurchaseRequest(Long userId, Long activityId, String giftName) {
this.userId = userId;
this.activityId = activityId;
this.giftName = giftName;
}
// getter和setter方法
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Long getActivityId() { return activityId; }
public void setActivityId(Long activityId) { this.activityId = activityId; }
public String getGiftName() { return giftName; }
public void setGiftName(String giftName) { this.giftName = giftName; }
@Override
public String toString() {
return String.format("{\"userId\": %d, \"activityId\": %d, \"giftName\": \"%s\"}",
userId, activityId, giftName);
}
}
// GiftPurchaseResult.java
public class GiftPurchaseResult {
private int code;
private String message;
private Integer stock;
public GiftPurchaseResult(int code, String message, Integer stock) {
this.code = code;
this.message = message;
this.stock = stock;
}
// getter和setter方法
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
}
4. Redis Lua脚本执行器
@Component
public class RedisLuaExecutor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@SuppressWarnings("unchecked")
public GiftPurchaseResult executeGiftPurchase(Long userId, String giftInfo, String activityId, int expireSeconds) {
try {
List<String> keys = Arrays.asList(
"gift:stock:" + activityId, // 库存键
"gift:users:" + activityId, // 已领取用户集合
"gift:stream:" + activityId // Stream队列
);
List<String> args = Arrays.asList(
String.valueOf(userId), // 用户ID
giftInfo, // 礼品信息
String.valueOf(expireSeconds) // 过期时间
);
DefaultRedisScript<List> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setLocation(new ClassPathResource("lua/gift_purchase.lua"));
redisScript.setResultType(List.class);
List<Object> result = (List<Object>) redisTemplate.execute(
redisScript,
new StringRedisSerializer(),
new StringRedisSerializer(),
keys,
args.toArray(new String[0])
);
if (result != null && !result.isEmpty()) {
List<Object> response = (List<Object>) result.get(0);
int code = ((Long) response.get(0)).intValue();
String message = (String) response.get(1);
Integer stock = response.size() > 2 ? ((Long) response.get(2)).intValue() : null;
return new GiftPurchaseResult(code, message, stock);
}
return new GiftPurchaseResult(0, "执行失败", null);
} catch (Exception e) {
e.printStackTrace();
return new GiftPurchaseResult(0, "系统错误: " + e.getMessage(), null);
}
}
}
5. Kafka消息生产者
@Component
public class GiftPurchaseProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Value("${gift.purchase.topic:gift-purchase-topic}")
private String topicName;
public void sendGiftPurchaseMessage(String userId, String giftInfo) {
GiftPurchaseRequest request = new GiftPurchaseRequest(Long.valueOf(userId), 10000L, "礼品名称");
String message = request.toString();
kafkaTemplate.send(topicName, userId, message);
}
}
6. 抢购服务实现
@Service
@Slf4j
public class GiftPurchaseService {
@Autowired
private RedisLuaExecutor redisLuaExecutor;
@Autowired
private GiftPurchaseProducer giftPurchaseProducer;
@Autowired
private RedissonClient redissonClient;
private static final int INITIAL_STOCK = 100;
private static final int EXPIRE_SECONDS = 86400; // 24小时过期
private static final String ACTIVITY_ID = "10000";
@PostConstruct
public void initStock() {
String stockKey = "gift:stock:" + ACTIVITY_ID;
Boolean hasStock = redisTemplate.hasKey(stockKey);
if (!hasStock || (Integer) redisTemplate.opsForValue().get(stockKey) == 0) {
redisTemplate.opsForValue().set(stockKey, INITIAL_STOCK);
redisTemplate.expire(stockKey, EXPIRE_SECONDS, TimeUnit.SECONDS);
}
}
public GiftPurchaseResult purchaseGift(Long userId) {
RLock lock = redissonClient.getLock("gift_lock_" + ACTIVITY_ID);
try {
boolean acquired = lock.tryLock(10, TimeUnit.SECONDS);
if (!acquired) {
return new GiftPurchaseResult(0, "系统繁忙,请稍后再试", null);
}
String giftInfo = String.format("{\"userId\": %d, \"activityId\": %d, \"giftName\": \"礼品名称\"}",
userId, 10000L);
GiftPurchaseResult result = redisLuaExecutor.executeGiftPurchase(
userId, giftInfo, ACTIVITY_ID, EXPIRE_SECONDS);
if (result.getCode() == 1) {
// 异步处理礼品发放
giftPurchaseProducer.sendGiftPurchaseMessage(userId.toString(), giftInfo);
log.info("用户 {} 抢购成功,剩余库存: {}", userId, result.getStock());
}
return result;
} catch (Exception e) {
log.error("抢购过程中发生异常", e);
return new GiftPurchaseResult(0, "系统错误: " + e.getMessage(), null);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
7. Kafka消费者
@Component
@Slf4j
public class GiftPurchaseConsumer {
@Value("${gift.purchase.topic:gift-purchase-topic}")
private String topicName;
@KafkaListener(topics = "${gift.purchase.topic:gift-purchase-topic}", groupId = "gift-purchase-group")
public void handleGiftPurchase(String message) {
try {
log.info("开始处理礼品发放消息: {}", message);
// 解析JSON消息
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(message);
Long userId = jsonNode.get("userId").asLong();
Long activityId = jsonNode.get("activityId").asLong();
// 这里执行实际的礼品发放逻辑
// 例如:调用外部API、更新数据库等
processGiftDelivery(userId, activityId);
log.info("礼品发放成功,用户ID: {}, 活动ID: {}", userId, activityId);
} catch (Exception e) {
log.error("处理礼品发放消息失败: {}", message, e);
}
}
private void processGiftDelivery(Long userId, Long activityId) {
// 实际的礼品发放逻辑
// 例如:发送邮件、短信通知,或者调用第三方礼品发放API
System.out.println("正在为用户 " + userId + " 发放活动 " + activityId + " 的礼品...");
}
}
8. 控制器
@RestController
@RequestMapping("/gift")
@Slf4j
public class GiftPurchaseController {
@Autowired
private GiftPurchaseService giftPurchaseService;
@PostMapping("/purchase/{userId}")
public ResponseEntity<GiftPurchaseResult> purchaseGift(@PathVariable Long userId) {
log.info("用户 {} 发起抢购请求", userId);
GiftPurchaseResult result = giftPurchaseService.purchaseGift(userId);
return ResponseEntity.ok(result);
}
@GetMapping("/stock/{activityId}")
public ResponseEntity<Integer> getStock(@PathVariable String activityId) {
String stockKey = "gift:stock:" + activityId;
Object stockObj = redisTemplate.opsForValue().get(stockKey);
Integer stock = stockObj != null ? (Integer) stockObj : 0;
return ResponseEntity.ok(stock);
}
}
9. 配置文件 (application.yml)
spring:
redis:
host: localhost
port: 6379
timeout: 10s
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: gift-purchase-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
gift:
purchase:
topic: gift-purchase-topic
server:
port: 8080
10. 启动类
@SpringBootApplication
@EnableKafka
public class GiftPurchaseApplication {
public static void main(String[] args) {
SpringApplication.run(GiftPurchaseApplication.class, args);
}
}
经过上述实现,基本可以进行抢购活动的进行,当然具体还要根据其具体业务,做响应的修改,这里只是一个demo,只是说明一种思想,抢购的实践方案。
如此设计的优点是
- Redis分布式锁: 使用Redisson确保并发安全
- Lua脚本原子操作: 库存扣减、用户记录、Stream写入一步完成
- Kafka异步处理: 抢购成功后异步处理礼品发放
- Redis Stream: 消息持久化和顺序处理
- 防重复领取: 使用Set数据结构防止同一用户多次领取
- 自动过期: Redis键值设置过期时间,避免数据堆积
更多推荐




所有评论(0)