设计模式综合实战七:基于Redis实现分布式锁(单例+代理+策略+模板方法)

说实话,分布式锁这个话题,我每年面试都要问不少人。大部分人都能说出「用Redis实现分布式锁」这句话,但真到了落地层面,坑多得能让你怀疑人生。今天咱们就把这件事彻底讲透——用四个设计模式,搭一套真正能上生产的分布式锁框架。

为什么需要组合多种设计模式?

先说说我个人的习惯。我从来不为了用设计模式而用设计模式。每个模式的出现,一定是因为它解决了某个具体痛点。咱们来看看分布式锁有哪些痛点:

  • 锁实例不能乱创建 —— 一个JVM里只需要一个Redis客户端连接池,这就得用单例
  • 锁的行为需要扩展 —— 今天用单机Redis,明天可能用Redisson,后天可能用ZooKeeper,策略模式正好派上用场
  • 加锁/解锁的流程是固定的 —— 先尝试获取锁,获取不到就重试或等待,这个骨架可以用模板方法固定下来
  • 调用方不想关心锁的实现细节 —— 代理模式帮我们隐藏一切

你看,四个模式各司其职,缺一不可。

整体架构设计

先画一张图,让大家对整体结构有个直观印象。这张图我画了好几个版本,最终选了最清晰的一个。

基于Redis分布式锁 — 设计模式组合架构图 业务代码(客户端) 调用方只需注入锁接口,无需关心底层实现 代理模式 — DistributedLockProxy 统一入口:自动重试、超时控制、异常处理 模板方法模式 — AbstractDistributedLock 定义加锁/解锁骨架:tryLock → 执行业务 → unlock 策略模式 — RedisLockStrategy / RedissonLockStrategy / ZkLockStrategy 可插拔:运行时动态切换锁的实现方式 单例模式 Redis连接池

这张图我建议你多看两眼。从上到下,每一层都只依赖它的下一层,职责非常清晰。我在项目中就吃过「职责不清」的亏——所有逻辑揉在一个类里,后来想换锁实现,改得想哭。

单例模式:锁工厂

先说说单例。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