好记忆不如烂笔头,能记下点东西,就记下点,有时间拿出来看看,也会发觉不一样的感受.

一,概况

1.介绍

2.关键信息

3.设计思路

二,核心实现

1.添加依赖

2.Lua脚本文件

3.消息实体

4. Redis Lua脚本执行器

5. Kafka消息生产者

6. 抢购服务实现

7. Kafka消费者

8. 控制器

9. 配置文件 (application.yml)

10. 启动类


一,概况

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,只是说明一种思想,抢购的实践方案。

如此设计的优点是
 

  1. Redis分布式锁: 使用Redisson确保并发安全
  2. Lua脚本原子操作: 库存扣减、用户记录、Stream写入一步完成
  3. Kafka异步处理: 抢购成功后异步处理礼品发放
  4. Redis Stream: 消息持久化和顺序处理
  5. 防重复领取: 使用Set数据结构防止同一用户多次领取
  6. 自动过期: Redis键值设置过期时间,避免数据堆积
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐