50-Redis高级应用详解

0 阅读14分钟

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/LuaAPI限流、防刷
排行榜ZSet游戏排名、热度榜
布隆过滤器Bitmap/RedisBloom防止缓存穿透、URL去重
消息队列List/Stream异步任务、事件通知
地理位置GEO附近的人、门店查询
UV统计HyperLogLog网站UV、大数据量去重统计

9.2 注意事项

  1. Lua脚本要简洁,避免复杂逻辑
  2. 大key要拆分,避免阻塞
  3. 设置合理的过期时间
  4. 监控内存使用
  5. 生产环境使用Redis Cluster
  6. 关键操作要加重试机制

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);
}

十、思考与练习

思考题

  1. 基础题:分布式锁使用 SETNX + Lua 脚本实现的原理是什么?为什么解锁也需要用 Lua 脚本?

  2. 进阶题:比较固定窗口、滑动窗口、令牌桶、漏桶四种限流算法的优缺点,它们分别适合什么场景?

  3. 实战题:Redis Stream 相比 List 实现消息队列有什么优势?在设计消息队列时,如何保证消息不丢失?

编程练习

练习:实现一个基于 Redis 的分布式秒杀系统,要求:

  • 使用分布式锁防止超卖
  • 使用 Lua 脚本保证原子性
  • 使用 Redis Stream 记录订单
  • 实现库存预热和自动回滚

提示:扣减库存的 Lua 脚本需要先检查库存再扣减,保证原子性。

章节关联

  • 前置章节:《缓存一致性详解》- 理解分布式数据一致性
  • 后续章节:《限流算法详解》- 深入学习各类限流算法
  • 扩展阅读:《Redis 设计与实现》第二部分

📝 下一章预告

接下来将进入服务治理专题,首先学习限流算法详解,包括固定窗口、滑动窗口、令牌桶、漏桶等算法的详细原理与实现。


本章完