在使用 Redis 实现分布式锁时,看似简单(一个 SETNX 搞定),但在高并发和分布式环境下,要做到可靠性安全性,需要深入考虑以下几个核心问题:


1. 互斥性与原子性 (Atomicity)

这是分布式锁最基本的要求。必须保证在任何时刻,只有一个客户端能持有锁。

  • 解决方案:不能先用 EXISTS 判断再用 SET,这存在竞态条件。必须使用原子指令。
  • 最佳实践:使用 SET key value NX PX milliseconds。其中 NX 保证只有不存在时才设置成功,PX 保证原子性地设置过期时间。

2. 防死锁 (Deadlock Prevention)

如果客户端获取锁后崩溃或网络中断,锁没有被显式释放,其他客户端将永远无法获取锁。

  • 解决方案:必须为锁设置过期时间(TTL)
  • 注意:过期时间不能设置得太短(任务没执行完就过期)或太长(崩溃后恢复慢)。

3. 锁的错误释放 (Safe Unlock)

客户端 A 获取了锁,但由于执行任务过长,导致锁自动过期。此时客户端 B 获取了锁。随后客户端 A 执行完任务,调用 DEL 释放锁,却把客户端 B 正在使用的锁给删了。

  • 解决方案解铃还须系铃人
    • 在加锁时,value 必须是一个唯一标识符(如 UUID)。
    • 在释放锁时,先检查锁的 value 是否等于自己的标识符,相等才删除。
    • 原子性要求:检查和删除必须是原子的,通常使用 Lua 脚本来实现。
-- Lua 脚本:释放锁
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

4. 锁的自动续期 (Lock Renewal)

如果业务逻辑执行时间超过了锁的过期时间,锁被自动释放会导致并发安全问题。

  • 解决方案看门狗机制 (Watch Dog)
    • 在获取锁成功后,开启一个后台守护线程。
    • 每隔一段时间(如过期时间的 1/3)检查锁是否还存在,如果存在则重置过期时间。
    • Redisson(Java 客户端)就内置了这种机制。

5. 集群环境下的可靠性 (Redlock)

在 Redis 主从架构或哨兵模式下,主从同步是异步的。

  • 风险点:客户端 A 在 Master 上加锁成功,Master 还没来得及同步给 Slave 就挂了。Slave 升级为新 Master,客户端 B 请求加锁也能成功。此时两个客户端同时持有锁。
  • 解决方案:Redis 官方提出了 Redlock 算法
    • 在多个(通常是 5 个)独立的 Redis 实例上尝试加锁。
    • 只有在超过半数(N/2 + 1)的节点加锁成功,且总耗时小于锁有效时间时,才认为加锁成功。
    • 注:Redlock 在业界仍有争议,但在绝大多数业务场景下,单机/哨兵模式配合合理的超时机制已足够。

6. 锁等待策略 (Blocking vs Polling)

当锁被占用时,未获取到锁的客户端该怎么办?

  • 轮询 (Polling):不断尝试加锁。缺点是浪费 CPU,建议增加退避算法(如随机延迟)。
  • 发布/订阅 (Pub/Sub):客户端订阅锁释放的消息,收到消息后再尝试获取。这能显著降低 Redis 的压力。

总结对照表

考虑维度 核心风险 应对方案
原子性 加锁与设置超时非原子执行 SET key value NX PX
安全性 A 释放了 B 的锁 唯一标识 (UUID) + Lua 脚本
可靠性 业务没跑完锁过期了 看门狗 (Watch Dog) 自动续期
可用性 Redis Master 宕机同步延迟 Redlock 算法或增加手动补偿
性能 自旋锁占用 CPU 结合 Pub/Sub 或限时阻塞

示例


import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁实现
 */
public class RedisDistributedLock {
    /**
     * map默认容量
     */
    private static final int DEFAULT_MAP_SIZE = 16;

    /**
     * 默认锁过期时间/秒
     */
    private static final int DEFAULT_TIME_OUT_SECOND = 10;

    /**
     * 默认锁过期时间/毫秒
     */
    private static final int DEFAULT_TIME = DEFAULT_TIME_OUT_SECOND * 1000;

    /**
     * 表示过期时间单位是milliseconds
     */
    private static final String PX = "px";

    /**
     * 表示当key存在时才set值
     */
    private static final String XX = "xx";

    /**
     * 表示当key不存在时才set值
     */
    private static final String NX = "nx";

    /**
     * 表示过期时间单位是秒
     */
    private static final String EX = "ex";

    /**
     * 加锁成功
     */
    private static final String LOCK_SUCCESS = "OK";

    /**
     * 锁过期时间处理线程池
     */
    private static final ScheduledExecutorService executorService = Executors
            .newScheduledThreadPool(Runtime.getRuntime().availableProcessors() / 2, new LockThreadFactory());

    /**
     * 缓存锁与客户端唯一标识的映射
     */
    private static final ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>(DEFAULT_MAP_SIZE);

    /**
     * 处理锁过期时间
     */
    private ConcurrentHashMap<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>();

    private SecureRandom sr;


    public RedisDistributedLock() {
        this.sr = new SecureRandom();
    }

    /**
     * 加锁
     *
     * @param key 加锁的key
     * @return 是否加锁成功
     */
    public boolean lock(String key) {
        return lock(key, DEFAULT_TIME);
    }

    /**
     * 加锁
     *
     * @param key     加锁的key
     * @param timeOut 锁过期时间
     * @return 是否加锁成功
     */
    public boolean lock(String key, long timeOut) {
        if (StringUtils.isEmpty(key)) {
            return false;
        }
        // 随机生成客户id 防止其他线程区释放锁
        String clientId = String.valueOf(sr.nextLong());
        long startTime = System.currentTimeMillis();
        long expireTime = timeOut > 0 && timeOut < DEFAULT_TIME ? timeOut : DEFAULT_TIME;
        // 获取锁 原子性 无中断
        boolean isLock = lockUnInterrupted(key, clientId, expireTime);

        if (isLock) {
            map.put(key, clientId);
            // 处理业务时间过长导致锁过期,1秒钟定期检查
            ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    // 超时的话 释放锁 否则锁续命
                    if (timeOut == 0 || (System.currentTimeMillis() - startTime) > DEFAULT_TIME) {
                        System.out.println("timeout release lock, key: " + key + " clientId: " + clientId);
                        unlockUnInterrupted(key, clientId);
                    } else {
                        System.out.println("---expire key: " + key + " clientId: " + clientId);
                        updateExpireTime(key, clientId, expireTime);
                    }
                }
            }, 1, 1, TimeUnit.SECONDS);

            ScheduledFuture future = futureMap.put(clientId, scheduledFuture);
            System.out.println("---------- " + future);
            if (future != null) {
                future.cancel(false);
            }

            if (System.currentTimeMillis() - startTime > DEFAULT_TIME) {
                System.out.println("lock cost time is " + (System.currentTimeMillis() - startTime) + " second");
            }
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     *
     * @param key 释放锁key
     */
    public void unlock(String key) {
        if (StringUtils.isEmpty(key)) {
            return;
        }

        String clientId = map.get(key);
        if (clientId == null) {
            return;
        }

        map.remove(key);
        ScheduledFuture future = futureMap.remove(clientId);
        if (future != null) {
            future.cancel(false);
        }

        System.out.println("clear redis lock  key: " + key + " clientId: " + clientId);
        unlockUnInterrupted(key, clientId);
    }

    /**
     * 更新锁的过期时间
     *
     * @param key      释放锁key
     * @param clientId 锁拥有的客户端
     * @param timeOut  过期时间
     */
    public void updateExpireTime(String key, String clientId, long timeOut) {
        Jedis jedis = null;
        try {
            jedis = JedisUtils.getJedis();
            if (jedis == null) {
                return;
            }
            // 如果存在 给key设置过期时间 保证原子性
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";

            Object res = jedis.eval(script, Collections.singletonList(key), Arrays.asList(clientId, (timeOut / 1000) + ""));
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 原子性加锁
     *
     * @param key      加锁的key
     * @param clientId 锁拥有的客户端
     * @param expire   过期时间
     * @return 加锁是否成功
     */
    public boolean lockUnInterrupted(String key, String clientId, long expire) {
        Jedis jedis = null;
        try {
            jedis = JedisUtils.getJedis();
            if (jedis != null) {
                // set key value nx px timeOut 保证原子性
                SetParams setParams = new SetParams();
                setParams.nx();
                setParams.px(expire);
                String res = jedis.set(key, clientId, setParams);
                return LOCK_SUCCESS.equals(res);
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param key      释放锁key
     * @param clientId 锁拥有的客户端
     */
    public void unlockUnInterrupted(String key, String clientId) {
        Jedis jedis = null;
        try {
            jedis = JedisUtils.getJedis();
            if (jedis == null) {
                return;
            }
            // 如果存在,删除key 保证原子性
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            Object res = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(clientId));
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }


}



Redisson

在 Java 生态中,虽然可以手写 Lua 脚本实现分布式锁,但生产环境通常推荐使用 Redisson。它已经完美解决了我在上条回答中提到的:原子性、看门狗自动续期、锁重入、以及释放他人锁等所有坑。

以下是基于 Redisson 的完整示例,涵盖了配置、加锁及业务逻辑。


1. 引入依赖 (Maven)

pom.xml 中添加 Redisson 依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.21.0</version> </dependency>

2. 完整 Java 示例代码

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

public class RedisLockDemo {

    public static void main(String[] args) {
        // 1. 配置 Redisson
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setDatabase(0);
              // .setPassword("your_password"); // 如果有密码

        // 2. 创建 Redisson 实例
        RedissonClient redisson = Redisson.create(config);

        // 3. 获取锁实例(注意:此时并未在 Redis 中加锁,只是获取了一个锁对象)
        String lockKey = "order_payment_lock_1001";
        RLock lock = redisson.getLock(lockKey);

        try {
            /**
             * 4. 尝试加锁
             * waitTime:  等待获取锁的最长时间
             * leaseTime: 锁自动释放的时间(如果不设置或设为 -1,则启用 Watch Dog 看门狗)
             * unit:      时间单位
             */
            boolean isLocked = lock.tryLock(10, -1, TimeUnit.SECONDS);

            if (isLocked) {
                try {
                    System.out.println("成功获取分布式锁,开始执行核心业务逻辑...");
                    
                    // 模拟业务处理耗时
                    Thread.sleep(5000);
                    
                    System.out.println("业务处理完成。");
                } finally {
                    // 5. 释放锁(必须放在 finally 中,且判断是否还被当前线程持有)
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                        System.out.println("成功释放锁。");
                    }
                }
            } else {
                System.err.println("获取锁失败,可能有其他线程正在处理该订单。");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 6. 关闭 Redisson 客户端
            redisson.shutdown();
        }
    }
}

3. 代码核心逻辑解析

  • 看门狗 (Watch Dog)
    lock.tryLock(10, -1, TimeUnit.SECONDS) 中,第二个参数传 -1。这意味着 Redisson 会启动看门狗,每 10 秒(默认 lockWatchdogTimeout 的 1/3)检查一次,如果业务没跑完,自动给锁续期。这解决了业务执行时间不确定的问题。

  • 重入性 (Reentrancy)
    Redisson 的锁支持重入,这意味着同一个线程可以多次调用 lock() 而不会死锁,Redis 内部会通过 Hash 结构维护一个计数器。

  • 释放安全
    lock.unlock() 内部封装了 Lua 脚本,会自动判断当前线程是否是锁的持有者,避免了“误删他人锁”的尴尬。

4. 生产环境建议

如果是 Spring Boot 项目,建议直接使用 redisson-spring-boot-starter,通过 application.yml 进行配置,并使用 @Autowired 注入 RedissonClient 即可,代码会更加简洁。

时钟回拨

在分布式锁或分布式 ID 生成(如 Snowflake 算法)的场景中,时钟回拨(Clock Skew/Backwards) 是个非常棘手的问题。如果服务器时间突然跳回过去,可能导致已经过期的锁“死灰复燃”,或者生成重复的 ID。

针对分布式锁,处理时钟回拨通常有以下几种策略:


1. 运维层面的规避:使用 NTP 的 step vs slew

最根本的解决办法是在系统层面防止时间大幅度跳变。

  • Step 模式:发现时间不对,直接“跳”过去。这会导致时钟回拨,风险极大。
  • Slew 模式(平滑同步):发现时间不对,通过微调时钟频率,让时间慢慢“追”上或“等”一下正确时间。每次调整极小(如 0.5ms/s)。
  • 建议:在分布式环境下,配置 NTP 服务使用 -x 参数(slew mode),禁止大幅度的时间跳变。

2. 算法层面的容错(以 Redisson 为例)

成熟的 Redis 客户端(如 Redisson)在实现分布式锁时,并不完全依赖本地系统时间来判断锁是否到期。

  • 内部计时器(Relative Time):Redisson 在处理 TTL(过期时间)时,使用的是 System.nanoTime() 这种单调递增的时间戳,而不是 System.currentTimeMillis()
  • 原理nanoTime 返回的是从某个固定但任意的时间点开始的纳秒数,它不受系统时钟调整的影响。即便你手动把系统时间调回 1 小时前,nanoTime 依然会持续增加。

3. Redlock 算法中的时钟处理

如果你使用了 Redis 官方推荐的 Redlock 算法,时钟回拨会导致其安全性失效。比如:

  1. 客户端 A 在 N1, N2, N3 三个节点加锁成功。
  2. N1 节点发生时钟回拨,导致锁提前过期。
  3. 客户端 B 再次在 N1 以及之前的 N4, N5 加锁成功。

解决方案:

  • 延迟重启:如果一台 Redis 服务器发生了时钟跳变或宕机,不应立即投入使用,而应该等待一个大于“最大锁有效期”的时间后再加入集群。这样可以保证该节点上旧的锁肯定已经全部自然过期。

4. 业务层的“时钟感知”检查

如果你是手写分布式锁或使用 Snowflake ID,可以记录上一次操作的时间戳:

long lastTimestamp = ...;
long currentTimestamp = System.currentTimeMillis();

if (currentTimestamp < lastTimestamp) {
    // 发现时钟回拨
    long offset = lastTimestamp - currentTimestamp;
    if (offset <= 5) {
        // 1. 如果回拨时间很短(如 5ms),可以尝试等待
        Thread.sleep(offset << 1);
        currentTimestamp = System.currentTimeMillis();
    } else {
        // 2. 如果回拨时间过长,直接抛出异常,拒绝服务
        throw new RuntimeException("Clock moved backwards. Refusing to generate ID/Lock.");
    }
}

总结

  • 最推荐:运维层面强制开启 NTP slew mode,且代码框架选用 Redisson(它通过单调时间解决了大部分本地时钟回拨问题)。
  • 最严苛:如果是金融级场景,配合延迟重启策略回拨异常监控

RedLock 实现原理

Redlock(红锁)算法由 Redis 的作者 Antirez 提出,旨在解决单机 Redis 或主从架构在发生 Failover(故障转移)时可能导致的分布式锁失效问题。其核心思想是:让客户端向多个独立的 Redis 节点依次请求加锁,只有在超过半数节点成功且耗时在有效范围内时,才认为最终获取锁成功。


1. 红锁算法的前提条件

  • 独立节点:必须部署 \(N\) 个(通常 \(N=5\))完全独立的 Redis 实例。这些实例之间不需要主从复制,也不需要哨兵集群,它们是互不通信的孤岛。
  • 时钟漂移:算法假设不同机器间的时钟漂移在可控范围内(通常远小于锁的过期时间)。

2. 红锁实现的 5 大步骤

假设我们有 5 个独立的 Redis 实例,客户端获取锁的具体流程如下:

  1. 获取当前时间戳
    客户端记录当前的毫秒级时间戳 \(T_1\)
  2. 依次尝试加锁
    客户端按顺序向 5 个实例发起加锁请求。
    • 使用相同的 key 和唯一的 value(如 UUID)。
    • 设置超时时间:为了防止某个 Redis 节点宕机导致客户端长时间阻塞,向每个实例请求锁的超时时间应该远小于锁的有效时间(例如锁过期时间 10s,请求超时可设为 5-50ms)。
  3. 计算加锁耗时与成功数
    当尝试完所有节点后,客户端记录当前时间 \(T_2\)
    • 总耗时\(\Delta T = T_2 - T_1\)
    • 成功数:记录有多少个节点返回加锁成功。
  4. 有效性判断
    只有满足以下两个条件,才认为最终获取锁成功
    • 条件 A:客户端成功获取了大多数节点的锁(即 \(\ge N/2 + 1\),5 个节点需成功 3 个)。
    • 条件 B:总耗时 \(\Delta T\) 小于锁的有效时间(TTL)。
  5. 计算锁的最终有效时间
    如果获取锁成功,锁的实际存活时间需要扣除获取锁时的开销。即:

    \[\text{Final\_TTL} = \text{Initial\_TTL} - \Delta T - \text{Clock\_Drift} \]

    (其中 Clock_Drift 是为了补偿系统时钟误差而预留的补偿值)。

失败回滚:如果获取锁失败(节点数不足或时间超时),客户端必须立即向所有实例(包括加锁失败的实例)发送释放锁的 Lua 脚本,以防因网络波动导致的“残余锁”占用资源。


3. 红锁解决的核心问题:脑裂与同步延迟

在普通的主从架构中,如果 Master 刚写完锁还没同步给 Slave 就挂了,Slave 变成新 Master 后,锁就丢失了。
Redlock 的逻辑是:即使 5 个节点里挂了 1-2 个,只要剩下 3 个节点还能正常工作且持有锁信息,其他客户端就无法通过“过半数”校验,从而保证了锁的安全性 (Safety)


4. 业界争议与深度思考

著名的分布式系统专家 Martin Kleppmann 曾发文质疑过 Redlock 的安全性,主要集中在以下几点:

  • 时钟跳变风险:如果其中一台机器发生了大幅度的时钟跳变(比如手动调快了 1 分钟),会导致该节点上的锁提前失效。
    • 对策:运维层面需严禁 ntpdate 强制同步,必须使用平滑步进的 slew 模式。
  • 进程停顿(GC Pause):客户端获取锁成功后,如果发生长时间的 Full GC 导致进程卡死,等恢复时锁可能已过期,但客户端仍认为自己持有锁。
    • 对策:业务逻辑中引入 Fencing Token(击剑令牌),每次写入数据库时携带一个递增的序列号,数据库端校验序号的合法性。
  • 性能开销:相比单机锁,Redlock 需要多次网络往返(RTT),吞吐量会有所下降。

5. Java 中如何快速实现?

不需要手动写循环,Redisson 已经封装好了 RedissonRedLock

RLock lock1 = redissonInstance1.getLock("lock");
RLock lock2 = redissonInstance2.getLock("lock");
RLock lock3 = redissonInstance3.getLock("lock");

// 创建红锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    // 尝试加锁,最多等待 100 秒,加锁后 10 秒自动解锁
    boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
        // 成功获取锁
    }
} finally {
    redLock.unlock();
}

总结:Redlock 是一种“以空间换安全”的策略。如果你的业务场景对并发安全性要求极高(如金融结算),建议使用 Redlock 或直接转向 Zookeeper/Etc-d 这种基于强一致性协议(CP)的实现。如果是普通高并发场景(如秒杀),单机 Redis 配合 Watch Dog 往往已经足够。


Fencing Token(击剑令牌) 是一种用于解决分布式锁在“长进程停顿(如 Full GC、网络丢包)”导致锁过期后,保护共享资源(如数据库)不被旧请求错误修改的机制。

其核心原理非常简单:全局递增。 每次获取锁时,从服务端(或全局计数器)拿到一个递增的 ID,数据库在更新时检查该 ID 是否比当前记录的 ID 大。


1. 数据库表设计

首先,你的业务表需要一个字段来记录“最后一次操作成功的 Token”。

CREATE TABLE order_info (
    order_id BIGINT PRIMARY KEY,
    status VARCHAR(20),
    -- 记录最后一次成功修改该行记录的 Token 
    last_fencing_token BIGINT DEFAULT 0
);

2. Java 示例代码

这里结合 Redisson 获取锁,并模拟一个递增的 Fencing Token

public class FencingTokenService {

    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private StringRedisTemplate redisTemplate; // 用于生成全局递增 Token

    @Autowired
    private OrderMapper orderMapper;

    public void processOrder(Long orderId) {
        String lockKey = "lock:order:" + orderId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 1. 获取锁
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                try {
                    // 2. 获取全局递增的 Fencing Token (可以使用 Redis 的 INCR)
                    Long currentToken = redisTemplate.opsForValue().increment("fencing:token:generator");

                    // 执行业务逻辑...
                    System.out.println("获取锁成功,Token 为: " + currentToken);
                    
                    // 3. 模拟长停顿 (例如 GC),导致锁在 Redis 端过期
                    // Thread.sleep(40000); 

                    // 4. 写入数据库时,利用 SQL 检查 Token 有效性
                    boolean success = updateOrderWithFencing(orderId, "PAID", currentToken);
                    
                    if (!success) {
                        // 说明在你停顿期间,已经有 Token 更大的请求完成了修改
                        System.err.println("拒绝写入:发现更新的 Token,当前请求已失效!");
                    }
                } finally {
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private boolean updateOrderWithFencing(Long orderId, String status, Long token) {
        /**
         * 关键 SQL 逻辑:
         * UPDATE order_info 
         * SET status = #{status}, last_fencing_token = #{token} 
         * WHERE order_id = #{orderId} 
         * AND last_fencing_token < #{token}  <-- 核心:只接受更大的 Token
         */
        int rows = orderMapper.updateWithToken(orderId, status, token);
        return rows > 0;
    }
}

3. 原理解析图

  1. 客户端 1 获取锁,拿到 Token: 1。随后发生 Full GC,进程卡死。
  2. Redis 发现锁过期,将其释放。
  3. 客户端 2 获取锁,拿到 Token: 2。执行业务,将数据库中的 last_fencing_token 更新为 2,并释放锁。
  4. 客户端 1 恢复运行,尝试更新数据库。它携带的是 Token: 1
  5. 数据库校验:由于 WHERE last_fencing_token < 1 不成立(当前是 2),更新失败。

4. 为什么不直接用数据库乐观锁(Version)?

其实 Fencing Token 在实现上和“版本号乐观锁”非常相似,但它们关注的维度不同:

  • 乐观锁 (Version):关注数据的一致性,防止并发冲突。
  • Fencing Token:关注锁的有效性。在分布式系统中,它解决了“即便我拿到了锁,但我怎么确定我现在的操作依然是合法的”这一哲学问题。

5. 注意事项

  • Token 的生成:必须是全局单调递增的。可以使用 Redis 的 INCR,也可以使用 Zookeeper 的顺序节点。
  • 外部系统:如果你的业务逻辑是发邮件、调用第三方支付,这种“不可回滚”的操作,Fencing Token 无法从物理上阻止(发出去的邮件撤不回),它主要保护的是持久化存储(DB/文件系统)

这种机制通常只在对数据准确性要求极高(如银行转账、库存结算)的场景下使用。