1. Redis在秒杀系统中的实战应用
秒杀系统是电商平台最考验技术架构的场景之一,典型特点是瞬时流量极高(10万+QPS)、库存有限且必须保证不超卖。我在多个电商平台的秒杀系统设计中,Redis始终扮演着核心角色。下面从五个关键维度分享具体实现方案。
1.1 原子性库存控制方案
库存控制是秒杀系统的命脉所在。早期我们采用"查询+扣减"两步操作,在1000QPS测试环境下看似正常,但上线后在大流量下出现了严重超卖。根本原因在于非原子操作导致的竞态条件。
解决方案一:INCRBY负数操作
java复制// 库存初始值为100(正数),扣减时INCRBY -1
Long remain = redisTemplate.opsForValue().increment("seckill:stock:1001", -1);
if (remain == null || remain < 0) {
// 扣减后值小于0说明库存不足
redisTemplate.opsForValue().increment("seckill:stock:1001", 1); // 回滚
return "秒杀失败";
}
这种方案的优点是实现简单,但需要注意:
- 初始库存必须设置为正数
- 扣减失败时需要回滚操作
- 需要配合WATCH/MULTI实现事务
解决方案二:Lua脚本方案
lua复制local stock = tonumber(redis.call('get', KEYS[1]))
if stock and stock > 0 then
redis.call('decr', KEYS[1])
return 1
else
return 0
end
Java调用示例:
java复制String script = "local stock = tonumber(...)"; // 完整脚本见上文
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("seckill:stock:1001")
);
Lua脚本的优势在于:
- 整个操作在Redis中原子执行
- 减少网络往返时间
- 避免客户端处理竞态条件的复杂性
关键经验:生产环境中建议使用Lua脚本方案,我们在某次大促中实现了200万QPS的库存扣减,系统稳定运行。
1.2 多层级限流策略
秒杀系统必须实施严格的流量控制,我们的策略分为三个层次:
用户维度限流
java复制String userLimitKey = "seckill:limit:uid_" + userId + ":sku_" + skuId;
Long count = redisTemplate.opsForValue().increment(userLimitKey, 1);
if (count == 1) {
redisTemplate.expire(userLimitKey, 60, TimeUnit.SECONDS);
}
if (count > 1) {
return "您已参与过本次秒杀";
}
IP维度限流
java复制String ipLimitKey = "seckill:limit:ip_" + userIp;
Long ipCount = redisTemplate.opsForValue().increment(ipLimitKey, 1);
if (ipCount == 1) {
redisTemplate.expire(ipLimitKey, 60, TimeUnit.SECONDS);
}
if (ipCount > 5) { // 单个IP限制5次
return "操作过于频繁";
}
全局限流(令牌桶算法)
java复制// 使用Redis实现令牌桶
String globalLimitKey = "seckill:global:limit";
Long current = System.currentTimeMillis();
// 每个请求获取1个令牌,每秒补充10000个令牌
Long result = redisTemplate.execute(globalLimitScript,
Arrays.asList(globalLimitKey),
current.toString(), "10000", "1", "1000");
if (result == 0) {
return "秒杀活动太火爆,请稍后再试";
}
1.3 异步化订单处理
秒杀成功的订单必须异步处理,我们采用Redis Stream作为消息队列:
java复制// 秒杀成功写入Stream
Map<String, String> fields = new HashMap<>();
fields.put("userId", userId);
fields.put("skuId", skuId);
fields.put("time", String.valueOf(System.currentTimeMillis()));
redisTemplate.opsForStream().add("seckill:orders", fields);
// 消费者组配置
StreamReadOptions readOptions = StreamReadOptions.empty()
.block(Duration.ofSeconds(1))
.count(10);
StreamOffset<String> streamOffset = StreamOffset.create("seckill:orders", ReadOffset.lastConsumed());
while (true) {
List<MapRecord<String, String, String>> records = redisTemplate.opsForStream()
.read(Consumer.from("group1", "consumer1"),
readOptions,
streamOffset);
// 处理订单落库
}
这种架构的优势:
- 削峰填谷:将瞬时高峰转换为平稳消费
- 解耦合:秒杀逻辑与订单处理分离
- 可扩展:可以增加多个消费者提高处理能力
2. 电商缓存体系构建实战
电商系统的缓存设计直接影响用户体验和系统稳定性。经过多个项目的实践,我总结出以下关键要点。
2.1 缓存一致性解决方案
方案对比表
| 方案 | 适用场景 | 实现复杂度 | 一致性强度 | 性能影响 |
|---|---|---|---|---|
| 先更新DB后删除缓存 | 读多写少 | 低 | 最终一致 | 小 |
| 双删策略 | 写多读少 | 中 | 较强 | 中等 |
| 异步监听binlog | 读写均衡 | 高 | 最终一致 | 小 |
| 加分布式锁 | 强一致性要求 | 高 | 强一致 | 大 |
推荐实现(先更新DB后删除缓存)
java复制@Transactional
public void updateProduct(Product product) {
// 1. 更新数据库
productDao.update(product);
// 2. 删除缓存
redisTemplate.delete("product:" + product.getId());
// 3. 发送MQ确保删除成功(可选)
mqSender.sendCacheDeleteMessage("product:" + product.getId());
}
异常处理要点
- 数据库更新成功但缓存删除失败时,可以通过消息队列重试
- 设置缓存删除的最大重试次数(建议3次)
- 最终仍失败时记录日志人工介入
2.2 缓存穿透防护体系
多级防护方案
- 空值缓存
java复制public Product getProduct(Long id) {
String key = "product:" + id;
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
if ("".equals(value)) return null; // 空值标识
return JSON.parseObject(value, Product.class);
}
Product product = productDao.getById(id);
if (product == null) {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 1, TimeUnit.HOURS);
return product;
}
- 布隆过滤器
java复制// 初始化布隆过滤器
RBloomFilter<String> filter = redisson.getBloomFilter("product:filter");
filter.tryInit(1000000L, 0.01); // 预期元素100万,误判率1%
// 预热数据
List<Long> allIds = productDao.getAllIds();
allIds.forEach(id -> filter.add("product:" + id));
// 查询时先检查过滤器
public Product getProductWithFilter(Long id) {
String key = "product:" + id;
if (!filter.contains(key)) {
return null; // 肯定不存在
}
// ...后续逻辑与普通查询相同
}
- 接口限流
对不存在的ID频繁查询进行限流:
java复制String invalidKey = "invalid:query:" + id;
Long count = redisTemplate.opsForValue().increment(invalidKey, 1);
if (count == 1) {
redisTemplate.expire(invalidKey, 1, TimeUnit.MINUTES);
}
if (count > 10) { // 1分钟内查询超过10次
throw new BusinessException("请求过于频繁");
}
2.3 热点Key处理方案
识别热点Key
- 监控Redis的CPU使用率突增
- 使用Redis的hotkeys命令(生产环境慎用)
- 客户端统计Key访问频率
解决方案
java复制public String getHotProductInfo(Long productId) {
String key = "hot:product:" + productId;
// 1. 尝试从缓存获取
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 2. 获取分布式锁
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
// 2.1 未获取到锁,短暂等待后重试
Thread.sleep(100);
return getHotProductInfo(productId);
}
try {
// 3. 二次检查(防止其他线程已经更新)
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 4. 查询数据库
Product product = productDao.getById(productId);
if (product == null) {
return null;
}
// 5. 更新缓存(设置较长过期时间)
String productJson = JSON.toJSONString(product);
redisTemplate.opsForValue().set(key, productJson, 1, TimeUnit.DAYS);
return productJson;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
3. 分布式锁的深度实践
分布式锁是分布式系统中的重要基础组件,Redis因其高性能常被用作锁服务。但在实际应用中存在诸多陷阱。
3.1 正确实现Redis分布式锁
基础实现
java复制public boolean tryLock(String lockKey, String requestId, long expireTime) {
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
}
public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1;
}
Redisson实现(推荐)
java复制// 获取锁
RLock lock = redissonClient.getLock("order:lock:" + orderId);
try {
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 执行业务逻辑
}
} finally {
lock.unlock();
}
3.2 锁续期机制
对于执行时间不确定的长任务,需要实现锁续期:
java复制private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
public void renewLock(String lockKey, String requestId, long expireTime) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
"return 0 " +
"end";
executorService.scheduleAtFixedRate(() -> {
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId,
String.valueOf(expireTime/1000)
);
}, expireTime/3, expireTime/3, TimeUnit.MILLISECONDS);
}
3.3 集群环境下的RedLock
在Redis集群环境下,为避免主从切换导致的锁失效,可以采用RedLock算法:
java复制Config config1 = new Config();
config1.useSingleServer().setAddress("redis://node1:6379");
RedissonClient client1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://node2:6379");
RedissonClient client2 = Redisson.create(config2);
// 创建RedLock
RLock lock1 = client1.getLock("lock");
RLock lock2 = client2.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2);
try {
// 加锁(需要大部分节点成功)
boolean locked = redLock.tryLock();
if (locked) {
// 执行业务逻辑
}
} finally {
redLock.unlock();
}
4. Redis高可用架构设计
4.1 哨兵模式部署
典型架构
code复制 +-------------+
| Sentinel1 |
+------+------+
|
+---------+ +-----+-----+ +---------+
| Master +-+ Slave1 +-+ Slave2 |
+---------+ +----------+ +---------+
配置要点
- 至少部署3个Sentinel节点(最好分布在不同机器)
- 设置合理的down-after-milliseconds(通常5000ms)
- 配置parallel-syncs控制主从同步并发数
4.2 Cluster模式最佳实践
数据分片策略
java复制// 使用hash tag确保相关key分配到同一slot
String userKey = "user:{1001}:info";
String orderKey = "order:{1001}:123";
// 这两个key会被分配到同一slot因为使用了相同的hash tag {1001}
集群扩容步骤
- 准备新节点并启动Redis服务
- 使用redis-cli --cluster add-node添加新节点
- 使用redis-cli --cluster reshard迁移数据
- 平衡各个节点的slot分布
4.3 持久化策略选择
RDB配置
code复制save 900 1 # 15分钟内有至少1个key变化
save 300 10 # 5分钟内有至少10个key变化
save 60 10000 # 1分钟内有至少10000个key变化
stop-writes-on-bgsave-error yes
rdbcompression yes
AOF配置
code复制appendonly yes
appendfsync everysec # 折中方案
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
混合持久化配置:
code复制aof-use-rdb-preamble yes # Redis 4.0+支持
5. 性能优化实战技巧
5.1 内存优化方案
缩减key长度
java复制// 不推荐
String key = "user:order:history:" + userId + ":" + date;
// 推荐
String key = "u:o:h:" + userId + ":" + date;
使用hash代替string
java复制// 存储用户信息
redisTemplate.opsForHash().putAll("user:" + userId,
Map.of("name", userName, "age", userAge));
ziplist优化
code复制hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
set-max-intset-entries 512
5.2 延迟问题排查
慢查询监控
code复制slowlog-log-slower-than 10000 # 记录超过10ms的查询
slowlog-max-len 128 # 保留128条慢查询
热点key发现
bash复制redis-cli --hotkeys
# 或使用monitor命令(生产环境慎用)
大key扫描
bash复制redis-cli --bigkeys
# 或使用memory usage命令
redis-cli memory usage keyname
5.3 连接池优化
Lettuce配置示例
properties复制spring.redis.lettuce.pool.max-active=50
spring.redis.lettuce.pool.max-idle=20
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-wait=1000
spring.redis.lettuce.shutdown-timeout=100
Jedis配置示例
java复制JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);
config.setMaxIdle(20);
config.setMinIdle(5);
config.setMaxWaitMillis(1000);
config.setTestOnBorrow(true);
6. 监控与告警体系
6.1 关键监控指标
基础指标
- 内存使用率(used_memory)
- 连接数(connected_clients)
- QPS(instantaneous_ops_per_sec)
- 命中率(keyspace_hits/keyspace_misses)
高级指标
- 复制延迟(master_repl_offset)
- 持久化状态(rdb_last_bgsave_status)
- 键空间统计(expired_keys, evicted_keys)
6.2 Prometheus监控配置
redis_exporter部署
bash复制./redis_exporter -redis.addr localhost:6379 -web.listen-address :9121
Grafana仪表盘
- 导入Redis官方仪表盘(ID 763)
- 设置关键告警规则:
- 内存使用超过90%
- 连接数超过最大限制的80%
- 命中率低于90%
- 主从复制延迟超过10MB
6.3 容量规划建议
内存估算公式
code复制总内存 = (key数量 × (key大小 + value大小 + 100字节开销)) × 1.3
分片策略
- 每个分片不超过25GB
- 每个分片QPS不超过10万
- 主从节点分散在不同机架
7. 常见问题解决方案
7.1 缓存雪崩预防
多级缓存方案
java复制public Product getProductWithMultiCache(Long id) {
// 1. 检查本地缓存
Product product = localCache.get(id);
if (product != null) {
return product;
}
// 2. 检查Redis缓存
String redisKey = "product:" + id;
String json = redisTemplate.opsForValue().get(redisKey);
if (json != null) {
product = JSON.parseObject(json, Product.class);
localCache.put(id, product); // 回填本地缓存
return product;
}
// 3. 查询数据库
product = productDao.getById(id);
if (product != null) {
// 异步更新缓存
CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(product),
30 + new Random().nextInt(30), // 30-60秒随机过期
TimeUnit.SECONDS
);
});
}
return product;
}
7.2 大Key拆分方案
hash分片示例
java复制// 原始大key
String bigKey = "user:data:" + userId;
// 拆分为多个hash key
String[] hashKeys = {
"user:data:" + userId + ":base",
"user:data:" + userId + ":contact",
"user:data:" + userId + ":preference"
};
// 分片存储
redisTemplate.opsForHash().putAll(hashKeys[0], baseInfoMap);
redisTemplate.opsForHash().putAll(hashKeys[1], contactMap);
redisTemplate.opsForHash().putAll(hashKeys[2], preferenceMap);
7.3 热Key自动检测
实现原理
- 客户端统计key访问频率
- 定期上报到中心服务
- 对热点key进行特殊处理
示例代码
java复制public class HotKeyDetector {
private ConcurrentHashMap<String, AtomicLong> counter = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public void init() {
scheduler.scheduleAtFixedRate(this::reportHotKeys, 1, 1, TimeUnit.MINUTES);
}
public void increment(String key) {
counter.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
}
private void reportHotKeys() {
counter.entrySet().stream()
.filter(e -> e.getValue().get() > 1000) // 阈值1000次/分钟
.forEach(e -> {
// 上报到配置中心
configCenter.markHotKey(e.getKey());
// 重置计数器
e.getValue().set(0);
});
}
}
8. Redis与其他技术栈集成
8.1 Spring Cache整合
配置示例
java复制@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
使用示例
java复制@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productDao.getById(id);
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productDao.update(product);
}
8.2 分布式限流方案
Redis+Lua实现令牌桶
lua复制-- KEYS[1]: 限流key
-- ARGV[1]: 当前时间(毫秒)
-- ARGV[2]: 桶容量
-- ARGV[3]: 每次请求令牌数
-- ARGV[4]: 令牌添加速率(令牌/毫秒)
local rate = tonumber(ARGV[4])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[1])
local requested = tonumber(ARGV[3])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
local last_tokens = tonumber(redis.call("get", KEYS[1]))
if last_tokens == nil then
last_tokens = capacity
end
local last_refreshed = tonumber(redis.call("get", KEYS[1]..":ts"))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[1]..":ts", ttl, now)
return allowed and 1 or 0
8.3 二级缓存架构
Guava+Redis实现
java复制public class TwoLevelCache implements Cache {
private final Cache localCache;
private final RedisTemplate<String, Object> redisTemplate;
private final String name;
public TwoLevelCache(String name, RedisTemplate<String, Object> redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
this.localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
}
@Override
public Object get(Object key) {
// 1. 查本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 查Redis
String redisKey = name + ":" + key.toString();
value = redisTemplate.opsForValue().get(redisKey);
if (value != null) {
// 回填本地缓存
localCache.put(key, value);
}
return value;
}
@Override
public void put(Object key, Object value) {
String redisKey = name + ":" + key.toString();
// 先更新Redis
redisTemplate.opsForValue().set(redisKey, value, 1, TimeUnit.HOURS);
// 再更新本地缓存
localCache.put(key, value);
}
}
9. 生产环境检查清单
9.1 上线前检查项
-
配置检查
- 禁用危险命令(KEYS, FLUSHALL等)
- 设置合理的内存淘汰策略
- 确认持久化配置
-
安全加固
- 启用密码认证
- 绑定特定IP
- 禁用protected-mode
-
监控准备
- 部署redis_exporter
- 配置告警规则
- 准备容量规划
9.2 日常运维要点
-
定期巡检
- 内存碎片率(mem_fragmentation_ratio)
- 连接数波动
- 持久化状态
-
备份策略
- RDB每日全量备份
- AOF持续增量备份
- 异地备份方案
-
性能调优
- 大Key定期扫描
- 热点Key监控
- 慢查询分析
9.3 故障处理预案
-
内存溢出
- 临时解决方案:设置maxmemory-policy为allkeys-lru
- 根本解决:扩容或优化数据结构
-
主从同步失败
- 检查网络连接
- 检查主从配置
- 必要时重建从节点
-
集群脑裂
- 手动故障转移
- 检查quorum配置
- 验证数据一致性
10. Redis 6/7新特性应用
10.1 客户端缓存(Redis 6)
java复制// 服务端配置
client-tracking on
client-tracking-redirect-to 12345 // 客户端ID
// 客户端实现
Jedis jedis = new Jedis();
jedis.clientTrackingOn("12345", true);
// 当服务端数据变更时会发送invalidate消息
10.2 多线程IO(Redis 6)
配置项:
code复制io-threads 4
io-threads-do-reads yes
适用场景:
- 大value读写
- 高并发环境
- 网络延迟较高时
10.3 Functions(Redis 7)
lua复制#!lua name=mylib
local function multiply(x, y)
return x * y
end
redis.register_function('multiply', multiply)
调用方式:
bash复制redis-cli --eval multiply.lua
> FCALL multiply 2 3 4
(integer) 12
11. 替代方案对比
11.1 Redis vs Memcached
| 特性 | Redis | Memcached |
|---|---|---|
| 数据结构 | 丰富(String/Hash/List等) | 简单(Key-Value) |
| 持久化 | 支持 | 不支持 |
| 集群 | 原生支持 | 需要客户端实现 |
| 线程模型 | 单线程(Redis6+支持IO多线程) | 多线程 |
| 适用场景 | 复杂业务场景 | 简单缓存场景 |
11.2 Redis vs 本地缓存
组合使用建议
- 一级缓存:本地缓存(Caffeine/Guava)
- 超时时间短(秒级)
- 容量有限
- 二级缓存:Redis
- 数据全量存储
- 分布式共享
- 三级存储:数据库
- 数据持久化
- 最终一致性保障
11.3 Redis vs 专业时序数据库
对于监控、IoT等时序数据场景:
- Redis TimeSeries模块
- 优势:低延迟、高吞吐
- 劣势:存储成本高
- InfluxDB/TDengine
- 优势:压缩率高、查询功能强
- 劣势:写入延迟较高
12. 未来发展趋势
-
计算存储分离架构
- 计算节点无状态
- 存储层共享
- 动态扩缩容
-
持久内存应用
- 使用PMEM作为内存扩展
- 降低DRAM成本
- 保持高性能
-
AI集成
- 自动调优参数
- 智能预测扩容
- 异常检测
在实际项目落地时,需要根据具体业务特点选择合适的Redis使用模式。我们团队在电商、社交、金融等多个领域积累了丰富的Redis实战经验,以上方案都经过了大流量生产环境的验证。建议读者在应用时先进行充分的测试,并建立完善的监控体系。