
🍃 予枫 :个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》
💻 Debug 这个世界,Return 更好的自己!
引言
面试中Redis相关的手撕代码题,分布式锁和限流绝对是高频中的高频!很多同学要么死记硬背代码,要么只懂理论不会落地,一到现场手写就卡壳。本文聚焦面试核心需求,从基础实现到加分细节,手把手手撕基于Redis的分布式锁和限流算法(令牌桶/漏桶),每一行代码都带详细注释,看完直接能复刻到面试答题纸上,助力大家顺利拿下offer~
文章目录
- 引言
- 一、面试核心:为什么Redis适合做分布式锁和限流?
-
- [1.1 分布式锁核心诉求(面试必答)](#1.1 分布式锁核心诉求(面试必答))
- [1.2 限流核心诉求(面试必答)](#1.2 限流核心诉求(面试必答))
- 二、手撕Redis分布式锁:基础实现+面试加分项
-
- [2.1 基础实现(基于SETNX+过期时间)](#2.1 基础实现(基于SETNX+过期时间))
- [2.2 面试高频追问与加分细节(必记)](#2.2 面试高频追问与加分细节(必记))
- 三、手撕Redis限流算法:令牌桶+漏桶(面试重点)
-
- [3.1 令牌桶算法(面试高频,推荐优先掌握)](#3.1 令牌桶算法(面试高频,推荐优先掌握))
- [3.2 漏桶算法(面试对比项,手写简化版)](#3.2 漏桶算法(面试对比项,手写简化版))
- [3.3 令牌桶vs漏桶(面试必对比,加分关键)](#3.3 令牌桶vs漏桶(面试必对比,加分关键))
- 四、全文总结
一、面试核心:为什么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脚本)、重入锁优化,到令牌桶和漏桶限流算法的完整实现,每一步都结合面试加分点进行详解。记住:面试手写代码不仅要"能跑通",更要讲清楚底层逻辑、边界情况和优化方案,这样才能在众多候选人中脱颖而出。
📌 最后:如果本文对你的面试有帮助,欢迎点赞、收藏、转发!有任何疑问或补充,评论区留言交流~ 关注我(予枫),后续持续分享更多面试手撕代码干货!