分布式锁总结2 - redis实现分布式锁并解决常见问题

目录

1. redis分布式锁

1.1基本原理图示如下

1.2 Redis通过一个lock变量实现一个最简单的分布式锁实现代码:

2 升级简单分布式锁(实现原子加锁与安全删锁)

2.1 但1中的简单分布式锁存在几个问题:

2.1.1 问题1. 如果加完锁执行业务过程中,还未到释放锁的环节,系统中断,出现死锁的情况,怎么解决?

2.2.2 问题2. 业务未完成,锁到期自动释放,但把别人正在持有的锁删除了,应该如何解决(如下图)?

2.2 针对以上问题我们将上述分布式锁代码升级为如下代码:

3. 继续升级分布式锁(实现原子删锁)

3.1 此分布式锁解决了上述的问题1与问题2,但释放锁还存在问题

3.1.1 问题3. 在线程A释放锁时,因是先发送请求获取到锁A的value,对比成功后,删除。若线程A在对比成功以后,删除锁之前,锁刚好自动过期,而另一个线程B抢占到资源,刚好加上锁。则此时线程A刚好执行删除操作则会删除线程B的锁。应该如何解决(如下图)?

3.2 实现原子删除操作的代码

4. 完整实现代码

5. 总结


1. redis分布式锁

1.1基本原理图示如下

1.2 Redis通过一个lock变量实现一个最简单的分布式锁实现代码:

//查询缓存中没有想要获取的数据
//开始
String lockKey = "lock_key";
// 1 获取锁。即“占坑”操作
boolean locked = jedis.set(lockKey, "lockValue", "NX") != null;

if (locked) {
    try {
        //1.1 加锁成功
        // 1.1.1 执行业务逻辑,即查询数据库获得数据并返回
    } finally {
        // 1.1.2 释放锁
         jedis.del(lockKey);
    }
} else {
    // 1.2 未获取锁,处理逻辑
    //1.2.1 休眠100ms 为避免重试频率过高
    return //1.2.2 返回本方法
    //通过返回本方法实现自旋,不断尝试获取锁
}

2 升级简单分布式锁(实现原子加锁与安全删锁)

上述的方法可以实现一个简单的分布式锁,即所有资源抢占时所有服务器排队抢占锁。

2.1 但1中的简单分布式锁存在几个问题:

2.1.1 问题1. 如果加完锁执行业务过程中,还未到释放锁的环节,系统中断,出现死锁的情况,怎么解决?

        解决方案:设置锁自动过期时间,且必须在加锁的同时设置过期时间(必须为原子操作,分开操作也可能会导致死锁问题)。

2.2.2 问题2. 业务未完成,锁到期自动释放,但把别人正在持有的锁删除了,应该如何解决(如下图)?

        解决方案:加锁时指定value为自己的uuid,释放锁时匹配自己的uuid才能释放。

2.2 针对以上问题我们将上述分布式锁代码升级为如下代码:

//查询缓存中没有想要获取的数据
//开始
String lockKey = "lock_key";
String clientId = UUID.randomUUID().toString(); // 用于标识持有锁的客户端
int lockTimeout = 10; // 锁的过期时间,单位为秒

// 1 获取锁。即“占坑”操作(原子操作设置过期时间,解决问题1)
boolean locked = jedis.set(lockKey, clientId, "NX", "EX", lockTimeout) != null;

if (locked) {
    try {
        //1.1 加锁成功
        // 1.1.1 执行业务逻辑,即查询数据库获得数据并返回
    } finally {
        // 1.1.2 释放锁
        if (clientId.equals(jedis.get(lockKey))) {
            //只能删除自己的锁(解决问题2)
            jedis.del(lockKey);
        }
    }
} else {
    // 1.2 未获取锁,处理逻辑
    //1.2.1 休眠100ms 为避免重试频率过高
    return //1.2.2 返回本方法
    //通过返回本方法实现自旋,不断尝试获取锁
}

上述代码成功实现了原子加锁与只能删除自己锁的功能,成功解决了前面提出的两个问题。

3. 继续升级分布式锁(实现原子删锁)

3.1 此分布式锁解决了上述的问题1与问题2,但释放锁还存在问题

3.1.1 问题3. 在线程A释放锁时,因是先发送请求获取到锁A的value,对比成功后,删除。若线程A在对比成功以后,删除锁之前,锁刚好自动过期,而另一个线程B抢占到资源,刚好加上锁。则此时线程A刚好执行删除操作则会删除线程B的锁。应该如何解决(如下图)?

        解决方案:释放锁即删除锁时执行原子操作,而删除锁无法像加锁时那么简单,此处使用lua脚本实现原子操作删除锁。

// Lua 脚本,用于原子性释放锁
    private static final String releaseLockScript =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";
//上述脚本实现功能为 get查询变量KEYS[1]的值是否等于ARGV[1] ,
//如果等则del删除变量KEYS[1],否则结束脚本

        然后在代码中执行上述脚本即可完成删除锁的原子操作,因为脚本是一个整体,查询与删除完成或者失败是一体的。

3.2 实现原子删除操作的代码

    private static final String lockKey = "lock_key"; // 锁的 Redis key
    private static final String clientId = UUID.randomUUID().toString(); // 用于标识持有锁的客户端
    private static final int lockTimeout = 10; // 锁超时时间,单位:秒

    private Jedis jedis; // Redis 客户端
    // Lua 脚本,用于原子性释放锁
    private static final String releaseLockScript =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";

    public String getDataFromCacheOrDb() {
        // 1. 先查询缓存,判断缓存中是否已经有数据

        // 2. 缓存未命中,尝试获取分布式锁
        boolean locked = jedis.set(lockKey, clientId, "NX", "EX", lockTimeout) != null;
        if (locked) {
            try {
                // 再次检查缓存,避免多个线程在锁之前都没有缓存
                // 3. 缓存依然未命中,去数据库中获取数据
                // 4. 将数据写入缓存,设置一个过期时间
                return dbData;

            } finally {
                // 5. 释放锁,使用 Lua 脚本确保只有当前持有锁的客户端才能删除锁
                jedis.eval(releaseLockScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
            }
        } else {
            // 6. 如果未获取到锁,等待其他线程完成缓存操作,稍作延迟后再次尝试读取缓存
            try {
                Thread.sleep(100); // 短暂休眠,等待其他线程完成操作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            // 7. 重新从缓存读取数据
            return ;
        }
    }

4. 完整实现代码

模拟获取热点数据整个流程:

import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.UUID;

public class CacheService {

    private static final String lockKey = "lock_key"; // 锁的 Redis key
    private static final String cacheKey = "hotspot_data"; // 缓存中的 key
    private static final String clientId = UUID.randomUUID().toString(); // 用于标识持有锁的客户端
    private static final int lockTimeout = 10; // 锁超时时间,单位:秒

    private Jedis jedis; // Redis 客户端

    // Lua 脚本,用于原子性释放锁
    private static final String releaseLockScript =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";

    public String getDataFromCacheOrDb() {
        // 1. 先查询缓存,判断缓存中是否已经有数据
        String cachedData = jedis.get(cacheKey);
        if (cachedData != null) {
            return cachedData; // 缓存命中,直接返回数据
        }

        // 2. 缓存未命中,尝试获取分布式锁 实现原子加锁
        boolean locked = jedis.set(lockKey, clientId, "NX", "EX", lockTimeout) != null;
        if (locked) {
            try {
                // 再次检查缓存,避免多个线程在锁之前都没有缓存
                cachedData = jedis.get(cacheKey);
                if (cachedData != null) {
                    return cachedData; // 如果其他线程已经缓存了数据,直接返回
                }

                // 3. 缓存依然未命中,去数据库中获取数据
                String dbData = getDataFromDb(); // 从数据库中获取数据

                // 4. 将数据写入缓存,设置一个过期时间
                jedis.setex(cacheKey, 60, dbData); // 60秒缓存有效期,实际情况按需求调整
                return dbData;

            } finally {
                // 5. 释放锁,使用 Lua 脚本确保只有当前持有锁的客户端才能删除锁 实现原子删锁
                jedis.eval(releaseLockScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
            }
        } else {
            // 6. 如果未获取到锁,等待其他线程完成缓存操作,稍作延迟后再次尝试读取缓存
            try {
                Thread.sleep(100); // 短暂休眠,等待其他线程完成操作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            // 7. 重新从缓存读取数据
            return jedis.get(cacheKey);
        }
    }

    // 模拟从数据库获取数据
    private String getDataFromDb() {
        // 实际中,这里会执行数据库查询操作
        return "data_from_db";
    }
}

 经过升级后的最终分布式锁流程图如下:

5. 总结

分布式锁实现过程:

借助 SET 命令实现加分布式锁原子操作:

SET key value NX EX seconds

(NX 表示只有当键不存在时才设置值(即,保证了原子性,避免了多个客户端同时获取锁))。

(EX 设置锁的过期时间,防止锁永远不会释放(即死锁情况))。

实现步骤

1. 尝试使用 SET key value NX EX seconds 来加锁,实现原子操作

• 如果返回 OK,说明锁获取成功。

• 如果返回 nil,说明锁已经被其他客户端持有,获取失败。

2. 如果成功获取锁,执行关键业务逻辑。

3. 业务完成后,通过lua脚本释放锁,实现原子操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

One day️

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值