Java+Redis高可用电商系统:秒杀与优惠券模块架构设计与实战



本文将深入探讨基于Java+Redis的高可用电商系统中秒杀与优惠券模块的核心架构设计,通过完整的代码实例展示如何应对高并发场景。



1. 引言:高并发电商系统的挑战


在当今互联网时代,电商平台的秒杀活动和优惠券促销是吸引用户、提升转化的关键手段。这些场景往往伴随着极高的并发访问,如何保证系统的高可用性、数据一致性和性能成为技术设计的核心挑战。


传统架构的痛点:
- 数据库成为性能瓶颈,无法承受瞬时高并发
- 超卖问题导致库存不一致
- 优惠券被重复使用或超发
- 系统单点故障导致服务不可用


本文将基于Spring Boot + Redis技术栈,详细解析高可用电商系统中秒杀与优惠券模块的设计与实现。


2. 技术栈选型与整体架构


2.1 核心技术组件


```xml




org.springframework.boot
spring-boot-starter-data-redis




org.redisson
redisson-spring-boot-starter
3.27.0




org.apache.commons
commons-pool2

```


2.2 系统架构设计


```java
@Component
public class SeckillSystemConfig {


// Redis集群配置
@Bean
@ConfigurationProperties(prefix = "spring.redis")
public RedisClusterConfiguration redisClusterConfiguration() {
return new RedisClusterConfiguration();
}

// Lettuce连接工厂
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.useSsl().and()
.commandTimeout(Duration.ofSeconds(2))
.shutdownTimeout(Duration.ZERO)
.build();

return new LettuceConnectionFactory(redisClusterConfiguration(), clientConfig);
}

// RedisTemplate配置
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}

}
```


3. 秒杀模块核心设计与实现


3.1 秒杀业务流程设计


```java
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {


@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private RedissonClient redissonClient;

@Autowired
private OrderService orderService;

// 秒杀令牌生成
@Override
public String generateSeckillToken(Long userId, Long seckillId) {
String key = "seckill:token:" + seckillId + ":" + userId;
String token = UUID.randomUUID().toString().replace("-", "");

// 令牌有效期为10秒
redisTemplate.opsForValue().set(key, token, Duration.ofSeconds(10));
return token;
}

// 秒杀核心逻辑
@Override
public SeckillResponse executeSeckill(Long userId, Long seckillId, String token) {
// 1. 验证令牌
if (!validateToken(userId, seckillId, token)) {
return SeckillResponse.fail("非法请求或令牌已过期");
}

// 2. 校验用户重复购买
if (checkUserPurchased(userId, seckillId)) {
return SeckillResponse.fail("您已参与过本次秒杀");
}

// 3. 获取分布式锁
RLock lock = redissonClient.getLock("seckill:lock:" + seckillId);
try {
// 尝试加锁,最多等待100ms,锁持有时间5秒
if (lock.tryLock(100, 5000, TimeUnit.MILLISECONDS)) {
try {
// 4. 校验库存
Integer stock = getSeckillStock(seckillId);
if (stock == null || stock <= 0) {
return SeckillResponse.fail("商品已售罄");
}

// 5. 扣减库存
if (decrementStock(seckillId)) {
// 6. 创建订单
Order order = createSeckillOrder(userId, seckillId);

// 7. 记录用户购买记录
recordUserPurchase(userId, seckillId);

return SeckillResponse.success(order);
} else {
return SeckillResponse.fail("扣减库存失败");
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
return SeckillResponse.fail("系统繁忙,请重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SeckillResponse.fail("系统异常");
}
}

// Lua脚本实现原子性库存扣减
private static final String DECR_STOCK_SCRIPT =
"if redis.call('exists', KEYS[1]) == 1 then " +
" local stock = tonumber(redis.call('get', KEYS[1])) " +
" if stock > 0 then " +
" redis.call('decr', KEYS[1]) " +
" return 1 " +
" end " +
" return 0 " +
"end " +
"return -1";

private boolean decrementStock(Long seckillId) {
String stockKey = "seckill:stock:" + seckillId;
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(DECR_STOCK_SCRIPT);
script.setResultType(Long.class);

Long result = redisTemplate.execute(script, Collections.singletonList(stockKey));
return result != null && result == 1;
}

}
```


3.2 库存预热与缓存策略


```java
@Service
public class SeckillPreheatService {


@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private SeckillItemMapper seckillItemMapper;

// 秒杀开始前预热数据到Redis
@Async
public void preheatSeckillData(Long seckillId) {
SeckillItem seckillItem = seckillItemMapper.selectById(seckillId);
if (seckillItem == null) {
log.error("秒杀商品不存在: {}", seckillId);
return;
}

// 设置商品库存
String stockKey = "seckill:stock:" + seckillId;
redisTemplate.opsForValue().set(stockKey, seckillItem.getStock());

// 设置商品详情缓存
String itemKey = "seckill:item:" + seckillId;
redisTemplate.opsForValue().set(itemKey, seckillItem, Duration.ofHours(2));

// 设置秒杀开始结束时间
String timeKey = "seckill:time:" + seckillId;
Map<String, Object> timeMap = new HashMap<>();
timeMap.put("startTime", seckillItem.getStartTime().getTime());
timeMap.put("endTime", seckillItem.getEndTime().getTime());
redisTemplate.opsForHash().putAll(timeKey, timeMap);

log.info("秒杀商品数据预热完成: {}", seckillId);
}

// 获取秒杀商品信息
public SeckillItem getSeckillItem(Long seckillId) {
String key = "seckill:item:" + seckillId;
SeckillItem item = (SeckillItem) redisTemplate.opsForValue().get(key);

if (item == null) {
// 缓存未命中,从数据库加载
item = seckillItemMapper.selectById(seckillId);
if (item != null) {
redisTemplate.opsForValue().set(key, item, Duration.ofHours(1));
}
}
return item;
}

}
```


4. 优惠券模块设计与实现


4.1 优惠券发放与使用


```java
@Service
@Slf4j
public class CouponServiceImpl implements CouponService {


@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private CouponMapper couponMapper;

// 发放优惠券
@Override
public boolean grantCoupon(Long userId, Long couponId) {
String lockKey = "coupon:grant:lock:" + couponId;
RLock lock = redissonClient.getLock(lockKey);

try {
if (lock.tryLock(50, 10, TimeUnit.SECONDS)) {
// 检查优惠券库存
String stockKey = "coupon:stock:" + couponId;
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);

if (stock == null) {
// 从数据库加载库存
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) return false;

stock = coupon.getStock();
redisTemplate.opsForValue().set(stockKey, stock);
}

if (stock <= 0) {
return false;
}

// 使用Lua脚本原子性扣减库存
String luaScript =
"if redis.call('get', KEYS[1]) >= '1' then " +
" redis.call('decr', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";

DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);

Long result = redisTemplate.execute(script, Collections.singletonList(stockKey));

if (result != null && result == 1) {
// 发放优惠券给用户
String userCouponKey = "user:coupon:" + userId;
String couponInfo = couponId + ":" + System.currentTimeMillis();

redisTemplate.opsForSet().add(userCouponKey, couponInfo);
redisTemplate.expire(userCouponKey, Duration.ofDays(30));

log.info("用户{}成功领取优惠券{}", userId, couponId);
return true;
}
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

// 使用优惠券
@Override
public boolean useCoupon(Long userId, Long couponId, String orderId) {
String userCouponKey = "user:coupon:" + userId;
String usedKey = "coupon:used:" + couponId + ":" + userId;

// 检查优惠券是否已使用
if (redisTemplate.hasKey(usedKey)) {
return false;
}

// 检查用户是否拥有该优惠券
Boolean hasCoupon = redisTemplate.opsForSet().isMember(userCouponKey,
couponId + ":");

if (Boolean.TRUE.equals(hasCoupon)) {
// 标记优惠券为已使用
redisTemplate.opsForValue().set(usedKey, orderId, Duration.ofDays(30));

// 从用户优惠券集合中移除(或标记为已使用)
String pattern = couponId + ":";
Set<Object> userCoupons = redisTemplate.opsForSet().members(userCouponKey);

for (Object coupon : userCoupons) {
if (coupon.toString().startsWith(couponId + ":")) {
redisTemplate.opsForSet().remove(userCouponKey, coupon);
break;
}
}

log.info("用户{}使用优惠券{}于订单{}", userId, couponId, orderId);
return true;
}

return false;
}

}
```


4.2 优惠券核销与防刷策略


```java
@Service
public class CouponValidationService {


@Autowired
private RedisTemplate<String, Object> redisTemplate;

// 优惠券防刷限制
public boolean checkRateLimit(Long userId, Long couponId) {
String limitKey = "coupon:limit:" + couponId + ":" + userId;
String dailyLimitKey = "coupon:daily_limit:" + couponId + ":" +
LocalDate.now().toString();

// 个人总限制
Long userTotal = redisTemplate.opsForValue().increment(limitKey, 1);
if (userTotal != null && userTotal == 1) {
redisTemplate.expire(limitKey, Duration.ofDays(30)); // 30天过期
}

// 每日限制
Long dailyCount = redisTemplate.opsForValue().increment(dailyLimitKey, 1);
if (dailyCount != null && dailyCount == 1) {
redisTemplate.expire(dailyLimitKey, Duration.ofDays(1));
}

Coupon coupon = getCouponInfo(couponId);
if (coupon == null) return false;

// 检查限制
if (coupon.getUserLimit() > 0 && userTotal > coupon.getUserLimit()) {
return false;
}

if (coupon.getDailyLimit() > 0 && dailyCount > coupon.getDailyLimit()) {
return false;
}

return true;
}

// 批量核销优惠券
public BatchCouponResult batchVerifyCoupons(BatchCouponRequest request) {
String batchKey = "coupon:batch:" + UUID.randomUUID().toString();
BatchCouponResult result = new BatchCouponResult();

// 使用Redis事务保证原子性
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();

try {
for (CouponVerifyItem item : request.getItems()) {
String verifyKey = "coupon:verify:" + item.getCouponCode();

// 检查优惠券状态
if (!redisTemplate.hasKey(verifyKey)) {
// 设置优惠券为已核销
redisTemplate.opsForValue().set(verifyKey,
item.getOrderId(), Duration.ofHours(24));
result.addSuccess(item.getCouponCode());
} else {
result.addFailed(item.getCouponCode(), "优惠券已核销");
}
}

redisTemplate.exec();
return result;
} catch (Exception e) {
redisTemplate.discard();
throw new RuntimeException("批量核销失败", e);
} finally {
redisTemplate.setEnableTransactionSupport(false);
}
}

}
```


5. 高可用架构保障


5.1 Redis集群配置与故障转移


```yaml


application-redis.yml


spring:
redis:
cluster:
nodes:
- 192.168.1.101:6379
- 192.168.1.102:6379
- 192.168.1.103:6379
max-redirects: 3
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 1000ms
cluster:
refresh:
adaptive: true
period: 2000ms
timeout: 1000ms
```


5.2 熔断降级策略


```java
@Component
public class RedisCircuitBreaker {


private final CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.ringBufferSizeInHalfOpenState(10)
.ringBufferSizeInClosedState(100)
.build();

private final CircuitBreaker circuitBreaker = CircuitBreaker.of("redis", config);

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private DatabaseFallbackService fallbackService;

public <T> T executeWithCircuitBreaker(Supplier<T> redisOperation,
String operationName) {
return circuitBreaker.executeSupplier(() -> {
try {
return redisOperation.get();
} catch (RedisConnectionFailureException e) {
log.warn("Redis操作失败,触发熔断: {}", operationName);
throw e;
}
});
}

// 降级到数据库的方案
public boolean seckillWithFallback(Long userId, Long seckillId, String token) {
try {
return executeWithCircuitBreaker(() -> {
// Redis秒杀逻辑
return doSeckillInRedis(userId, seckillId, token);
}, "seckill_operation");
} catch (CallNotPermittedException e) {
// 熔断开启,降级到数据库方案
log.info("触发熔断,降级到数据库方案");
return fallbackService.seckillInDatabase(userId, seckillId);
}
}

}
```


5.3 监控与告警


```java
@Component
public class RedisMonitor {


@Autowired
private RedisTemplate<String, Object> redisTemplate;

// 监控Redis关键指标
public void monitorRedisHealth() {
// 连接数监控
String info = redisTemplate.execute((RedisCallback<String>) connection ->
connection.info("clients").get("connected_clients"));

// 内存使用监控
String memoryInfo = redisTemplate.execute((RedisCallback<String>) connection ->
connection.info("memory").get("used_memory"));

// 慢查询监控
List<Object> slowLogs = redisTemplate.opsForList().range("slowlog", 0, -1);

// 发送到监控系统
sendToMonitorSystem(info, memoryInfo, slowLogs);
}

// 自定义监控指标
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "ecommerce-seckill",
"module", "redis-cache"
);
}

}
```


6. 性能优化实战


6.1 Pipeline批量操作优化


```java
@Service
public class RedisPipelineService {


@Autowired
private RedisTemplate<String, Object> redisTemplate;

// 使用pipeline批量查询用户优惠券
public Map<String, Boolean> batchCheckUserCoupons(Long userId, List<Long> couponIds) {
return redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
String userCouponKey = "user:coupon:" + userId;

for (Long couponId : couponIds) {
String pattern = couponId + ":";
connection.sIsMember(userCouponKey.getBytes(), pattern.getBytes());
}
return null;
}
});
}

// 批量获取秒杀商品信息
public List<SeckillItem> batchGetSeckillItems(List<Long> seckillIds) {
List<Object> results = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (Long seckillId : seckillIds) {
String key = "seckill:item:" + seckillId;
connection.get(key.getBytes());
}
return null;
}
});

return results.stream()
.map(item -> (SeckillItem) item)
.collect(Collectors.toList());
}

}
```


7. 总结与最佳实践


本文详细介绍了基于Java+Redis的高可用电商系统秒杀与优惠券模块的设计与实现。关键要点总结如下:


7.1 核心技术要点



  1. 原子操作保障:使用Lua脚本确保库存扣减的原子性

  2. 分布式锁应用:通过Redisson实现分布式环境下的互斥访问

  3. 缓存预热策略:提前加载热点数据到Redis,降低数据库压力

  4. 熔断降级机制:在Redis不可用时自动降级到数据库方案


7.2 性能优化实践



  1. Pipeline批量操作:减少网络往返时间,提升批量查询效率

  2. 连接池优化:合理配置连接池参数,避免连接瓶颈

  3. 集群监控:实时监控Redis集群状态,及时发现问题


7.3 高可用保障



  1. 多级缓存策略:本地缓存+分布式缓存组合使用

  2. 故障转移机制:主从切换、集群自动故障恢复

  3. 容量规划:根据业务峰值合理规划Redis集群容量


通过本文的架构设计和代码实现,电商系统可以支撑万级甚至十万级的并发秒杀场景,同时保证系统的高可用性和数据一致性。在实际项目中,还需要根据具体业务需求进行适当的调整和优化。


参考资料:
- Spring官方文档:Spring Data Redis
- Redis官方文档:Cluster Tutorial
- 阿里云开发者社区:Redis最佳实践
- GitHub开源项目:redisson、resilience4j



本文详细代码示例已上传至GitHub,欢迎Star和贡献:https://github.com/example/ecommerce-seckill



Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐