基于MySQL

悲观锁

通过 select ... for update 实现

  • 原理
    • 利用数据库的唯一索引特性,通过插入一条记录来获取锁,删除记录来释放锁。
  • 实现
    • 创建一张锁表,包含资源名称和唯一索引。
    • 获取锁时,尝试插入一条记录;如果插入成功,则获取锁;如果插入失败(唯一索引冲突),则锁已被占用。
    • 释放锁时,删除对应的记录。
  • 优点
    • 实现简单,依赖数据库。
  • 缺点
    • 性能较差,不适合高并发场景。
    • 需要处理数据库连接和事务问题。
CREATE TABLE `distributed_lock` (
  `key_resource` varchar(64) NOT NULL COMMENT '资源键',
  `status` enum('S','F','P') NOT NULL DEFAULT 'F' COMMENT '状态:S(成功)、F(失败)、P(处理中)',
  `lock_flag` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '锁标志:1(已锁)、0(未锁)',
  `begin_time` datetime(3) DEFAULT NULL COMMENT '开始时间(毫秒精度)',
  `end_time` datetime(3) DEFAULT NULL COMMENT '结束时间(毫秒精度)',
  `client_ip` varchar(45) DEFAULT NULL COMMENT '抢到锁的客户端IP地址',
  `timeout` int(10) unsigned DEFAULT NULL COMMENT '锁的超时时间(单位:秒)',
  PRIMARY KEY (`key_resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.*;

// MySQL 驱动坐标:mysql:mysql-connector-java:8.0.22
public class UniqueIndexDistributedLock {
    /**
     * 尝试获取锁
     *
     * @param keyResource    资源键
     * @param clientIp       客户端IP地址
     * @param timeoutSeconds 锁的超时时间(单位:秒)
     * @return 是否获取成功
     */
    public boolean tryLock(String keyResource, String clientIp, int timeoutSeconds) {
        String selectSql = "SELECT * FROM distributed_lock WHERE key_resource = ? FOR UPDATE";
        String insertSql = "INSERT INTO distributed_lock (key_resource, status, lock_flag, begin_time, client_ip, timeout) VALUES (?, 'P', 1, NOW(3), ?, ?)";
        String updateSql = "UPDATE distributed_lock SET lock_flag = 1, status = 'P', begin_time = NOW(3), client_ip = ?, timeout = ? WHERE key_resource = ?";

        try (Connection connection = DataSourceUtil.getConnection()) {
            connection.setAutoCommit(false); // 开启事务

            try (PreparedStatement selectStatement = connection.prepareStatement(selectSql)) {
                selectStatement.setString(1, keyResource);
                try (ResultSet rs = selectStatement.executeQuery()) {
                    if (rs.next()) {
                        // 检查锁是否已被占用
                        int lockFlag = rs.getInt("lock_flag");
                        if (lockFlag == 1) {
                            // 检查锁是否已超时
                            long beginTime = rs.getTimestamp("begin_time").getTime();
                            long currentTime = System.currentTimeMillis();
                            if (currentTime - beginTime < rs.getInt("timeout") * 1000L) {
                                connection.rollback();
                                return false; // 锁已被占用且未超时
                            }
                        }
                        // 锁已超时或未占用,更新锁
                        try (PreparedStatement updateStatement = connection.prepareStatement(updateSql)) {
                            updateStatement.setString(1, clientIp);
                            updateStatement.setInt(2, timeoutSeconds);
                            updateStatement.setString(3, keyResource);
                            updateStatement.executeUpdate();
                        }
                    } else {
                        // 插入新锁
                        try (PreparedStatement insertStatement = connection.prepareStatement(insertSql)) {
                            insertStatement.setString(1, keyResource);
                            insertStatement.setString(2, clientIp);
                            insertStatement.setInt(3, timeoutSeconds);
                            insertStatement.executeUpdate();
                        }
                    }
                }
            }

            connection.commit(); // 提交事务
            return true;
        } catch (SQLException e) {
            throw new RuntimeException("获取锁失败", e);
        }
    }

    /**
     * 释放锁
     *
     * @param keyResource 资源键
     */
    public void unlock(String keyResource) {
        String sql = "UPDATE distributed_lock SET lock_flag = 0, status = 'S', end_time = NOW(3) WHERE key_resource = ?";
        try (Connection connection = DataSourceUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, keyResource);
            statement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("释放锁失败", e);
        }
    }

    // 测试
    public static void main(String[] args) throws SQLException {
        UniqueIndexDistributedLock lock = new UniqueIndexDistributedLock();

        String keyResource = "resource_1";
        String clientIp = "192.168.1.1";
        int timeoutSeconds = 10; // 锁的超时时间为 10 秒

        if (lock.tryLock(keyResource, clientIp, timeoutSeconds)) {
            try {
                System.out.println("获取锁成功,执行业务逻辑...");
                Thread.sleep(5000); // 模拟业务逻辑
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(keyResource);
                System.out.println("释放锁成功");
            }
        } else {
            System.out.println("获取锁失败");
        }
    }
}

// 是用 HikariCP 连接池来管理数据库连接,避免频繁创建和关闭连接
// 适配 Java 8,使用 HikariCP 4.x 版本: com.zaxxer:HikariCP:4.0.3
class DataSourceUtil {
    private static final HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC");
        config.setUsername("root");
        config.setPassword("1234");
        config.setMaximumPoolSize(10); // 根据需求调整
        dataSource = new HikariDataSource(config);
    }

    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

乐观锁

  • 原理
    • 通过版本号或时间戳实现乐观锁。
  • 实现
    • 在数据表中增加一个版本号字段。
    • 获取锁时,读取当前版本号,更新时检查版本号是否一致。
    • 如果版本号一致,则更新成功并获取锁;否则,获取锁失败。
  • 优点
    • 避免悲观锁的性能问题。
  • 缺点
    • 需要重试机制,可能增加复杂度。

在商品表中添加一个 version 字段,表示数据的版本号;每次更新库存时,检查版本号是否匹配:

CREATE TABLE `product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `name` varchar(255) NOT NULL COMMENT '商品名称',
  `stock` int(10) NOT NULL COMMENT '库存数量',
  `version` int(10) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

更新库存的 SQL:

UPDATE product
SET stock = stock - 1,
    version = version + 1
WHERE id = ? AND version = ? AND stock > 0;
public boolean reduceStock(long productId, int quantity) {
    String selectSql = "SELECT stock, version FROM product WHERE id = ?";
    String updateSql = "UPDATE product SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ? AND stock >= ?";

    try (Connection connection = DataSourceUtil.getConnection();
         PreparedStatement selectStatement = connection.prepareStatement(selectSql)) {
        selectStatement.setLong(1, productId);
        try (ResultSet rs = selectStatement.executeQuery()) {
            if (rs.next()) {
                int stock = rs.getInt("stock");
                int version = rs.getInt("version");
                if (stock >= quantity) {
                    try (PreparedStatement updateStatement = connection.prepareStatement(updateSql)) {
                        updateStatement.setInt(1, quantity);
                        updateStatement.setLong(2, productId);
                        updateStatement.setInt(3, version);
                        updateStatement.setInt(4, quantity);
                        int rows = updateStatement.executeUpdate();
                        return rows > 0; // 更新成功表示库存扣减成功
                    }
                }
            }
        }
        return false; // 库存不足或版本号不匹配
    } catch (SQLException e) {
        throw new RuntimeException("库存扣减失败", e);
    }
}

基于Redis

  • 原理
    • 利用 Redis 的 SETNX(SET if Not eXists)命令实现锁的获取和释放。
  • 实现
    • 获取锁时,使用 SETNX 设置一个键值对,并设置过期时间(避免死锁)。
    • 释放锁时,删除对应的键。
  • 优化
    • 使用 Redlock 算法(Redis 官方推荐的分布式锁算法),通过多个 Redis 实例实现高可用。
  • 优点
    • 性能高,适合高并发场景。
  • 缺点
    • 需要处理锁的过期时间和续期问题。

基于 SETNX 的实现

使用 SETNX(SET if Not eXists)命令尝试设置一个键值对。如果键不存在,则设置成功,获取锁;否则,获取锁失败。

Redis 在 2.6.12 版本之前,set 不支持 nx 参数,只能通过 setnx + expire 完成一个锁

import redis.clients.jedis.Jedis;

public class DistributedLock {
    private Jedis jedis;

    public DistributedLock(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey    分布式锁的键
     * @param requestId  请求标识(用于释放锁时验证)
     * @param expireTime 失效时间,单位毫秒
     * @return
     */
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
            // 设置锁的过期时间,避免死锁
            jedis.pexpire(lockKey, expireTime);
            return true;
        }
        return false;
    }

    private static final String UNLOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";

    public boolean unlock(String lockKey, String requestId) {
        // 使用 Lua 脚本保证原子性
        // 只有在锁的值与 requestId 匹配时才会删除锁,避免误删其他请求的锁
        Object result = jedis.eval(UNLOCK_SCRIPT, 1, lockKey, requestId);
        // Lua 脚本返回 1 表示解锁成功,返回 0 表示解锁失败
        return Long.valueOf(1).equals(result);
    }

    // 测试代码
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        DistributedLock lock = new DistributedLock(jedis);

        String lockKey = "distributed_lock";
        String requestId = "request_123";
        long expireTime = 5000;
        if (lock.tryLock(lockKey, requestId, expireTime)) {
            try {
                System.out.println("获取锁成功,执行业务逻辑...");

                // 模拟业务逻辑
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.unlock(lockKey, requestId))
                    System.out.println("释放锁成功");
                else
                    System.out.println("释放锁失败");
            }
        } else {
            System.out.println("获取锁失败");
        }
    }
}

问题:加锁操作和设置超时时间是分开的,不是原子操作。

Redis 支持通过 Lua 脚本执行多个命令,这些命令在 Redis 中是原子执行的。Lua 脚本 UNLOCK_SCRIPT

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
  • KEYS[1] 是传入的锁键(lockKey
  • ARGV[1] 是传入的请求标识(requestId

基于 SET 的实现

使用 SET 命令的 NX(Not eXists)和 PX(毫秒级过期时间)选项,实现原子性的锁获取和过期时间设置。

  • NX:表示 “Only set the key if it does not already exist”(仅当键不存在时才设置)。
    • 如果键已经存在,则 SET 命令不会执行任何操作,返回 nil
    • 如果键不存在,则设置键的值,并返回 OK
  • PX:表示 “Set the specified expire time, in milliseconds”(设置键的过期时间,单位为毫秒)。
  • XX:表示 “Only set the key if it already exists”(仅当键存在时才设置)。
    • 如果键不存在,则 SET 命令不会执行任何操作,返回 nil
    • 如果键存在,则设置键的值,并返回 OK
  • EX:表示 “Set the specified expire time, in seconds”(设置键的过期时间,单位为秒)。
  • KEEPTTL:表示 “Retain the time to live associated with the key”(保留键的剩余过期时间)。
    • 如果键已经设置了过期时间,使用 KEEPTTL 可以保留原有的过期时间。

加锁

SET lock_key lock_value NX PX 10000

解锁,使用 Lua 脚本确保解锁操作的原子性:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

需要考虑的问题

  • 问题一:锁过期释放了,业务还没执行完。如果业务逻辑执行时间超过了锁的过期时间(LOCK_EXPIRE_TIME),锁会自动释放,导致其他客户端可以获取锁,从而引发并发问题。
    • 解决:锁续期(Watchdog 机制),在业务逻辑执行期间,定期(如每隔 10 秒)检查锁是否仍然持有,并延长锁的过期时间。可以使用 Redis 的 PEXPIRE 命令实现锁续期。
  • 问题二:锁被别的线程误删。如果客户端 A 获取锁后,由于某些原因(如长时间 GC 或网络延迟)未能及时释放锁,锁过期后被客户端 B 获取;客户端 A 恢复后,可能会误删客户端 B 的锁。
    • 解决:在设置锁时,为每个客户端生成一个唯一的标识(如 UUID),并将其作为锁的值。释放锁时,先检查锁的值是否与当前客户端的标识一致,只有一致时才删除锁。

解决锁误删问题:

public boolean tryLock(String requestId) {
    SetParams params = SetParams.setParams().nx().px(LOCK_EXPIRE_TIME);
    String result = jedis.set(LOCK_KEY, requestId, params);
    return "OK".equals(result);
}
public void unlock(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.eval(script, 1, LOCK_KEY, requestId);
}

每个客户端使用唯一的 requestId 作为锁的值,避免锁的值冲突。只有当前客户端持有锁(即锁的值与 requestId 匹配)时,才允许删除锁。

测试:

Jedis jedis = new Jedis("localhost", 6379);
RedisDistributedLock lock = new RedisDistributedLock(jedis);

String requestId1 = "request_123";
String requestId2 = "request_456";

// 客户端 A 获取锁
if (lock.tryLock(requestId1)) {
    System.out.println("客户端 A 获取锁成功");

    // 模拟客户端 B 尝试获取锁
    if (lock.tryLock(requestId2)) {
        System.out.println("客户端 B 获取锁成功");
    } else {
        System.out.println("客户端 B 获取锁失败");
    }

    // 客户端 A 释放锁
    lock.unlock(requestId1);
    System.out.println("客户端 A 释放锁");

    // 客户端 B 再次尝试获取锁
    if (lock.tryLock(requestId2)) {
        System.out.println("客户端 B 获取锁成功");
        lock.unlock(requestId2);
        System.out.println("客户端 B 释放锁");
    } else {
        System.out.println("客户端 B 获取锁失败");
    }
} else {
    System.out.println("客户端 A 获取锁失败");
}

解决锁过期+锁误删问题:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class RedisDistributedLockWithWatchdog {

    private static final String LOCK_KEY = "distributed_lock";
    private static final int LOCK_EXPIRE_TIME = 5000; // 锁的过期时间(毫秒)
    private static final int WATCHDOG_INTERVAL = 4000; // 锁续期间隔(毫秒)

    private Jedis jedis;
    private ScheduledExecutorService watchdogExecutor;
    private String requestId;

    public RedisDistributedLockWithWatchdog(Jedis jedis) {
        this.jedis = jedis;
        this.watchdogExecutor = Executors.newSingleThreadScheduledExecutor();
    }

    /**
     * 尝试获取锁
     *
     * @param requestId 请求标识
     * @return 是否获取成功
     */
    public boolean tryLock(String requestId) {
        this.requestId = requestId;
        SetParams params = SetParams.setParams().nx().px(LOCK_EXPIRE_TIME);
        if ("OK".equals(jedis.set(LOCK_KEY, requestId, params))) {
            // 启动 Watchdog 定期续期
            watchdogExecutor.scheduleAtFixedRate(this::renewLock, WATCHDOG_INTERVAL, WATCHDOG_INTERVAL, TimeUnit.MILLISECONDS);
            return true;
        }
        return false;
    }

    /**
     * 锁续期
     */
    private void renewLock() {
        // 使用 Lua 脚本保证原子性
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
        jedis.eval(script, 1, LOCK_KEY, requestId, String.valueOf(LOCK_EXPIRE_TIME));
    }

    /**
     * 释放锁
     */
    public void unlock() {
        // 使用 Lua 脚本保证原子性
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, 1, LOCK_KEY, requestId);
        watchdogExecutor.shutdown(); // 停止 Watchdog
    }
}

通过一个后台线程(Watchdog)定期检查锁的状态,并在锁即将过期时延长其过期时间,从而避免锁在业务逻辑执行期间过期。

  • 使用 ScheduledExecutorService 定期执行锁续期任务。
  • 每隔 WATCHDOG_INTERVAL 时间,检查锁是否仍然持有,并延长锁的过期时间。

测试:

Jedis jedis = new Jedis("localhost", 6379);
RedisDistributedLockWithWatchdog lock = new RedisDistributedLockWithWatchdog(jedis);

String requestId = "request_123";
if (lock.tryLock(requestId)) {
    try {
        System.out.println("获取锁成功,执行业务逻辑...");
        // 模拟业务逻辑
        Thread.sleep(15000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
        System.out.println("释放锁成功");
    }
} else {
    System.out.println("获取锁失败");
}

通过 redis> TTL key_name 可以 key 的剩余过期时间(单位为秒)。当 key 不存在时,返回 -2 ;当 key 存在但没有设置剩余生存时间时,返回 -1

参数 设置规则 推荐值
LOCK_EXPIRE_TIME 大于业务逻辑的最长执行时间,通常为 1.5 倍到 2 倍。 例如,业务逻辑最长 10 秒,设置为 15 秒到 20 秒。
WATCHDOG_INTERVAL 小于 LOCK_EXPIRE_TIME,通常为 LOCK_EXPIRE_TIME 的 1/3 到 1/2。 例如,LOCK_EXPIRE_TIME 为 30 秒,设置为 10 秒到 15 秒。

优化

使用 JedisPool 来管理 Redis 连接,避免频繁创建和销毁连接带来的性能开销,同时也能防止连接泄漏:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;

// Jedis 3.x 及以上版本
public class DistributedLock {
    private final JedisPool jedisPool;

    public DistributedLock(String host, int port) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100); // 最大连接数
        poolConfig.setMaxIdle(50);   // 最大空闲连接数
        poolConfig.setMinIdle(10);   // 最小空闲连接数
        poolConfig.setTestOnBorrow(true); // 获取连接时测试有效性
        poolConfig.setTestWhileIdle(true); // 空闲时定期测试有效性

        this.jedisPool = new JedisPool(poolConfig, host, port);
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey    分布式锁的键
     * @param requestId  请求标识(用于释放锁时验证)
     * @param expireTime 失效时间,单位毫秒
     * @return
     */
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        try (Jedis jedis = jedisPool.getResource()) {
            SetParams params = SetParams.setParams().nx().px(expireTime);
            String result = jedis.set(lockKey, requestId, params);
            return "OK".equals(result);
        }
    }

    private static final String UNLOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";

    public boolean unlock(String lockKey, String requestId) {
        try (Jedis jedis = jedisPool.getResource()) {
            // 使用 Lua 脚本保证原子性
            // 只有在锁的值与 requestId 匹配时才会删除锁,避免误删其他请求的锁
            Object result = jedis.eval(UNLOCK_SCRIPT, 1, lockKey, requestId);
            // Lua 脚本返回 1 表示解锁成功,返回 0 表示解锁失败
            return Long.valueOf(1).equals(result);
        }
    }

    public void close() {
        if (jedisPool != null && !jedisPool.isClosed()) {
            jedisPool.close();
        }
    }

    // 测试代码
    public static void main(String[] args) {
        DistributedLock lock = new DistributedLock("localhost", 6379);
        try {
            String lockKey = "distributed_lock";
            String requestId = "request_123";
            long expireTime = 5000;

            boolean lockAcquired = lock.tryLock(lockKey, requestId, expireTime);
            if (lockAcquired) {
                System.out.println("获取锁成功,执行业务逻辑...");
                try {
                    // 模拟业务逻辑
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    boolean unlockSuccess = lock.unlock(lockKey, requestId);
                    if (unlockSuccess)
                        System.out.println("释放锁成功");
                    else
                        System.out.println("释放锁失败");
                }
            } else {
                System.out.println("获取锁失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭连接池
            lock.close();
            System.out.println("连接池已关闭");
        }
    }
}

配置合理的连接池参数

  • maxTotal:连接池的最大连接数。根据业务并发量和 Redis 服务器的性能设置。
  • maxIdle:连接池中最大空闲连接数。
  • minIdle:连接池中最小空闲连接数。
  • testOnBorrow:在从连接池获取连接时,是否测试连接的有效性。
  • testWhileIdle:在连接空闲时,是否定期测试连接的有效性。

基于 Redlock 的实现

Redlock 是 Redis 官方推荐的分布式锁算法,它的核心思想是通过多个独立的 Redis 实例来实现锁的获取和释放,从而避免单点故障问题。获取锁时,需要在大多数 Redis 实例上成功获取锁,才算真正获取锁。

Redlock 的步骤

  1. 获取当前时间:记录获取锁的开始时间。
  2. 依次尝试获取锁
    • 向多个独立的 Redis 实例发送 SET 命令,尝试获取锁。
    • 每个实例的锁设置相同的 keyvalue 和过期时间。
  3. 计算获取锁的时间
    • 计算从开始获取锁到成功获取锁所花费的时间。
    • 如果获取锁的时间小于锁的过期时间,并且成功获取锁的实例数超过半数,则认为获取锁成功。
  4. 检查是否满足以下条件:
    • 成功获取锁的实例数超过半数。
    • 获取锁的时间 elapsedTime 小于锁的过期时间 expireTime
  5. 释放锁
    • 向所有 Redis 实例发送 DEL 命令,释放锁。

Redlock 的实现

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.Arrays;
import java.util.List;

public class RedlockDistributedLock {

    private static final int LOCK_EXPIRE_TIME = 30000; // 锁的过期时间(毫秒)
    private static final int RETRY_COUNT = 3; // 重试次数
    private static final int RETRY_DELAY = 200; // 重试延迟(毫秒)

    private List<Jedis> jedisList;

    public RedlockDistributedLock(List<Jedis> jedisList) {
        this.jedisList = jedisList;
    }

    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        int successCount = 0;

        for (int i = 0; i < RETRY_COUNT; i++) {
            for (Jedis jedis : jedisList) {
                SetParams params = SetParams.setParams().nx().px(expireTime);
                if ("OK".equals(jedis.set(lockKey, requestId, params))) {
                    successCount++;
                }
            }

            if (successCount > jedisList.size() / 2) {
                return true;
            }

            // 重试前等待(不是最佳实现,会阻塞当前线程)
            // 防止客户端在获取锁失败后立即重试,
            try {
                Thread.sleep(RETRY_DELAY);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return false;
    }

    private static final String UNLOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";

    public void unlock(String lockKey, String requestId) {
        for (Jedis jedis : jedisList) {
            jedis.eval(UNLOCK_SCRIPT, 1, lockKey, requestId);
        }
    }

    // 测试
    public static void main(String[] args) {
        List<Jedis> jedisList = Arrays.asList(
                new Jedis("localhost", 6379),
                new Jedis("localhost", 6380),
                new Jedis("localhost", 6381),
                new Jedis("localhost", 6382)
        );
        RedlockDistributedLock lock = new RedlockDistributedLock(jedisList);

        String lockKey = "distributed_lock";
        String requestId = "request_123";
        long expireTime = 5000;
        if (lock.tryLock(lockKey, requestId, expireTime)) {
            try {
                System.out.println("获取锁成功,执行业务逻辑...");
                // 模拟业务逻辑
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(lockKey, requestId);
                System.out.println("释放锁成功");
            }
        } else {
            System.out.println("获取锁失败");
        }
    }
}
  • windows 启动 reids 时指定端口:redis-server.exe --port 6379

  • 连接 redis-server 时指定端口:redis-cli.exe -h 127.0.0.1 -p 6379

比较

实现方式 优点 缺点 适用场景
SETNX 实现简单 需要单独设置过期时间,存在死锁风险 简单的分布式锁场景
SET 原子性操作,避免死锁 需要处理 Lua 脚本 高并发场景
Redlock 高可用,适合多 Redis 实例场景 实现复杂,性能较低 对可靠性要求高的场景

YOLO