Redis 分布式锁实战

一、分布式锁的核心需求

在分布式系统中,当多个进程或服务需要访问共享资源时,分布式锁是保证数据一致性的关键。一个可靠的分布式锁需要满足以下特性:

  • 互斥性:同一时刻只有一个客户端持有锁

  • 不会死锁:即使持有锁的客户端崩溃,锁也能被释放

  • 容错性:只要大部分Redis节点存活,锁就能正常工作

  • 一致性:锁的状态在分布式环境中保持一致

二、Redis 分布式锁实现原理

1. 基础实现:SET NX EX 命令

Redis 的 SET 命令提供了原子性操作,这是实现分布式锁的基础:

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

public class RedisLock {
    private final Jedis jedis;
    private final String lockKey;
    private final int ttl;
    private final String lockValue;
    
    public RedisLock(Jedis jedis, String lockKey, int ttl) {
        this.jedis = jedis;
        this.lockKey = "lock:" + lockKey;
        this.ttl = ttl;
        this.lockValue = UUID.randomUUID().toString();
    }
    
    public boolean acquire() {
        // SET key value NX EX ttl
        // NX:只有key不存在时才设置
        // EX:设置过期时间(秒)
        String result = jedis.set(lockKey, lockValue, "NX", "EX", ttl);
        return "OK".equals(result);
    }
    
    public boolean release() {
        // 使用 Lua 脚本保证原子性释放
        String script = 
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('DEL', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        Long result = (Long) jedis.eval(script, 1, lockKey, lockValue);
        return result == 1;
    }
}

关键要点

  • 使用 NX 参数确保只有第一个请求能获取锁

  • 使用 EX 参数设置自动过期时间,防止死锁

  • 使用 UUID 作为锁的值,确保只有持有者能释放锁

  • 释放锁必须使用 Lua 脚本,避免误删其他客户端的锁

2. 为什么不用 EXIST + SET

很多人可能会这样实现:

复制代码
// 错误示例!存在竞态条件
if (!jedis.exists(lockKey)) {
    jedis.set(lockKey, value);
    return true;
}
return false;

问题existsset 之间存在时间窗口,多个客户端可能同时通过检查,导致锁被多个客户端持有。

三、生产环境进阶方案

1. Redlock 算法

当 Redis 部署为集群时,单节点锁可能存在单点故障问题。Redlock 算法通过多个节点来提高可靠性:

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

public class Redlock {
    private final List<Jedis> nodes;
    private final String lockKey;
    private final int ttl;
    private final int quorum;
    
    public Redlock(List<Jedis> nodes, String lockKey, int ttl) {
        this.nodes = nodes;
        this.lockKey = "lock:" + lockKey;
        this.ttl = ttl;
        this.quorum = (nodes.size() / 2) + 1;
    }
    
    public LockResult acquire() {
        String lockValue = UUID.randomUUID().toString();
        int acquiredCount = 0;
        long startTime = System.currentTimeMillis();
        
        for (Jedis node : nodes) {
            try {
                String result = node.set(lockKey, lockValue, "NX", "EX", ttl);
                if ("OK".equals(result)) {
                    acquiredCount++;
                }
            } catch (Exception e) {
                // 节点不可用,跳过
            }
        }
        
        // 超过半数节点成功获取锁,且在有效时间内
        long elapsed = System.currentTimeMillis() - startTime;
        if (acquiredCount >= quorum && elapsed < ttl * 1000) {
            return new LockResult(lockValue, acquiredCount);
        } else {
            // 释放已获取的锁
            release(lockValue);
            return new LockResult(null, 0);
        }
    }
    
    public void release(String lockValue) {
        String script = 
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('DEL', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        for (Jedis node : nodes) {
            try {
                node.eval(script, 1, lockKey, lockValue);
            } catch (Exception e) {
                // 节点不可用,跳过
            }
        }
    }
    
    public static class LockResult {
        public final String lockValue;
        public final int acquiredCount;
        
        public LockResult(String lockValue, int acquiredCount) {
            this.lockValue = lockValue;
            this.acquiredCount = acquiredCount;
        }
    }
}

适用场景

  • 需要极高可靠性的关键业务

  • Redis 集群部署环境

  • 对锁的安全性要求高于性能的场景

2. 带续约机制的分布式锁

当业务执行时间可能超过锁的过期时间时,需要定期续约:

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

public class RenewingLock {
    private final Jedis jedis;
    private final String lockKey;
    private final int ttl;
    private final int renewInterval;
    private final String lockValue;
    private final ScheduledExecutorService scheduler;
    private final AtomicBoolean stopped = new AtomicBoolean(false);
    
    public RenewingLock(Jedis jedis, String lockKey, int ttl, int renewInterval) {
        this.jedis = jedis;
        this.lockKey = "lock:" + lockKey;
        this.ttl = ttl;
        this.renewInterval = renewInterval;
        this.lockValue = UUID.randomUUID().toString();
        this.scheduler = Executors.newSingleThreadScheduledExecutor();
    }
    
    private void renew() {
        String script = 
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
            "else " +
            "    return 0 " +
            "end";
        scheduler.scheduleAtFixedRate(() -> {
            if (!stopped.get()) {
                try {
                    jedis.eval(script, 1, lockKey, lockValue, String.valueOf(ttl));
                } catch (Exception e) {
                    // 续约失败,可能锁已过期或节点不可用
                }
            }
        }, 0, renewInterval, TimeUnit.SECONDS);
    }
    
    public boolean acquire() {
        String result = jedis.set(lockKey, lockValue, "NX", "EX", ttl);
        if ("OK".equals(result)) {
            // 启动续约任务
            stopped.set(false);
            renew();
            return true;
        }
        return false;
    }
    
    public boolean release() {
        stopped.set(true);
        scheduler.shutdown();
        
        String script = 
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('DEL', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        Long result = (Long) jedis.eval(script, 1, lockKey, lockValue);
        return result == 1;
    }
}

适用场景

  • 业务执行时间不确定的场景

  • 需要长时间持有锁的操作

  • 防止锁被意外释放

四、实战应用场景

1. 库存扣减场景

复制代码
public Map<String, Object> deductStock(Jedis jedis, String productId, int quantity) {
    RedisLock lock = new RedisLock(jedis, "stock:" + productId, 10);
    
    if (!lock.acquire()) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", "系统繁忙,请稍后重试");
        return result;
    }
    
    try {
        // 获取当前库存
        String stockStr = jedis.get("stock:" + productId);
        if (stockStr == null || Integer.parseInt(stockStr) < quantity) {
            Map<String, Object> result = new HashMap<>();
            result.put("success", false);
            result.put("message", "库存不足");
            return result;
        }
        
        // 扣减库存
        jedis.decrBy("stock:" + productId, quantity);
        
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "扣减成功");
        return result;
    } finally {
        lock.release();
    }
}

2. 订单重复提交防护

复制代码
public Map<String, Object> createOrder(Jedis jedis, String userId, OrderData orderData) {
    // 用用户ID和业务标识作为锁key
    String lockKey = "order:" + userId + ":" + orderData.getOrderNo();
    RedisLock lock = new RedisLock(jedis, lockKey, 60);
    
    if (!lock.acquire()) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", "订单已提交");
        return result;
    }
    
    try {
        // 检查是否已存在订单
        if (checkOrderExists(orderData.getOrderNo())) {
            Map<String, Object> result = new HashMap<>();
            result.put("success", false);
            result.put("message", "订单已存在");
            return result;
        }
        
        // 创建订单
        Order order = saveOrder(orderData);
        
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("data", order);
        return result;
    } finally {
        lock.release();
    }
}

3. 异步任务幂等性保障

复制代码
public Map<String, Object> processTask(Jedis jedis, String taskId) {
    RedisLock lock = new RedisLock(jedis, "task:" + taskId, 300);
    
    if (!lock.acquire()) {
        // 任务正在处理中或已完成
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", "任务处理中");
        return result;
    }
    
    try {
        // 检查任务状态
        String status = jedis.get("task:" + taskId + ":status");
        if ("completed".equals(status)) {
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("message", "任务已完成");
            return result;
        }
        
        // 执行任务
        executeTask(taskId);
        
        // 标记任务完成
        jedis.set("task:" + taskId + ":status", "completed");
        
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "任务处理完成");
        return result;
    } finally {
        lock.release();
    }
}

五、常见问题与解决方案

1. 锁过期导致数据不一致

问题:业务执行时间超过锁的 TTL,锁被自动释放,其他客户端获取锁后修改数据。

解决方案

  • 预估业务执行时间,设置合理的 TTL

  • 使用续约机制延长锁的有效期

  • 在业务操作前再次验证锁的持有者

2. 锁释放时的误删问题

问题:客户端 A 的锁过期后,客户端 B 获取了锁,此时 A 执行完业务,误删了 B 的锁。

解决方案

  • 使用 UUID 作为锁的值

  • 释放锁时使用 Lua 脚本验证锁的持有者

  • 避免直接使用 DEL 命令

3. 集群环境下的一致性问题

问题:Redis 主从复制存在延迟,主节点宕机后,从节点可能未同步锁信息。

解决方案

  • 使用 Redlock 算法

  • 对数据一致性要求高的场景,使用强一致性存储

4. 网络分区导致的脑裂

问题:网络分区导致不同节点对锁的状态产生分歧。

解决方案

  • 设置合理的 quorum 数量

  • 使用 Redlock 的时钟偏移检测机制

  • 设置较短的锁过期时间

六、性能优化建议

1. 合理设置锁的粒度

  • 避免使用过大的锁粒度(如整个业务模块)

  • 尽量使用细粒度锁(如按商品ID、用户ID)

  • 减少锁的持有时间

2. 优化锁的获取策略

复制代码
public RedisLock acquireWithRetry(Jedis jedis, String lockKey, int maxRetries, long delayMs) {
    for (int i = 0; i < maxRetries; i++) {
        RedisLock lock = new RedisLock(jedis, lockKey, 30);
        if (lock.acquire()) {
            return lock;
        }
        try {
            Thread.sleep(delayMs);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }
    return null;
}

3. 使用连接池复用连接

复制代码
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

// 创建连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(20);
poolConfig.setMinIdle(5);

JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);

// 获取连接
try (Jedis jedis = jedisPool.getResource()) {
    // 执行操作
    jedis.set("key", "value");
}

4. 监控锁的使用情况

复制代码
public void monitorLockUsage(Jedis jedis) {
    // 获取所有锁的数量
    Set<String> lockKeys = jedis.keys("lock:*");
    System.out.println("当前持有锁数量: " + lockKeys.size());
    
    // 检查过期时间
    for (String key : lockKeys) {
        Long ttl = jedis.ttl(key);
        System.out.println(key + ": TTL=" + ttl + "秒");
    }
}

七、总结

Redis 分布式锁是分布式系统中不可或缺的工具,正确使用能有效保证数据一致性。核心要点:

  1. 原子性:使用 SET NX EX 命令保证锁获取的原子性

  2. 安全性:使用 UUID 和 Lua 脚本防止误删锁

  3. 可靠性:设置合理的过期时间,避免死锁

  4. 进阶方案:根据业务需求选择 Redlock 或续约机制

在实际应用中,需要根据业务场景选择合适的锁策略,平衡一致性、可用性和性能之间的关系。

相关推荐
粉嘟小飞妹儿5 小时前
Java Switch与Break用法详解
java·开发语言
艾莉丝努力练剑5 小时前
【QT】常用控件(三)Qt布局管理器(网格/表单/间隔器)
java·linux·运维·服务器·开发语言·网络·qt
骑士雄师5 小时前
python 的列表和java中的集合有什么区别
java·windows·python
それども5 小时前
redis 集群操作进阶 - hashtag
数据库·redis·缓存
尋找記憶的魚5 小时前
基于langchain4j的ai编程助手项目(完整篇)
java·人工智能·spring boot·langchain·ai编程
罗不俷5 小时前
从零搭建 Mac Java 开发环境:Homebrew + JDK + Maven + Git 全流程配置
java
折哥的程序人生 · 物流技术专研5 小时前
Java 23 种设计模式:从踩坑到精通 —— 开篇及系列介绍
java·开发语言·后端·设计模式·面试·架构
_日拱一卒5 小时前
LeetCode:124二叉树中的最大路径和
java·数据结构·算法
ch.ju5 小时前
Java程序设计(第3版)第四章——构造方法
java·开发语言