Redis高级应用详解
一、知识概述
Redis不仅是简单的键值缓存,更提供了丰富的数据结构和功能,可以解决众多业务场景的问题。本文将深入讲解Redis的高级应用,包括:
- 分布式锁实现
- 限流算法
- 排行榜系统
- 布隆过滤器
- 消息队列
- 地理位置(GEO)
- HyperLogLog统计
- Lua脚本高级用法
二、分布式锁
2.1 基础实现
@Service
@Slf4j
public class RedisLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "lock:";
/**
* 简单加锁
* 问题:无法防止误删其他线程的锁
*/
public boolean tryLock(String lockKey, String value, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 简单解锁
* 问题:可能误删其他线程的锁
*/
public void unlock(String lockKey) {
redisTemplate.delete(lockKey);
}
}
2.2 安全的分布式锁(SETNX + Lua)
@Service
@Slf4j
public class DistributedLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 加锁Lua脚本
private static final String LOCK_SCRIPT = """
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
return 0
end
""";
// 解锁Lua脚本(保证原子性,只删自己的锁)
private static final String UNLOCK_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
// 看门狗续期脚本
private static final String RENEW_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
""";
/**
* 尝试获取锁
* @param lockKey 锁的key
* @param clientId 客户端唯一标识
* @param expireSeconds 过期时间
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String clientId, long expireSeconds) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
clientId,
String.valueOf(expireSeconds)
);
return result != null && result == 1L;
}
/**
* 尝试获取锁(带超时等待)
* @param lockKey 锁的key
* @param clientId 客户端唯一标识
* @param expireSeconds 过期时间
* @param waitTime 等待时间(毫秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String clientId, long expireSeconds, long waitTime) {
long startTime = System.currentTimeMillis();
long retryInterval = 100; // 重试间隔
while (true) {
if (tryLock(lockKey, clientId, expireSeconds)) {
return true;
}
// 检查是否超时
if (System.currentTimeMillis() - startTime > waitTime) {
return false;
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
/**
* 释放锁(只能释放自己的锁)
*/
public boolean unlock(String lockKey, String clientId) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
clientId
);
return result != null && result == 1L;
}
/**
* 续期(看门狗机制)
*/
public boolean renewLock(String lockKey, String clientId, long expireSeconds) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(RENEW_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
clientId,
String.valueOf(expireSeconds)
);
return result != null && result == 1L;
}
}
2.3 可重入锁实现
@Service
@Slf4j
public class ReentrantLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 可重入锁Lua脚本
private static final String LOCK_SCRIPT = """
-- 锁不存在或已持有
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[1], 1)
redis.call('expire', KEYS[1], ARGV[2])
return 1
end
-- 当前线程持有锁,重入计数+1
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[1], 1)
redis.call('expire', KEYS[1], ARGV[2])
return 1
end
-- 锁被其他线程持有
return 0
""";
// 解锁脚本
private static final String UNLOCK_SCRIPT = """
-- 锁不存在
if redis.call('exists', KEYS[1]) == 0 then
return 1
end
-- 不是当前线程持有
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
return 0
end
-- 计数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if counter > 0 then
-- 还有重入,续期
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
-- 释放锁
redis.call('del', KEYS[1])
return 1
end
""";
public boolean tryLock(String lockKey, String threadId, long expireSeconds) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
threadId,
String.valueOf(expireSeconds)
);
return result != null && result == 1L;
}
public boolean unlock(String lockKey, String threadId, long expireSeconds) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
threadId,
String.valueOf(expireSeconds)
);
return result != null && result == 1L;
}
}
2.4 使用模板模式封装
@Component
public class RedisLockTemplate {
@Autowired
private DistributedLockService lockService;
/**
* 在锁保护下执行任务
*/
public <T> T executeWithLock(String lockKey, long expireSeconds, long waitTime,
Supplier<T> supplier) {
String clientId = UUID.randomUUID().toString();
try {
// 获取锁
if (!lockService.tryLock(lockKey, clientId, expireSeconds, waitTime)) {
throw new RuntimeException("获取锁失败: " + lockKey);
}
// 执行业务逻辑
return supplier.get();
} finally {
// 释放锁
lockService.unlock(lockKey, clientId);
}
}
/**
* 无返回值版本
*/
public void executeWithLock(String lockKey, long expireSeconds, long waitTime,
Runnable runnable) {
executeWithLock(lockKey, expireSeconds, waitTime, () -> {
runnable.run();
return null;
});
}
/**
* 看门狗自动续期版本
*/
public <T> T executeWithWatchdog(String lockKey, long expireSeconds,
Supplier<T> supplier) {
String clientId = UUID.randomUUID().toString();
ScheduledExecutorService watchdogExecutor = Executors.newSingleThreadScheduledExecutor();
try {
// 获取锁
if (!lockService.tryLock(lockKey, clientId, expireSeconds)) {
throw new RuntimeException("获取锁失败: " + lockKey);
}
// 启动看门狗(每隔过期时间/3续期一次)
long renewInterval = expireSeconds * 1000 / 3;
watchdogExecutor.scheduleAtFixedRate(() -> {
try {
lockService.renewLock(lockKey, clientId, expireSeconds);
} catch (Exception e) {
log.warn("锁续期失败", e);
}
}, renewInterval, renewInterval, TimeUnit.MILLISECONDS);
// 执行业务逻辑
return supplier.get();
} finally {
// 停止看门狗
watchdogExecutor.shutdownNow();
// 释放锁
lockService.unlock(lockKey, clientId);
}
}
}
// 使用示例
@Service
public class OrderService {
@Autowired
private RedisLockTemplate lockTemplate;
public void deductStock(Long productId, int quantity) {
String lockKey = "lock:stock:" + productId;
lockTemplate.executeWithLock(lockKey, 30, 5000, () -> {
// 在锁保护下执行库存扣减
doDeductStock(productId, quantity);
});
}
}
三、限流算法
3.1 固定窗口限流
@Service
public class FixedWindowRateLimiter {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
/**
* 固定窗口限流
* @param key 限流key
* @param limit 时间窗口内允许的最大请求数
* @param windowSeconds 时间窗口大小(秒)
* @return 是否允许通过
*/
public boolean tryAcquire(String key, long limit, long windowSeconds) {
long currentWindow = System.currentTimeMillis() / (windowSeconds * 1000);
String redisKey = "rate_limit:" + key + ":" + currentWindow;
Long count = redisTemplate.opsForValue().increment(redisKey);
if (count != null && count == 1) {
// 第一次访问,设置过期时间
redisTemplate.expire(redisKey, windowSeconds, TimeUnit.SECONDS);
}
return count != null && count <= limit;
}
}
问题: 边界突发问题
时间窗口:1秒,限制:10次
时间线:
|--窗口1--|--窗口2--|
^
第1秒末:10次请求
第2秒初:10次请求
→ 在2秒交界处,实际处理了20次请求(超过限制)
3.2 滑动窗口限流
@Service
public class SlidingWindowRateLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String SCRIPT = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 清理过期的请求记录
redis.call('zremrangebyscore', key, 0, now - window * 1000)
-- 获取当前窗口内的请求数
local count = redis.call('zcard', key)
if count < limit then
-- 添加当前请求
redis.call('zadd', key, now, now .. ':' .. math.random())
redis.call('expire', key, window)
return 1
else
return 0
end
""";
/**
* 滑动窗口限流
* @param key 限流key
* @param limit 时间窗口内允许的最大请求数
* @param windowSeconds 时间窗口大小(秒)
* @return 是否允许通过
*/
public boolean tryAcquire(String key, long limit, long windowSeconds) {
String redisKey = "rate_limit:sliding:" + key;
long now = System.currentTimeMillis();
DefaultRedisScript<Long> script = new DefaultRedisScript<>(SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(redisKey),
String.valueOf(limit),
String.valueOf(windowSeconds),
String.valueOf(now)
);
return result != null && result == 1L;
}
}
3.3 令牌桶限流
@Service
public class TokenBucketRateLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
-- 获取当前桶状态
local info = redis.call('hmget', key, 'tokens', 'last_time')
local tokens = tonumber(info[1]) or capacity
local lastTime = tonumber(info[2]) or now
-- 计算新增令牌
local delta = math.max(0, now - lastTime)
local newTokens = delta / 1000 * rate
-- 更新令牌数(不超过容量)
tokens = math.min(capacity, tokens + newTokens)
-- 判断是否足够
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'last_time', now)
redis.call('expire', key, math.ceil(capacity / rate) + 1)
return 1
else
redis.call('hmset', key, 'tokens', tokens, 'last_time', now)
redis.call('expire', key, math.ceil(capacity / rate) + 1)
return 0
end
""";
/**
* 令牌桶限流
* @param key 限流key
* @param capacity 桶容量
* @param rate 每秒生成的令牌数
* @param requested 请求的令牌数
* @return 是否允许通过
*/
public boolean tryAcquire(String key, long capacity, double rate, long requested) {
String redisKey = "rate_limit:token:" + key;
long now = System.currentTimeMillis();
DefaultRedisScript<Long> script = new DefaultRedisScript<>(SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(redisKey),
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(requested),
String.valueOf(now)
);
return result != null && result == 1L;
}
/**
* 简化版:每次请求消耗1个令牌
*/
public boolean tryAcquire(String key, long capacity, double rate) {
return tryAcquire(key, capacity, rate, 1);
}
}
3.4 漏桶限流
@Service
public class LeakyBucketRateLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 获取当前桶状态
local info = redis.call('hmget', key, 'water', 'last_time')
local water = tonumber(info[1]) or 0
local lastTime = tonumber(info[2]) or now
-- 计算漏出的水量
local delta = math.max(0, now - lastTime)
local leaked = delta / 1000 * rate
-- 更新水位(不低于0)
water = math.max(0, water - leaked)
-- 判断桶是否还有空间
if water < capacity then
water = water + 1
redis.call('hmset', key, 'water', water, 'last_time', now)
redis.call('expire', key, math.ceil(capacity / rate) + 1)
return 1
else
redis.call('hmset', key, 'water', water, 'last_time', now)
redis.call('expire', key, math.ceil(capacity / rate) + 1)
return 0
end
""";
/**
* 漏桶限流
* @param key 限流key
* @param capacity 桶容量
* @param rate 每秒漏出的请求数
* @return 是否允许通过
*/
public boolean tryAcquire(String key, long capacity, double rate) {
String redisKey = "rate_limit:leaky:" + key;
long now = System.currentTimeMillis();
DefaultRedisScript<Long> script = new DefaultRedisScript<>(SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(redisKey),
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(now)
);
return result != null && result == 1L;
}
}
3.5 限流注解实现
// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "";
long limit() default 100;
long window() default 1; // 秒
RateLimitType type() default RateLimitType.SLIDING_WINDOW;
String message() default "请求过于频繁,请稍后再试";
}
public enum RateLimitType {
FIXED_WINDOW,
SLIDING_WINDOW,
TOKEN_BUCKET,
LEAKY_BUCKET
}
// 切面实现
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Autowired
private SlidingWindowRateLimiter slidingWindowLimiter;
@Autowired
private TokenBucketRateLimiter tokenBucketLimiter;
@Autowired
private LeakyBucketRateLimiter leakyBucketLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
String key = generateKey(point, rateLimit);
boolean allowed = false;
switch (rateLimit.type()) {
case SLIDING_WINDOW:
allowed = slidingWindowLimiter.tryAcquire(key, rateLimit.limit(), rateLimit.window());
break;
case TOKEN_BUCKET:
allowed = tokenBucketLimiter.tryAcquire(key, rateLimit.limit(), rateLimit.limit());
break;
case LEAKY_BUCKET:
allowed = leakyBucketLimiter.tryAcquire(key, rateLimit.limit(), rateLimit.limit());
break;
default:
allowed = true;
}
if (!allowed) {
throw new RuntimeException(rateLimit.message());
}
return point.proceed();
}
private String generateKey(ProceedingJoinPoint point, RateLimit rateLimit) {
if (!rateLimit.key().isEmpty()) {
return rateLimit.key();
}
MethodSignature signature = (MethodSignature) point.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
return className + ":" + methodName;
}
}
// 使用示例
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/data")
@RateLimit(limit = 10, window = 1, message = "请求太频繁了")
public String getData() {
return "data";
}
}
四、排行榜系统
4.1 基础排行榜
@Service
public class LeaderboardService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 添加/更新分数
*/
public void addScore(String leaderboardKey, String userId, double score) {
redisTemplate.opsForZSet().add(leaderboardKey, userId, score);
}
/**
* 增加分数
*/
public Double incrementScore(String leaderboardKey, String userId, double delta) {
return redisTemplate.opsForZSet().incrementScore(leaderboardKey, userId, delta);
}
/**
* 获取排名(从高到低,第1名返回0)
*/
public Long getRank(String leaderboardKey, String userId) {
return redisTemplate.opsForZSet().reverseRank(leaderboardKey, userId);
}
/**
* 获取分数
*/
public Double getScore(String leaderboardKey, String userId) {
return redisTemplate.opsForZSet().score(leaderboardKey, userId);
}
/**
* 获取Top N排行榜
*/
public List<RankInfo> getTopN(String leaderboardKey, long n) {
Set<ZSetOperations.TypedTuple<Object>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(leaderboardKey, 0, n - 1);
if (tuples == null || tuples.isEmpty()) {
return Collections.emptyList();
}
List<RankInfo> result = new ArrayList<>();
long rank = 1;
for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
result.add(new RankInfo(
rank++,
tuple.getValue().toString(),
tuple.getScore()
));
}
return result;
}
/**
* 获取指定用户的排名信息及前后用户
*/
public UserRankInfo getUserRankWithNeighbors(String leaderboardKey, String userId, int neighborCount) {
Long rank = getRank(leaderboardKey, userId);
if (rank == null) {
return null;
}
Double score = getScore(leaderboardKey, userId);
// 获取前后各neighborCount名用户
long start = Math.max(0, rank - neighborCount);
long end = rank + neighborCount;
Set<ZSetOperations.TypedTuple<Object>> neighbors =
redisTemplate.opsForZSet().reverseRangeWithScores(leaderboardKey, start, end);
List<RankInfo> neighborList = new ArrayList<>();
if (neighbors != null) {
long currentRank = start + 1;
for (ZSetOperations.TypedTuple<Object> tuple : neighbors) {
neighborList.add(new RankInfo(
currentRank++,
tuple.getValue().toString(),
tuple.getScore()
));
}
}
return new UserRankInfo(
new RankInfo(rank + 1, userId, score),
neighborList
);
}
/**
* 分页获取排行榜
*/
public PageResult<RankInfo> getLeaderboardPage(String leaderboardKey, int page, int size) {
long start = (long) (page - 1) * size;
long end = start + size - 1;
Set<ZSetOperations.TypedTuple<Object>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(leaderboardKey, start, end);
Long total = redisTemplate.opsForZSet().size(leaderboardKey);
List<RankInfo> list = new ArrayList<>();
if (tuples != null) {
long rank = start + 1;
for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
list.add(new RankInfo(
rank++,
tuple.getValue().toString(),
tuple.getScore()
));
}
}
return new PageResult<>(list, total != null ? total : 0, page, size);
}
}
@Data
@AllArgsConstructor
class RankInfo {
private Long rank;
private String userId;
private Double score;
}
@Data
@AllArgsConstructor
class UserRankInfo {
private RankInfo current;
private List<RankInfo> neighbors;
}
@Data
@AllArgsConstructor
class PageResult<T> {
private List<T> data;
private Long total;
private Integer page;
private Integer size;
}
4.2 多维度排行榜
@Service
public class MultiLeaderboardService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 更新多维度排行榜
*/
public void updateScore(String userId, int score, int kills, int assists) {
long now = System.currentTimeMillis();
String today = LocalDate.now().toString();
// 总榜
redisTemplate.opsForZSet().incrementScore("leaderboard:total:score", userId, score);
// 击杀榜
redisTemplate.opsForZSet().incrementScore("leaderboard:total:kills", userId, kills);
// 助攻榜
redisTemplate.opsForZSet().incrementScore("leaderboard:total:assists", userId, assists);
// 日榜
redisTemplate.opsForZSet().incrementScore("leaderboard:daily:" + today + ":score", userId, score);
redisTemplate.opsForZSet().expire("leaderboard:daily:" + today + ":score", 7, TimeUnit.DAYS);
}
/**
* 获取综合排名
*/
public Map<String, Long> getAllRanks(String userId) {
Map<String, Long> ranks = new HashMap<>();
ranks.put("total_score", getRank("leaderboard:total:score", userId));
ranks.put("total_kills", getRank("leaderboard:total:kills", userId));
ranks.put("total_assists", getRank("leaderboard:total:assists", userId));
String today = LocalDate.now().toString();
ranks.put("daily_score", getRank("leaderboard:daily:" + today + ":score", userId));
return ranks;
}
private Long getRank(String key, String userId) {
Long rank = redisTemplate.opsForZSet().reverseRank(key, userId);
return rank != null ? rank + 1 : null;
}
}
五、布隆过滤器
5.1 RedisBloom模块
@Service
public class BloomFilterService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String ADD_SCRIPT = """
return redis.call('BF.ADD', KEYS[1], ARGV[1])
""";
private static final String EXISTS_SCRIPT = """
return redis.call('BF.EXISTS', KEYS[1], ARGV[1])
""";
private static final String MADD_SCRIPT = """
return redis.call('BF.MADD', KEYS[1], unpack(ARGV))
""";
/**
* 创建布隆过滤器
*/
public boolean createFilter(String key, long expectedItems, double errorRate) {
try {
// 需要RedisBloom模块支持
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.execute("BF.RESERVE",
key.getBytes(),
String.valueOf(errorRate).getBytes(),
String.valueOf(expectedItems).getBytes()
);
return null;
});
return true;
} catch (Exception e) {
log.error("创建布隆过滤器失败", e);
return false;
}
}
/**
* 添加元素
*/
public boolean add(String key, String value) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(ADD_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(key), value);
return result != null && result == 1L;
}
/**
* 检查元素是否存在
*/
public boolean mightContain(String key, String value) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(EXISTS_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(key), value);
return result != null && result == 1L;
}
/**
* 批量添加
*/
public List<Boolean> addAll(String key, List<String> values) {
DefaultRedisScript<List> script = new DefaultRedisScript<>(MADD_SCRIPT, List.class);
List<Long> results = redisTemplate.execute(script, Collections.singletonList(key),
values.toArray(new String[0]));
return results.stream()
.map(r -> r != null && r == 1L)
.collect(Collectors.toList());
}
}
5.2 基于位图的实现
@Service
public class SimpleBloomFilter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final int size;
private final int hashCount;
private final List<HashFunction> hashFunctions;
public SimpleBloomFilter(int expectedItems, double errorRate) {
// 计算最优参数
this.size = optimalSize(expectedItems, errorRate);
this.hashCount = optimalHashCount(size, expectedItems);
this.hashFunctions = createHashFunctions(hashCount);
}
/**
* 添加元素
*/
public void add(String key, String value) {
for (HashFunction function : hashFunctions) {
int hash = function.hash(value);
int index = Math.abs(hash % size);
redisTemplate.opsForValue().setBit(key, index, true);
}
}
/**
* 检查元素可能存在
*/
public boolean mightContain(String key, String value) {
for (HashFunction function : hashFunctions) {
int hash = function.hash(value);
int index = Math.abs(hash % size);
Boolean bit = redisTemplate.opsForValue().getBit(key, index);
if (Boolean.FALSE.equals(bit)) {
return false;
}
}
return true;
}
/**
* 计算位数组大小
*/
private int optimalSize(int n, double p) {
return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 计算哈希函数数量
*/
private int optimalHashCount(int m, int n) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
/**
* 创建哈希函数
*/
private List<HashFunction> createHashFunctions(int count) {
List<HashFunction> functions = new ArrayList<>();
for (int i = 0; i < count; i++) {
final int seed = i * 31 + 17;
functions.add(value -> {
int hash = value.hashCode();
hash = hash ^ seed;
hash = hash * 0x5bd1e995;
hash = hash ^ (hash >>> 15);
return hash;
});
}
return functions;
}
@FunctionalInterface
interface HashFunction {
int hash(String value);
}
}
六、消息队列
6.1 基于List实现
@Service
@Slf4j
public class RedisQueueService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 发送消息
*/
public void send(String queueName, Object message) {
redisTemplate.opsForList().rightPush(queueName, message);
}
/**
* 接收消息(阻塞)
*/
public Object receive(String queueName, long timeoutSeconds) {
return redisTemplate.opsForList().leftPop(queueName, timeoutSeconds, TimeUnit.SECONDS);
}
/**
* 消费者
*/
@Async
public void startConsumer(String queueName, Consumer<Object> consumer) {
while (true) {
try {
Object message = receive(queueName, 10);
if (message != null) {
consumer.accept(message);
}
} catch (Exception e) {
log.error("消费消息异常", e);
}
}
}
}
6.2 延时队列
@Service
@Slf4j
public class DelayQueueService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 发送延时消息
*/
public void sendDelayMessage(String queueName, Object message, long delaySeconds) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
redisTemplate.opsForZSet().add(queueName, message, executeTime);
}
/**
* 消费延时消息
*/
@Scheduled(fixedRate = 1000)
public void consumeDelayMessages() {
String queueName = "delay:queue";
long now = System.currentTimeMillis();
// 获取到期消息
Set<Object> messages = redisTemplate.opsForZSet()
.rangeByScore(queueName, 0, now);
if (messages != null && !messages.isEmpty()) {
for (Object message : messages) {
try {
// 处理消息
processMessage(message);
// 删除已处理消息
redisTemplate.opsForZSet().remove(queueName, message);
} catch (Exception e) {
log.error("处理延时消息失败: {}", message, e);
}
}
}
}
private void processMessage(Object message) {
// 业务处理逻辑
log.info("处理延时消息: {}", message);
}
}
6.3 基于Stream实现
@Service
@Slf4j
public class RedisStreamService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 发送消息到Stream
*/
public String send(String streamKey, Map<String, Object> message) {
RecordId recordId = redisTemplate.opsForStream().add(streamKey, message);
return recordId != null ? recordId.getValue() : null;
}
/**
* 创建消费者组
*/
public void createConsumerGroup(String streamKey, String groupName) {
try {
redisTemplate.opsForStream().createGroup(streamKey, groupName);
} catch (Exception e) {
log.info("消费者组已存在: {}", groupName);
}
}
/**
* 消费消息
*/
public List<MapRecord<String, Object, Object>> consume(
String streamKey, String groupName, String consumerName,
long count) {
return redisTemplate.opsForStream().read(
Consumer.from(groupName, consumerName),
StreamReadOptions.empty().count(count),
StreamOffset.create(streamKey, ReadOffset.lastConsumed())
);
}
/**
* 确认消息
*/
public Long ack(String streamKey, String groupName, String... recordIds) {
return redisTemplate.opsForStream().acknowledge(streamKey, groupName, recordIds);
}
/**
* 消费者
*/
@Async
public void startStreamConsumer(String streamKey, String groupName,
String consumerName,
Consumer<Map<String, Object>> processor) {
createConsumerGroup(streamKey, groupName);
while (true) {
try {
List<MapRecord<String, Object, Object>> records =
consume(streamKey, groupName, consumerName, 10);
if (records != null && !records.isEmpty()) {
for (MapRecord<String, Object, Object> record : records) {
try {
processor.accept(record.getValue());
ack(streamKey, groupName, record.getId().getValue());
} catch (Exception e) {
log.error("处理消息失败: {}", record.getId(), e);
}
}
} else {
Thread.sleep(1000);
}
} catch (Exception e) {
log.error("消费Stream异常", e);
try {
Thread.sleep(5000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}
七、地理位置(GEO)
@Service
public class GeoService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 添加位置
*/
public Long addLocation(String key, double longitude, double latitude, String memberId) {
return redisTemplate.opsForGeo().add(key, new Point(longitude, latitude), memberId);
}
/**
* 获取位置坐标
*/
public Point getLocation(String key, String memberId) {
List<Point> points = redisTemplate.opsForGeo().position(key, memberId);
return (points != null && !points.isEmpty()) ? points.get(0) : null;
}
/**
* 计算两点距离
*/
public Distance getDistance(String key, String member1, String member2, Metric metric) {
return redisTemplate.opsForGeo().distance(key, member1, member2, metric);
}
/**
* 获取附近的位置
*/
public List<GeoResult<RedisGeoCommands.GeoLocation<String>>> getNearbyLocations(
String key, double longitude, double latitude,
double distance, Metric metric, long limit) {
Circle circle = new Circle(new Point(longitude, latitude), new Distance(distance, metric));
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(limit);
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(key, circle, args);
return results != null ? results.getContent() : Collections.emptyList();
}
/**
* 获取成员附近的位置
*/
public List<GeoResult<RedisGeoCommands.GeoLocation<String>>> getNearbyByMember(
String key, String memberId, double distance, Metric metric, long limit) {
Distance dist = new Distance(distance, metric);
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(limit);
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(key, memberId, dist, args);
return results != null ? results.getContent() : Collections.emptyList();
}
}
// 使用示例
@Service
public class StoreService {
@Autowired
private GeoService geoService;
private static final String STORE_KEY = "stores:locations";
/**
* 添加门店位置
*/
public void addStore(Store store) {
geoService.addLocation(STORE_KEY, store.getLongitude(), store.getLatitude(),
store.getId().toString());
}
/**
* 查找附近门店
*/
public List<StoreDistance> findNearbyStores(double longitude, double latitude,
double distanceKm) {
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> results =
geoService.getNearbyLocations(STORE_KEY, longitude, latitude,
distanceKm, Metrics.KILOMETERS, 20);
return results.stream()
.map(result -> {
StoreDistance sd = new StoreDistance();
sd.setStoreId(result.getContent().getName());
sd.setDistance(result.getDistance().getValue());
sd.setLongitude(result.getContent().getPoint().getX());
sd.setLatitude(result.getContent().getPoint().getY());
return sd;
})
.collect(Collectors.toList());
}
}
八、HyperLogLog统计
@Service
public class HyperLogLogService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 添加元素
*/
public Long add(String key, String... values) {
return redisTemplate.opsForHyperLogLog().add(key, values);
}
/**
* 获取基数(去重后的数量)
*/
public Long count(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
/**
* 合并多个HyperLogLog
*/
public Long merge(String destination, String... sourceKeys) {
return redisTemplate.opsForHyperLogLog().union(destination, sourceKeys);
}
/**
* 统计UV(独立访客)
*/
public void recordUV(String date, String userId) {
String key = "uv:" + date;
redisTemplate.opsForHyperLogLog().add(key, userId);
}
/**
* 获取某天的UV
*/
public Long getUV(String date) {
return redisTemplate.opsForHyperLogLog().size("uv:" + date);
}
/**
* 获取某段时间的UV(合并计算)
*/
public Long getRangeUV(String startDate, String endDate) {
List<String> keys = new ArrayList<>();
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
while (!start.isAfter(end)) {
keys.add("uv:" + start);
start = start.plusDays(1);
}
String tempKey = "uv:temp:" + System.currentTimeMillis();
Long count = redisTemplate.opsForHyperLogLog().union(tempKey, keys.toArray(new String[0]));
redisTemplate.delete(tempKey);
return count;
}
}
九、总结与最佳实践
9.1 功能选择指南
| 功能 | 数据结构 | 适用场景 |
|---|---|---|
| 分布式锁 | String + Lua | 防止重复操作、库存扣减 |
| 限流 | ZSet/Lua | API限流、防刷 |
| 排行榜 | ZSet | 游戏排名、热度榜 |
| 布隆过滤器 | Bitmap/RedisBloom | 防止缓存穿透、URL去重 |
| 消息队列 | List/Stream | 异步任务、事件通知 |
| 地理位置 | GEO | 附近的人、门店查询 |
| UV统计 | HyperLogLog | 网站UV、大数据量去重统计 |
9.2 注意事项
- Lua脚本要简洁,避免复杂逻辑
- 大key要拆分,避免阻塞
- 设置合理的过期时间
- 监控内存使用
- 生产环境使用Redis Cluster
- 关键操作要加重试机制
9.3 性能优化建议
// 1. Pipeline批量操作
public void batchInsert(Map<String, String> data) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
data.forEach((key, value) -> {
connection.set(key.getBytes(), value.getBytes());
});
return null;
});
}
// 2. 合理使用连接池
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("localhost");
config.setPort(6379);
LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.poolConfig(new GenericObjectPoolConfig())
.build();
return new LettuceConnectionFactory(config, clientConfig);
}
十、思考与练习
思考题
-
基础题:分布式锁使用 SETNX + Lua 脚本实现的原理是什么?为什么解锁也需要用 Lua 脚本?
-
进阶题:比较固定窗口、滑动窗口、令牌桶、漏桶四种限流算法的优缺点,它们分别适合什么场景?
-
实战题:Redis Stream 相比 List 实现消息队列有什么优势?在设计消息队列时,如何保证消息不丢失?
编程练习
练习:实现一个基于 Redis 的分布式秒杀系统,要求:
- 使用分布式锁防止超卖
- 使用 Lua 脚本保证原子性
- 使用 Redis Stream 记录订单
- 实现库存预热和自动回滚
提示:扣减库存的 Lua 脚本需要先检查库存再扣减,保证原子性。
章节关联
- 前置章节:《缓存一致性详解》- 理解分布式数据一致性
- 后续章节:《限流算法详解》- 深入学习各类限流算法
- 扩展阅读:《Redis 设计与实现》第二部分
📝 下一章预告
接下来将进入服务治理专题,首先学习限流算法详解,包括固定窗口、滑动窗口、令牌桶、漏桶等算法的详细原理与实现。
本章完