【Redis面试高频篇】手撕Redis限流(令牌桶/漏桶)+分布式锁,面试再也不慌


🍃 予枫个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常

💻 Debug 这个世界,Return 更好的自己!


引言

面试中Redis相关的手撕代码题,分布式锁和限流绝对是高频中的高频!很多同学要么死记硬背代码,要么只懂理论不会落地,一到现场手写就卡壳。本文聚焦面试核心需求,从基础实现到加分细节,手把手手撕基于Redis的分布式锁和限流算法(令牌桶/漏桶),每一行代码都带详细注释,看完直接能复刻到面试答题纸上,助力大家顺利拿下offer~

文章目录

一、面试核心:为什么Redis适合做分布式锁和限流?

在动手写代码前,先搞懂底层逻辑,面试时能多一层加分项 ✨

核心结论:Redis的高性能(单线程+内存操作)、原子性命令(SETNX、INCR等)、过期时间特性,完美契合分布式锁"互斥性、安全性、可用性"和限流"高并发控制、低延迟"的核心需求。

1.1 分布式锁核心诉求(面试必答)

  • 互斥性:同一时间只有一个线程能获取锁
  • 安全性:锁只能被持有锁的线程释放
  • 可用性:即使Redis宕机,也能避免死锁
  • 可重入性(加分项):同一线程可重复获取锁

1.2 限流核心诉求(面试必答)

  • 控制并发访问量,避免服务被击垮
  • 低延迟:限流判断过程不能成为性能瓶颈
  • 灵活性:支持不同限流策略(令牌桶/漏桶等)

💡 小提示:面试时先答核心诉求,再写代码,会比直接上手写代码更显专业,加分!

二、手撕Redis分布式锁:基础实现+面试加分项

2.1 基础实现(基于SETNX+过期时间)

核心思路:利用Redis的SETNX命令(key不存在则设置,存在则失败)实现互斥,同时设置过期时间避免死锁。

java 复制代码
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis分布式锁基础实现(面试手写核心版)
 * 关键加分点:UUID防误释放、过期时间防死锁、try-finally保证释放
 */
public class RedisDistributedLock {
    // Redis连接(实际开发中用连接池,面试简化写)
    private Jedis jedis;
    // 锁的key前缀(避免key冲突,面试加分项)
    private static final String LOCK_PREFIX = "distributed_lock:";
    // 锁的过期时间(默认30秒,避免死锁,可根据业务调整)
    private long expireTime = 30;
    // 持有锁的标识(UUID+线程ID,防误释放,核心加分项)
    private String lockValue;
    // 锁的key
    private String lockKey;

    // 构造方法
    public RedisDistributedLock(Jedis jedis, String businessKey) {
        this.jedis = jedis;
        this.lockKey = LOCK_PREFIX + businessKey;
        // 生成唯一标识:UUID(避免不同线程/服务误释放锁)+ 线程ID
        this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
    }

    /**
     * 获取锁
     * @return 是否获取成功
     */
    public boolean lock() {
        // 核心命令:SETNX + 过期时间(原子操作,Redis 2.6.12+支持)
        // 面试重点:必须用原子命令,避免SETNX后宕机导致无法设置过期时间
        String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
        // 成功获取锁返回"OK",失败返回null
        return "OK".equals(result);
    }

    /**
     * 释放锁(核心:必须判断是自己的锁,避免误释放)
     * 面试加分项:用Lua脚本保证原子性,避免判断和删除非原子操作
     */
    public boolean unlock() {
        // Lua脚本:判断锁的值是否等于当前线程的标识,是则删除,否则返回0
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                          "return redis.call('del', KEYS[1]) " +
                          "else " +
                          "return 0 " +
                          "end";
        // 执行Lua脚本,KEYS[1]是lockKey,ARGV[1]是lockValue
        Object result = jedis.eval(luaScript, 1, lockKey, lockValue);
        // 成功释放返回1,失败返回0
        return 1L.equals(result);
    }

    // 重入锁实现(面试进阶加分项,可选写)
    public boolean reentrantLock() {
        // 思路:获取锁时判断是否是自己的锁,是则刷新过期时间并返回成功
        String currentValue = jedis.get(lockKey);
        if (lockValue.equals(currentValue)) {
            // 刷新过期时间
            jedis.expire(lockKey, expireTime);
            return true;
        }
        // 不是自己的锁,执行正常获取逻辑
        return lock();
    }
}

2.2 面试高频追问与加分细节(必记)

  • ❓ 为什么要用Lua脚本释放锁?
    ✅ 答:避免"判断锁是自己的"和"删除锁"两个操作非原子化,防止在判断后、删除前锁过期被其他线程获取,导致误释放。
  • ❓ 锁的过期时间设置多久合适?
    ✅ 答:根据业务执行时间定,一般30秒-1分钟,同时可加"锁续命"机制(后台线程定期刷新过期时间),避免业务没执行完锁过期。
  • ❓ 分布式锁的缺点?如何优化?
    ✅ 答:缺点是Redis单点故障风险;优化方案:用Redis集群(主从+哨兵),或RedLock算法(多节点加锁)。

📌 温馨提示:这部分代码和追问答案,面试时能完整写出来+讲清楚,基本能秒杀80%的候选人!记得点赞收藏,方便后续复习~

三、手撕Redis限流算法:令牌桶+漏桶(面试重点)

限流的核心是控制单位时间内的请求量,Redis中常用令牌桶和漏桶算法,两者各有适用场景,面试中常要求手写其中一种或对比两者区别。

3.1 令牌桶算法(面试高频,推荐优先掌握)

核心思路

  • 系统定期向桶中放入令牌(固定速率)
  • 每个请求过来需要先获取一个令牌,有令牌则允许访问,无令牌则拒绝
  • 桶有最大容量,令牌满了之后不再放入

手撕代码实现(Redis+Java)

java 复制代码
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;

/**
 * Redis令牌桶限流算法(面试手写核心版)
 * 适用场景:允许突发流量(桶内有积累的令牌时),适合大部分业务场景
 */
public class RedisTokenBucketLimiter {
    private Jedis jedis;
    // 限流key(按接口/用户区分)
    private String limitKey;
    // 桶的最大容量(最多存放多少令牌)
    private int bucketCapacity;
    // 令牌生成速率(每秒生成多少个令牌)
    private int tokenRate;

    // 构造方法
    public RedisTokenBucketLimiter(Jedis jedis, String limitKey, int bucketCapacity, int tokenRate) {
        this.jedis = jedis;
        this.limitKey = "rate_limit:token_bucket:" + limitKey;
        this.bucketCapacity = bucketCapacity;
        this.tokenRate = tokenRate;
    }

    /**
     * 判断是否允许访问(获取令牌)
     * @return true-允许,false-拒绝
     */
    public boolean allowAccess() {
        // 核心逻辑:用Redis的hash存储令牌桶信息(last_refresh_time:上次刷新时间,token_count:当前令牌数)
        String key = this.limitKey;
        long now = System.currentTimeMillis() / 1000; // 当前时间(秒)

        // 1. 获取当前令牌桶信息(上次刷新时间和当前令牌数)
        String lastRefreshTimeStr = jedis.hget(key, "last_refresh_time");
        String tokenCountStr = jedis.hget(key, "token_count");

        long lastRefreshTime = lastRefreshTimeStr == null ? now : Long.parseLong(lastRefreshTimeStr);
        int tokenCount = tokenCountStr == null ? bucketCapacity : Integer.parseInt(tokenCountStr);

        // 2. 计算从上次刷新到现在,应该生成的令牌数
        long timeDiff = now - lastRefreshTime;
        int generateToken = (int) (timeDiff * tokenRate);

        // 3. 更新当前令牌数(不能超过桶的最大容量)
        tokenCount = Math.min(bucketCapacity, tokenCount + generateToken);

        // 4. 刷新上次刷新时间
        jedis.hset(key, "last_refresh_time", String.valueOf(now));

        // 5. 判断是否有令牌可获取
        if (tokenCount > 0) {
            // 有令牌,获取一个(令牌数-1)
            jedis.hset(key, "token_count", String.valueOf(tokenCount - 1));
            // 设置key过期时间(避免垃圾key堆积,面试加分项)
            jedis.expire(key, 3600);
            return true;
        } else {
            // 无令牌,拒绝访问
            return false;
        }
    }
}

3.2 漏桶算法(面试对比项,手写简化版)

核心思路

  • 请求像水一样流入漏桶,漏桶以固定速率出水(处理请求)
  • 漏桶有最大容量,水满了之后新的水(请求)会溢出(拒绝)
  • 特点:输出速率恒定,能平滑突发流量,适合对输出速率有严格要求的场景

手撕代码实现(Redis+Java)

java 复制代码
import redis.clients.jedis.Jedis;

/**
 * Redis漏桶限流算法(面试手写简化版)
 * 适用场景:输出速率恒定,不允许突发流量(如接口下游是数据库,需要平稳访问)
 */
public class RedisLeakyBucketLimiter {
    private Jedis jedis;
    private String limitKey;
    // 漏桶容量(最大允许堆积的请求数)
    private int bucketCapacity;
    // 漏水速率(每秒处理多少个请求)
    private int leakRate;

    public RedisLeakyBucketLimiter(Jedis jedis, String limitKey, int bucketCapacity, int leakRate) {
        this.jedis = jedis;
        this.limitKey = "rate_limit:leaky_bucket:" + limitKey;
        this.bucketCapacity = bucketCapacity;
        this.leakRate = leakRate;
    }

    public boolean allowAccess() {
        long now = System.currentTimeMillis() / 1000;
        String key = this.limitKey;

        // 获取漏桶信息:last_leak_time(上次漏水时间)、water_count(当前水量/请求数)
        String lastLeakTimeStr = jedis.hget(key, "last_leak_time");
        String waterCountStr = jedis.hget(key, "water_count");

        long lastLeakTime = lastLeakTimeStr == null ? now : Long.parseLong(lastLeakTimeStr);
        int waterCount = waterCountStr == null ? 0 : Integer.parseInt(waterCountStr);

        // 1. 先漏水:计算从上次到现在应该漏掉的水量(处理的请求数)
        long timeDiff = now - lastLeakTime;
        int leakWater = (int) (timeDiff * leakRate);
        // 更新当前水量(不能小于0)
        waterCount = Math.max(0, waterCount - leakWater);
        // 刷新上次漏水时间
        jedis.hset(key, "last_leak_time", String.valueOf(now));

        // 2. 判断是否能放入新的水(请求)
        if (waterCount < bucketCapacity) {
            jedis.hset(key, "water_count", String.valueOf(waterCount + 1));
            jedis.expire(key, 3600);
            return true;
        } else {
            // 桶满了,拒绝请求
            return false;
        }
    }
}

3.3 令牌桶vs漏桶(面试必对比,加分关键)

特性 令牌桶算法 漏桶算法
流量控制 允许突发流量(桶内有令牌) 输出速率恒定,抑制突发
适用场景 大部分业务(如接口限流) 下游需平稳访问(如数据库)
实现复杂度 稍复杂(需计算令牌生成) 较简单(需计算漏水)
灵活性 高(可动态调整令牌速率) 中(速率固定)

✨ 面试技巧:被问到限流算法时,先讲清楚两种算法的核心区别,再根据业务场景推荐合适的算法,最后手写代码,这样会显得思考很全面!

四、全文总结

本文聚焦Redis面试核心手撕代码题,从分布式锁的基础实现(SETNX+过期时间)、释放锁的原子性保障(Lua脚本)、重入锁优化,到令牌桶和漏桶限流算法的完整实现,每一步都结合面试加分点进行详解。记住:面试手写代码不仅要"能跑通",更要讲清楚底层逻辑、边界情况和优化方案,这样才能在众多候选人中脱颖而出。

📌 最后:如果本文对你的面试有帮助,欢迎点赞、收藏、转发!有任何疑问或补充,评论区留言交流~ 关注我(予枫),后续持续分享更多面试手撕代码干货!

相关推荐
程序员后来3 小时前
Redis基本数据类型及其应用:从原理到实战的完整指南
数据库·redis·缓存
陌上丨4 小时前
深入理解Redis线程模型
数据库·redis·缓存
Huanlis4 小时前
Spring Data Redis Stream:全景架构、交互流转与线程池陷阱深度解析
redis·spring·架构
无限码力4 小时前
华为OD技术面真题 - 数据库Redis - 2
数据库·redis·华为od·面试真题·华为od技术面真题·华为od技术面八股文·华为od高频面试真题
码农水水4 小时前
小红书Java面试被问:mTLS(双向TLS)的证书验证和握手过程
java·开发语言·数据库·redis·python·面试·开源
像少年啦飞驰点、4 小时前
零基础入门 Spring Boot:从‘Hello World’到可上线的 Web 应用
java·spring boot·web开发·编程入门·后端开发
像少年啦飞驰点、5 小时前
零基础入门 Spring Boot:从“Hello World”到可上线的 Web 应用(附完整实操指南)
spring boot·编程入门·后端开发·java web·零基础教程
jiunian_cn5 小时前
【Redis】string数据类型相关指令
数据库·redis·缓存