设计模式综合实战七:基于Redis实现分布式锁(单例+代理+策略+模板方法)
说实话,分布式锁这个话题,我每年面试都要问不少人。大部分人都能说出「用Redis实现分布式锁」这句话,但真到了落地层面,坑多得能让你怀疑人生。今天咱们就把这件事彻底讲透——用四个设计模式,搭一套真正能上生产的分布式锁框架。
为什么需要组合多种设计模式?
先说说我个人的习惯。我从来不为了用设计模式而用设计模式。每个模式的出现,一定是因为它解决了某个具体痛点。咱们来看看分布式锁有哪些痛点:
- 锁实例不能乱创建 —— 一个JVM里只需要一个Redis客户端连接池,这就得用单例
- 锁的行为需要扩展 —— 今天用单机Redis,明天可能用Redisson,后天可能用ZooKeeper,策略模式正好派上用场
- 加锁/解锁的流程是固定的 —— 先尝试获取锁,获取不到就重试或等待,这个骨架可以用模板方法固定下来
- 调用方不想关心锁的实现细节 —— 代理模式帮我们隐藏一切
你看,四个模式各司其职,缺一不可。
整体架构设计
先画一张图,让大家对整体结构有个直观印象。这张图我画了好几个版本,最终选了最清晰的一个。
这张图我建议你多看两眼。从上到下,每一层都只依赖它的下一层,职责非常清晰。我在项目中就吃过「职责不清」的亏——所有逻辑揉在一个类里,后来想换锁实现,改得想哭。
单例模式:锁工厂
先说说单例。Redis客户端连接池这种资源,一个JVM里只需要一份。我习惯用枚举方式实现单例,简单又安全。
public enum RedisClientSingleton {
INSTANCE;
private final JedisPool jedisPool;
RedisClientSingleton() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(32);
config.setMaxIdle(8);
this.jedisPool = new JedisPool(config, "localhost", 6379);
}
public Jedis getResource() {
return jedisPool.getResource();
}
}
嗯,这里要注意:枚举单例不仅能防反射攻击,还能保证序列化安全。我见过有人用双重检查锁写单例,结果序列化之后反序列化出来两个对象……那场面,挺尴尬的。
策略模式:可插拔的锁实现
策略模式的核心思想很简单:定义一族算法,把它们封装起来,让它们可以互相替换。咱们的分布式锁,不同的实现就是不同的策略。
public interface LockStrategy {
boolean tryLock(String key, String requestId, long expireMs);
boolean unlock(String key, String requestId);
}
public class RedisLockStrategy implements LockStrategy {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
@Override
public boolean tryLock(String key, String requestId, long expireMs) {
Jedis jedis = RedisClientSingleton.INSTANCE.getResource();
String result = jedis.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireMs);
return LOCK_SUCCESS.equals(result);
}
@Override
public boolean unlock(String key, String requestId) {
// 使用Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Jedis jedis = RedisClientSingleton.INSTANCE.getResource();
Object result = jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(requestId));
return Long.valueOf(1).equals(result);
}
}
为什么解锁要用Lua脚本?因为「判断+删除」这两个操作必须原子执行。我曾经见过一个线上事故:线程A判断锁是自己的,还没来得及删除,锁过期了,线程B拿到了锁,然后A把B的锁给删了……你说乱不乱?Lua脚本就是解决这个问题的。
模板方法模式:固定骨架
加锁和解锁的流程其实很固定:尝试获取锁 → 如果成功就执行业务逻辑 → 最后释放锁。这个骨架一旦定下来,就不应该让每个实现类去重复写。模板方法模式就是干这个的。
public abstract class AbstractDistributedLock {
protected final LockStrategy strategy;
public AbstractDistributedLock(LockStrategy strategy) {
this.strategy = strategy;
}
// 模板方法:定义骨架
public boolean executeWithLock(String key, String requestId,
long expireMs, Runnable task) {
boolean locked = false;
try {
locked = strategy.tryLock(key, requestId, expireMs);
if (locked) {
task.run();
return true;
}
return false;
} finally {
if (locked) {
strategy.unlock(key, requestId);
}
}
}
// 子类可以扩展的钩子方法
protected void beforeLock() {}
protected void afterUnlock() {}
}
你看,模板方法把「获取锁 → 执行业务 → 释放锁」这个流程固定下来了。子类只需要关心自己的业务逻辑,不用操心锁的生命周期管理。我在项目中经常用这个模式,它特别适合「流程固定但细节可变」的场景。
代理模式:统一入口
代理模式在这里的作用,说白了就是给调用方一个干净的门面。调用方不需要知道什么策略、什么模板,只需要一个锁接口。
public interface DistributedLock {
boolean lock(String key, long expireMs, Runnable task);
}
public class DistributedLockProxy implements DistributedLock {
private final AbstractDistributedLock lockTemplate;
public DistributedLockProxy(LockStrategy strategy) {
this.lockTemplate = new AbstractDistributedLock(strategy) {
// 可以在这里添加钩子逻辑
};
}
@Override
public boolean lock(String key, long expireMs, Runnable task) {
String requestId = UUID.randomUUID().toString();
return lockTemplate.executeWithLock(key, requestId, expireMs, task);
}
}
调用方用起来就很简单了:
DistributedLock lock = new DistributedLockProxy(new RedisLockStrategy());
lock.lock("order:123", 30000, () -> {
// 处理订单
processOrder();
});
如果哪天想换成Redisson,只需要改一行:
DistributedLock lock = new DistributedLockProxy(new RedissonLockStrategy());
这就是策略模式+代理模式的威力——调用方零改动。
避坑指南
我曾经踩过的坑,希望你避开:
- 锁的过期时间设置:太短业务没执行完锁就过期了,太长万一宕机锁释放不了。我一般设30秒,配合看门狗机制自动续期。
- requestId必须唯一:用UUID就行,千万别用线程ID。为什么?因为不同JVM的线程ID可能重复。
- Redis主从切换问题:如果用了主从架构,锁写入主库还没同步到从库,主库挂了,从库顶上,锁就丢了。RedLock算法可以解决,但性能有损耗,看业务取舍。
我的个人习惯:
生产环境我一般用Redisson,它内置了看门狗、可重入、信号量等高级功能。但自己手写一套理解原理,对面试和架构设计都很有帮助。你想想看,如果连底层原理都搞不清楚,出了问题怎么排查?
总结
咱们这一章,用四个设计模式搭了一套分布式锁框架:
| 设计模式 | 职责 | 核心类 |
|---|---|---|
| 单例模式 | 管理Redis连接池 | RedisClientSingleton |
| 策略模式 | 可插拔的锁实现 | LockStrategy / RedisLockStrategy |
| 模板方法模式 | 固定加锁/解锁骨架 | AbstractDistributedLock |
| 代理模式 | 统一调用入口 | DistributedLockProxy |
说实话,这套组合拳我用了好几年,从单机Redis到集群,从Java到C++,核心思想一直没变。设计模式不是花架子,它是解决真实问题的利器。希望你能把这套思路内化成自己的东西,下次遇到分布式锁,心里就有底了。
公众号:蓝海资料掘金营,微信deep3321