前言
Redis 作为高性能内存数据库,已广泛应用于缓存、会话管理、消息队列等场景。然而,要充分发挥其价值,需深入理解其数据结构特性与最佳实践。本文将结合典型应用场景,通过 Java 代码示例详细讲解 Redis 的实战技巧,帮助开发者构建高效、稳定的分布式系统。
–
一、缓存设计与模式深度实践
1. 缓存穿透、击穿、雪崩解决方案
(1)缓存穿透(大量查询不存在的 Key)
问题:恶意攻击或无效查询导致请求直达数据库
解决方案:
- 布隆过滤器预校验:
// 初始化布隆过滤器(Guava实现) BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.forName("UTF-8")), 100000, 0.01); // 写入时添加Key到布隆过滤器 public void addToBloomFilter(String key) { bloomFilter.put(key); jedis.sadd("bloom:keys", key); // Redis存储全量Key(可选,用于定期重建过滤器) } // 查询时先校验布隆过滤器 public boolean checkBloomFilter(String key) { return bloomFilter.mightContain(key) || jedis.sismember("bloom:keys", key); }
(2)缓存击穿(热点 Key 过期瞬间大量请求)
问题:单个热点 Key 过期时,大量请求同时穿透到数据库
解决方案:
- 分布式锁保护(见本文分布式锁章节)
- 逻辑过期 + 异步重建:
// 带逻辑过期的缓存结构 public class CachedValue { private String value; private long expireTime; // 逻辑过期时间(非Redis TTL) } // 获取缓存(异步重建) public String getWithAsyncReload(String key) { CachedValue cached = (CachedValue) jedis.get(key); if (cached != null && System.currentTimeMillis() < cached.expireTime) { return cached.value; } String lockKey = "lock:reload:" + key; if (jedis.set(lockKey, "1", "NX", "EX", 10) != null) { try { String newValue = loadFromDB(key); CachedValue newCached = new CachedValue(newValue, System.currentTimeMillis() + 3600000); jedis.set(key, newCached, "NX", "EX", 3600); // Redis TTL略长于逻辑过期时间 return newValue; } finally { jedis.del(lockKey); } } else { // 等待100ms后重试(避免惊群效应) Thread.sleep(100); return getWithAsyncReload(key); } }
(3)缓存雪崩(大量 Key 同时过期)
问题:大面积缓存失效导致数据库压力突增
解决方案:
- 随机化过期时间:
// 设置带随机偏移的过期时间 jedis.setex(key, 3600 + ThreadLocalRandom.current().nextInt(600), value);
- 热点 Key 永不过期 + 异步更新:
// 永不过期,但通过异步任务定期更新 ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(() -> { String newValue = loadFromDB(key); jedis.set(key, newValue); // 无TTL,手动控制更新频率 }, 0, 30, TimeUnit.MINUTES);
2. 缓存更新策略对比与实现
(1)失效模式(Cache-Aside)
流程:更新数据库后删除缓存
// 双写一致性保障(延迟双删)
public void updateWithInvalidation(String key, String value) {
// 1. 更新数据库
db.update(key, value);
// 2. 删除缓存(主删)
jedis.del(key);
// 3. 异步延迟删除(解决并发脏读)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 等待可能的脏读请求完成
jedis.del(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
(2)更新模式(Write-Through)
流程:先更新缓存,再更新数据库(适合对一致性要求极高场景)
public void updateWithWriteThrough(String key, String value) {
// 1. 更新缓存
jedis.setex(key, 3600, value);
// 2. 更新数据库(可结合事务)
db.update(key, value);
}
(3)刷新模式(Refresh-Ahead)
流程:在缓存过期前主动刷新
// 基于Redisson的自动刷新锁
RedissonClient redisson = Redisson.create();
RLock lock = redisson.getLock("refresh:" + key);
public String getWithAutoRefresh(String key) {
String value = jedis.get(key);
if (value == null) {
lock.lock(30, TimeUnit.SECONDS);
try {
value = jedis.get(key);
if (value == null) {
value = loadFromDB(key);
jedis.setex(key, 3600, value);
}
// 启动异步刷新任务
redisson.getScheduler().schedule(() -> {
lock.lock();
try {
String newValue = loadFromDB(key);
jedis.setex(key, 3600, newValue);
} finally {
lock.unlock();
}
}, 300, TimeUnit.SECONDS); // 提前5分钟刷新
return value;
} finally {
lock.unlock();
}
}
return value;
}
二、分布式锁深化实践
1. 基于 SETNX 的简单分布式锁
// 基础实现(需手动释放锁)
public class SimpleRedisLock {
private final Jedis jedis;
private final String lockKey;
private final String requestId;
private final long expireTime;
public SimpleRedisLock(Jedis jedis, String lockKey, long expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.requestId = UUID.randomUUID().toString();
this.expireTime = expireTime;
}
public boolean acquire() {
// SETNX + 过期时间(避免死锁)
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
return "OK".equals(result);
}
public void release() {
// 验证并删除(原子性保障)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, requestId);
}
}
// 使用示例
try (Jedis jedis = jedisPool.getResource()) {
SimpleRedisLock lock = new SimpleRedisLock(jedis, "resource:lock", 10);
if (lock.acquire()) {
// 执行临界区代码
}
}
2. RedLock 算法完整实现(基于多节点)
// RedLock核心逻辑
public class RedLock {
private final List<Jedis> jedisInstances;
private final long lockExpire;
private final int retryCount;
public RedLock(List<Jedis> jedisInstances, long lockExpire, int retryCount) {
this.jedisInstances = jedisInstances;
this.lockExpire = lockExpire;
this.retryCount = retryCount;
}
public String acquire() {
String lockValue = UUID.randomUUID().toString();
long start = System.currentTimeMillis();
int acquiredNodes = 0;
try {
for (int i = 0; i < retryCount; i++) {
acquiredNodes = 0;
for (Jedis jedis : jedisInstances) {
if ("OK".equals(jedis.set(lockKey, lockValue, "NX", "EX", lockExpire))) {
acquiredNodes++;
}
}
if (acquiredNodes > jedisInstances.size() / 2) {
return lockValue; // 获得多数节点锁
}
// 等待重试
Thread.sleep(100);
}
} finally {
if (acquiredNodes > 0) {
release(lockValue); // 释放已获取的锁
}
}
return null;
}
private void release(String lockValue) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedisInstances.forEach(jedis -> jedis.eval(script, 1, lockKey, lockValue));
}
}
3. 可重入分布式锁
// 可重入分布式锁实现
public class ReentrantRedisLock {
private final JedisCluster jedisCluster;
private final String lockKey;
private final long lockExpire;
private final ThreadLocal<LockInfo> threadLocal = new ThreadLocal<>();
private static class LockInfo {
String requestId;
int lockCount;
public LockInfo(String requestId) {
this.requestId = requestId;
this.lockCount = 1;
}
}
public ReentrantRedisLock(JedisCluster jedisCluster, String lockKey, long lockExpire) {
this.jedisCluster = jedisCluster;
this.lockKey = lockKey;
this.lockExpire = lockExpire;
}
public boolean tryLock() {
LockInfo lockInfo = threadLocal.get();
if (lockInfo != null) {
lockInfo.lockCount++;
return true;
}
String requestId = UUID.randomUUID().toString();
String result = jedisCluster.set(lockKey, requestId, "NX", "EX", lockExpire);
if ("OK".equals(result)) {
threadLocal.set(new LockInfo(requestId));
return true;
}
return false;
}
public void unlock() {
LockInfo lockInfo = threadLocal.get();
if (lockInfo == null) {
throw new IllegalMonitorStateException("未获取锁");
}
if (--lockInfo.lockCount > 0) {
return;
}
try {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedisCluster.eval(script, 1, lockKey, lockInfo.requestId);
} finally {
threadLocal.remove();
}
}
}
三、会话管理
1. 基于 Redis 的会话存储
// 会话管理器实现
public class RedisSessionManager {
private final JedisCluster jedisCluster;
private final String sessionPrefix = "session:";
private final int sessionTimeout = 1800; // 30分钟
public RedisSessionManager(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
}
public void createSession(String sessionId, Map<String, Object> attributes) {
String key = sessionPrefix + sessionId;
Map<String, String> stringAttributes = attributes.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> serialize(e.getValue())
));
jedisCluster.hmset(key, stringAttributes);
jedisCluster.expire(key, sessionTimeout);
}
public Map<String, Object> getSession(String sessionId) {
String key = sessionPrefix + sessionId;
Map<String, String> stringAttributes = jedisCluster.hgetAll(key);
if (stringAttributes.isEmpty()) {
return Collections.emptyMap();
}
// 延长会话有效期
jedisCluster.expire(key, sessionTimeout);
return stringAttributes.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> deserialize(e.getValue())
));
}
public void invalidateSession(String sessionId) {
jedisCluster.del(sessionPrefix + sessionId);
}
private String serialize(Object obj) {
// 使用JSON序列化
return JSON.toJSONString(obj);
}
private Object deserialize(String str) {
// 反序列化为Map
return JSON.parseObject(str, Object.class);
}
}
2. 会话共享配置
// Spring Boot配置示例
@Configuration
public class SessionConfig extends AbstractHttpSessionApplicationInitializer {
@Bean
public JedisConnectionFactory connectionFactory() {
RedisClusterConfiguration config = new RedisClusterConfiguration(
Arrays.asList("node1:7000", "node2:7001", "node3:7002"));
return new JedisConnectionFactory(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
四、限流与排行榜、计数器实战
1. 滑动窗口限流
// 滑动窗口限流实现
public class SlidingWindowRateLimiter {
private final JedisCluster jedisCluster;
private final String keyPrefix = "rate_limit:";
private final int windowSize; // 窗口大小(毫秒)
private final int limit; // 最大请求数
public SlidingWindowRateLimiter(JedisCluster jedisCluster, int windowSize, int limit) {
this.jedisCluster = jedisCluster;
this.windowSize = windowSize;
this.limit = limit;
}
public boolean tryAcquire(String identifier) {
String key = keyPrefix + identifier;
long now = System.currentTimeMillis();
// 使用ZSET存储请求时间戳
Pipeline pipeline = (Pipeline) jedisCluster.getClusterNodes().values().iterator().next().getResource().pipelined();
pipeline.zremrangeByScore(key, 0, now - windowSize); // 移除窗口外的记录
pipeline.zadd(key, now, String.valueOf(now)); // 添加当前请求
pipeline.expire(key, windowSize / 1000 + 1); // 设置过期时间
pipeline.zcard(key); // 获取窗口内请求数
List<Object> results = pipeline.syncAndReturnAll();
long count = (Long) results.get(3);
return count <= limit;
}
}
2. 令牌桶算法
// 令牌桶限流实现
public class TokenBucketRateLimiter {
private final JedisCluster jedisCluster;
private final String keyPrefix = "token_bucket:";
private final long rate; // 令牌生成速率(个/秒)
private final long capacity; // 令牌桶容量
public TokenBucketRateLimiter(JedisCluster jedisCluster, long rate, long capacity) {
this.jedisCluster = jedisCluster;
this.rate = rate;
this.capacity = capacity;
}
public boolean tryAcquire(String identifier) {
String script =
"local tokens_key = KEYS[1] " +
"local timestamp_key = KEYS[2] " +
"local rate = tonumber(ARGV[1]) " +
"local capacity = tonumber(ARGV[2]) " +
"local now = tonumber(ARGV[3]) " +
"local requested = tonumber(ARGV[4]) " +
" " +
"local last_tokens = tonumber(redis.call('get', tokens_key) or capacity) " +
"local last_refreshed = tonumber(redis.call('get', timestamp_key) or 0) " +
" " +
"local delta = math.max(0, now - last_refreshed) " +
"local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000)) " +
"local allowed = filled_tokens >= requested " +
"local new_tokens = filled_tokens " +
"if allowed then " +
" new_tokens = filled_tokens - requested " +
"end " +
" " +
"redis.call('set', tokens_key, new_tokens) " +
"redis.call('set', timestamp_key, now) " +
" " +
"return allowed";
List<String> keys = Arrays.asList(
keyPrefix + identifier + ":tokens",
keyPrefix + identifier + ":timestamp"
);
List<String> args = Arrays.asList(
String.valueOf(rate),
String.valueOf(capacity),
String.valueOf(System.currentTimeMillis()),
"1" // 每次请求1个令牌
);
return (Long) jedisCluster.eval(script, keys, args) == 1;
}
}
3. Sorted Set 实现实时排行榜
(1)实时点赞排行榜
// 点赞操作(+1分)
public void like(String userId, String itemId, long score) {
jedis.zincrby("like:rank:" + itemId, 1, userId);
}
// 获取Top10点赞用户
public Set<Tuple> getTopLikes(String itemId, int limit) {
return jedis.zrevrangeWithScores("like:rank:" + itemId, 0, limit - 1);
}
// 移除点赞记录
public void unlike(String userId, String itemId) {
jedis.zrem("like:rank:" + itemId, userId);
}
(2)滑动窗口排行榜(按时间加权)
// 带时间衰减的分数计算
public void recordScore(String userId, double score) {
long now = System.currentTimeMillis();
double ttl = 86400000; // 1天有效期
double weight = Math.exp(-now / ttl); // 指数衰减
jedis.zadd("daily:rank", score * weight, userId);
}
4. 原子计数器进阶应用
(1)分段计数器(解决单 Key 性能瓶颈)
// 按小时分段计数
public long incrementHourlyCounter(String key) {
String hourlyKey = key + ":" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
return jedis.incr(hourlyKey);
}
// 汇总当天所有时段计数
public long getDailyCount(String key) {
String pattern = key + ":" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "*";
try (Scan scan = jedis.scan(ScanParams.scanParams().match(pattern).count(100))) {
long total = 0;
for (String k : scan.iterator()) {
total += Long.parseLong(jedis.get(k));
}
return total;
}
}
(2)全局唯一 ID 生成(INCR 优化)
// 带业务前缀的ID生成器
public class RedisIdGenerator {
private final Jedis jedis;
private final String prefix;
public RedisIdGenerator(Jedis jedis, String prefix) {
this.jedis = jedis;
this.prefix = prefix;
}
public long nextId() {
return jedis.incr(prefix + ":id");
}
}
五、会话缓存与分布式 Session 增强
1. 基于 Redis 的 Session 共享方案(Spring Session 整合)
(1)Spring Boot 配置
// application.properties
spring.session.store-type=redis
spring.redis.host=redis-host
spring.redis.port=6379
// 自定义Session属性序列化
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
(2)自定义 Session 管理器
public class CustomRedisSession {
private final RedisTemplate<String, Object> redisTemplate;
private final String sessionPrefix = "session:";
public CustomRedisSession(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
redisTemplate.setKeySerializer(new StringRedisSerializer());
}
public void createSession(String sessionId, Map<String, Object> attributes) {
redisTemplate.opsForHash().putAll(sessionPrefix + sessionId, attributes);
redisTemplate.expire(sessionPrefix + sessionId, 30, TimeUnit.MINUTES);
}
public Map<String, Object> getSession(String sessionId) {
return redisTemplate.opsForHash().entries(sessionPrefix + sessionId);
}
}
六、消息队列与任务队列实战
1. List 实现简单队列(生产者 - 消费者模型)
(1)阻塞式消费(支持优先级)
// 生产者(按优先级入队)
public void produce(String queueName, int priority, String message) {
String priorityQueue = priority + ":" + queueName;
jedis.lpush(priorityQueue, message);
}
// 消费者(多优先级队列监听)
public String consume(String... queues) {
List<String> result = jedis.blpop(0, queues); // 0表示永久阻塞
return result != null ? result.get(1) : null;
}
2. 基于 Streams 的高级队列
// 生产者
public String produce(String streamKey, Map<String, String> message) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.xadd(streamKey, "*", message);
}
}
// 消费者(独立模式)
public List<Map.Entry<String, List<Map.Entry<String, String>>>>> consume(String streamKey, String lastId, int count) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.xread(count, streamKey, lastId);
}
}
// 消费者组模式
public void consumeWithGroup(String streamKey, String groupName, String consumerName) {
try (Jedis jedis = jedisPool.getResource()) {
// 创建消费者组(如果不存在)
try {
jedis.xgroupCreate(streamKey, groupName, "0-0", true);
} catch (Exception e) {
// 组已存在
}
// 从消费者组获取消息
Map.Entry<String, List<Map.Entry<String, String>>>> result =
jedis.xreadGroup(groupName, consumerName, 1, 0, true, streamKey, ">");
if (result != null) {
String messageId = result.getValue().get(0).getKey();
// 处理消息
processMessage(result.getValue().get(0).getValue());
// 确认消息处理完成
jedis.xack(streamKey, groupName, messageId);
}
}
}
3. 基于 Sorted Set 的延迟队列设计
(1)延迟任务实现
// 延迟队列核心结构(score为到期时间戳)
public class DelayQueue {
private final Jedis jedis;
private final String queueKey = "delay:queue";
public DelayQueue(Jedis jedis) {
this.jedis = jedis;
}
// 提交延迟任务(延迟秒数)
public void submitDelayTask(String taskId, String payload, long delaySeconds) {
long expireTime = System.currentTimeMillis() + delaySeconds * 1000;
jedis.zadd(queueKey, expireTime, taskId + ":" + payload);
}
// 轮询获取到期任务
public List<String> pollExpiredTasks() {
long now = System.currentTimeMillis();
// 获取所有到期任务(score <= now)
Set<String> tasks = jedis.zrangeByScore(queueKey, 0, now, new ScanParams().count(100));
if (tasks.isEmpty()) {
return Collections.emptyList();
}
// 移除已处理任务
jedis.zrem(queueKey, tasks.toArray(new String[0]));
return new ArrayList<>(tasks);
}
}
// 消费者示例
public class DelayTaskConsumer {
private final DelayQueue delayQueue;
public DelayTaskConsumer(DelayQueue delayQueue) {
this.delayQueue = delayQueue;
}
public void start() {
while (true) {
List<String> tasks = delayQueue.pollExpiredTasks();
for (String task : tasks) {
processTask(task);
}
try {
Thread.sleep(100); // 控制轮询频率
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
六、分布式 ID 生成
1. 基于 INCR 的简单 ID 生成器
public class RedisIdGenerator {
private final JedisCluster jedisCluster;
private final String keyPrefix = "id_generator:";
private final long initialValue;
public RedisIdGenerator(JedisCluster jedisCluster, String businessType, long initialValue) {
this.jedisCluster = jedisCluster;
this.initialValue = initialValue;
// 初始化ID计数器
jedisCluster.setnx(keyPrefix + businessType, String.valueOf(initialValue));
}
public long nextId(String businessType) {
return jedisCluster.incr(keyPrefix + businessType);
}
}
2. Snowflake 算法实现
public class SnowflakeIdGenerator {
private final JedisCluster jedisCluster;
private final String workerIdKey = "snowflake:worker_id";
private final String datacenterIdKey = "snowflake:datacenter_id";
private final long workerId;
private final long datacenterId;
private final long startTimeStamp = 1609459200000L; // 2021-01-01
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
this.workerId = initId(workerIdKey, workerIdBits);
this.datacenterId = initId(datacenterIdKey, datacenterIdBits);
}
private synchronized long initId(String key, long maxId) {
String script =
"local current = redis.call('incr', KEYS[1]) " +
"if current > tonumber(ARGV[1]) then " +
" current = 1 " +
" redis.call('set', KEYS[1], current) " +
"end " +
"return current";
return (Long) jedisCluster.eval(script, 1, key, String.valueOf(maxId));
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 处理时钟回拨
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " +
(lastTimestamp - currentTimestamp) + " milliseconds");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - startTimeStamp) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
结语
Redis 凭借丰富的数据结构和原子操作能力,为分布式系统提供了多样化的解决方案。本文通过缓存设计、分布式锁、会话管理、限流、消息队列和 ID 生成等典型场景,详细展示了 Redis 的应用实践方法。在实际开发中,需根据业务需求选择合适的数据结构和优化策略,同时关注集群部署、监控调优等方面,以构建高性能、高可用的 Redis 应用。