如何实现分布式锁?

转载请注明:http://blog.youkuaiyun.com/HEL_WOR/article/details/51661591

从本质上说。
单机锁,锁本地的一个共享资源。
分布式锁,锁一个所有应用访问到的共享资源。

不管是redis锁,还是zookeeper锁,锁的都是共享资源。
在redis上,通过判断一个key是否存在存在作为锁,此时这个key就是共享资源,以key是否存在作为锁的标志,表现在代码中就是setnx是否返回1.
在zookeeper上,通过判断一个node是否存在作为锁,此时这个node就是共享资源,以node是否存在作为锁的标志,表现在代码中就是是否能create这个结点而不报出结点已存在的异常。

只是在这个逻辑可能有一些优化,比如curator框架中内置的interProcessLock,其也是通过判断能否成功在指定路径上能否创建结点完成加锁。但其作为一个客户端通过一个threadlocal保存已获得锁的线程完成了锁的可重入,并且避免每次获取锁都创建一次结点,因为它是一个客户端,因此threadlocal在这里使用是本地的操作,从设计来讲,这样的实现,比将threadlocal放在zookeeper内每次传送当前线程的id完成锁的可重入会完美许多。

举个redis锁的实现:
有一个计算流量的逻辑,需要每隔一段时间将单机中的流量缓存同步到Redis库中,现在就出现了这个问题,一个服务被部署到多服务器上,当需要同时写同一台redis数据库,就需要使用分布式锁。

这里使用了Redis来实现分布式锁,实现的逻辑类似单机中使用的Lock或者synchronized,由于每个对象都拥有一个锁,只有获得了对象锁的线程才能对对象进行操作(在synchronized中使用了锁计数来完成,即通过判断是否非0来表示是否上锁)。
这里把锁对象变成锁KV对。

如果redis库中有特定的KV对,就表示上锁。
如果redis库中没有特定的KV对,就表示未上锁。
锁使用完毕后需要删除redis中的特定KV对。

实现代码:

/**
 * 利用redis实现分布式锁
 *
 * @author zhenghao:
 * @version 1.0 2016-6-7
 */
@Component
public class RedLock {

    private static final Log LOG = LogFactory.getLog(RedLock.class);
    private static final String REDLOCK_KEY = "RedLock";
    private static final Long REDLOCK_EXPIRES = 5L;
    private static final int LOCK_TIMES_LIMIT = 5;
    private static volatile boolean locked = false;
    private static String redLockValue = String.valueOf(Math.random());
    private static AtomicInteger lockTimes = new AtomicInteger(0);

    @Autowired
    private CacheService cacheService;

    /**
     * 锁状态
     */
    public boolean isLocked(){
        return locked;
    }

    /**
     * 加锁
     * 通过Redis的setNx函数返回值判断加锁是否成功(1:成功, 0:失败)。
     * 如若不成功,说明锁已被占用,休眠REDLOCK_EXPIRES后重新获取锁
     * 加入对获取锁操作技术,超过一定范围说明锁竞争激烈,需要调整,否则会比较明显的降低程序的性能
     */
    public void lock() {
        while (cacheService.setRedLock(REDLOCK_KEY, redLockValue, REDLOCK_EXPIRES) == 0) {
            try {
                if (lockTimes.incrementAndGet() > LOCK_TIMES_LIMIT) {
                    LOG.info("SYSTEM_LOCK_CONTENTION: 锁竞争激烈");
                }

                Thread.sleep(REDLOCK_EXPIRES * 1000);
            } catch (InterruptedException e) {
                LOG.info("SYSTEM_INTERRUPTED_EXCEPTION: 内部异常中断错误");
            }
        }

        locked = true;
    }

    /**
     * 解除Redis锁
     * 判断逻辑加入的原因在于:考虑一种情况,单机A获得了Redis锁,但A由于处理超时5秒后KEY超时,此条记录已被Redis移除,单机A被迫释放锁。
     * 此时单机B进入已获得了Redis锁。
     * 如若单机A中调用了Unlock逻辑,执行了removeRedLock操作,则会删除单机B正常获取到的锁。
     * 判断单机B是否正常获取到了锁,通过比对保存的随机数完成。
     * isLocked不置为false仅当A超时B拿到锁的情况下.
     */
    public void unlock() {
        String oldRedLockValue = cacheService.getRedLock(REDLOCK_KEY);
        if (oldRedLockValue == null) {
            locked = false;
        } else if (oldRedLockValue.equals(redLockValue)) {
            cacheService.removeRedLock(REDLOCK_KEY);
            locked = false;
        }

        lockTimes.set(0);
    }
}

update:
1.基于setnx函数实现的redis锁属于悲观锁,线程抢不抢得到锁只能看运气,在做了休眠操作可以减缓锁竞争的冲突,但当此应用横向扩展部署十多台以应对活动带来的高并发,线程用在在获取锁的时间将大大拉长,先不说是否会有线程一直拿不到锁而饿死,获取锁的时间将大大拉长导致平均响应时间的拉长,根据吞吐量的计算公式,系统的吞吐量将降低,原本横向扩展带来的吞吐量提升因为锁的争抢,提升效果将下降。
如果因为响应时间的拉长影响到了用户,想想我们平时上网网页开的慢或者半天打不开会干嘛,Ctrl+F5,更多的重复请求到达。更多的线程开启,更多的内存占用,更多的上下文切换时间,要么连接耗尽,要么系统拖死,挂了一台,剩下的就是雪崩效应。

2.使用消息队列,强行将原本的多线程变成单线程,再多线程监听消息。如果处理速度跟不上消费速度,内存会被吃掉,最后吃完。另外还要确保发送消息时不要出现诸如EOF等的错误,导致用户的请求并没有进入消息队列。

3.使用乐观锁,redis自带watch。消耗cpu。

具体场景使用具体的方法。

<think>首先,用户的问题是关于Redis实现分布式锁的步骤和方法。我需要基于系统级指令来构建我的回答。 系统级指令关键点: - 所有行内数学表达式用$...$格式(但这里可能不涉及数学,所以暂时忽略)。 - 独立公式用$$...$$并单独成段(同样,可能不适用)。 - LaTeX语法正确(确保任何数学部分正确)。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾自然地添加引用标识,例如 [^1]。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考的引用: - [^1]: Redis 主从一致性问题,使用 Redlock 或 Redis 集群模式。 - [^2]: Redis 提供简单分布式锁,但 Redlock 或 Redisson 提高健壮性。 - [^3]: Redisson 使用看门狗机制延长锁生存时间。 - [^4]: 释放锁时使用 Lua 脚本确保原子性。 用户问题:"我想在Redis实现分布式锁的方法 请问Redis 实现分布式锁的步骤和方法" 我的回答需要: 1. 解释分布式锁的基本概念。 2. 详细步骤和方法,包括如何获取锁、释放锁、处理过期时间等。 3. 参考引用中的关键点,如原子性、主从问题、Redisson等。 4. 结构清晰,逐步解释。 5. 最后生成相关问题。 大纲回答: - 引言:简要介绍分布式锁Redis中的作用。 - 步骤: 1. 获取锁:使用SET命令,设置键值对,确保原子性(如使用SETNX或SET with NX PX)。 2. 设置过期时间:避免死锁。 3. 释放锁:使用Lua脚本确保只释放自己的锁。 4. 处理潜在问题:如主从一致性,建议使用Redlock或Redisson。 - 示例代码:基于引用[^4]提供Java代码片段。 - 最佳实践:建议使用Redisson或高级方案。 - 生成相关问题。 确保语言中文。 生成相关问题: - 基于主题,如:如何优化Redis分布式锁?如何处理锁续期?Redisson的实现细节?分布式锁的应用场景? 引用标识: - 在相关段落末尾添加 [^number],例如,提到主从问题时添加 [^1]。 完整回答结构: 1. 介绍分布式锁。 2. 步骤分解。 3. 代码示例。 4. 潜在问题及解决方案。 5. 总结。 6. 相关问题。</think>### Redis 实现分布式锁的步骤和方法 在分布式系统中,分布式锁是确保多个进程或服务对共享资源进行互斥访问的关键机制。Redis 凭借其高性能和原子操作,成为实现分布式锁的常用选择。下面我将逐步解释 Redis 实现分布式锁的核心步骤和方法,确保过程清晰、可靠。实现分布式锁的核心目标是保证原子性、安全性和可靠性,避免死锁或锁冲突[^1]。 #### 步骤 1: 获取锁 获取锁的目的是确保只有一个客户端能成功持有锁。在 Redis 中,推荐使用 `SET` 命令结合 `NX`(Not eXists)和 `PX`(毫秒级过期时间)参数来实现原子操作: - **命令格式**: `SET lock_key unique_value NX PX expire_time` - `lock_key`: 锁的键名,例如 `resource_lock`。 - `unique_value`: 唯一标识符(如 UUID 或客户端 ID),用于区分不同客户端的锁,防止误删[^4]。 - `expire_time`: 锁的过期时间(毫秒),例如 `30000`(30 秒),避免客户端崩溃导致死锁。 - **原子性保证**: `NX` 确保只有键不存在时才能设置成功,`PX` 自动设置过期时间,避免两步操作(如先 SETNX 再 EXPIRE)的非原子性问题。 - **返回值**: 如果设置成功,Redis 返回 `OK`;否则返回 `nil`,表示锁已被占用。 例如,在命令行中: ```bash SET resource_lock "client_123" NX PX 30000 ``` 如果返回 `OK`,表示锁获取成功;否则需重试或等待。 #### 步骤 2: 设置锁的过期时间 锁必须设置过期时间,以防止客户端故障时锁永久持有(死锁)。在步骤 1 中,`PX` 参数已集成过期时间设置,无需额外命令。建议: - **过期时间选择**: 根据业务逻辑调整,通常 10-30 秒。如果业务执行时间较长,需结合锁续期机制(如 Redisson 的看门狗)[^3]。 - **注意事项**: 避免在获取锁后单独调用 `EXPIRE`,因为这可能因网络问题导致非原子操作。 #### 步骤 3: 释放锁 释放锁时,必须确保只释放自己持有的锁,防止其他客户端的锁被误删。使用 Lua 脚本实现原子操作: - **Lua 脚本逻辑**: 先检查锁值是否匹配 `unique_value`,再删除键。 - **原子性保证**: Lua 脚本在 Redis 中单线程执行,确保检查和删除的原子性[^4]。 示例代码(Java 中使用 Spring Boot): ```java import org.springframework.data.redis.core.script.DefaultRedisScript; public boolean unlock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(script); redisScript.setResultType(Long.class); return redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId) == 1; } ``` 此代码中,`requestId` 是步骤 1 中的 `unique_value`。如果匹配,删除锁并返回 1(成功);否则返回 0(失败)[^4]。 #### 步骤 4: 处理潜在问题 Redis 分布式锁在简单场景下有效,但在高可用环境中需考虑以下问题: - **主从一致性问题**: Redis 主从复制是异步的,如果主节点故障,从节点可能未同步锁数据,导致锁丢失。解决方案: - 使用 **Redlock 算法**(Redis 官方推荐),通过多个独立 Redis 节点投票获取锁,提高健壮性[^1][^2]。 - 切换到 **Redis 集群模式**,但需注意集群的复杂性。 - **锁续期问题**: 如果业务执行时间超过锁过期时间,需自动延长锁。推荐使用 **Redisson** 库,它内置看门狗机制(后台线程每 10 秒检查并续期锁)[^3]。 - **网络延迟和重试**: 在锁获取失败时,实现指数退避重试机制,避免频繁请求。 #### 最佳实践和工具推荐 - **简单场景**: 直接使用 Redis `SET` 命令 + Lua 脚本,适用于单节点或低并发环境。 - **高可用场景**: 优先使用 **Redisson**(Java 客户端),它封装了 Redlock、看门狗和锁续期,简化实现[^3][^2]。或采用 **ZooKeeper** 等替代方案。 - **性能调优**: 监控锁竞争和过期时间,避免锁等待导致性能瓶颈。 - **可靠性**: 在关键业务中,结合日志和监控,确保锁操作可追溯。 通过以上步骤,Redis 分布式锁能有效协调分布式资源。但需根据业务需求选择方案:简单锁用原生 Redis,复杂场景用 Redisson 或 Redlock[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值