Redis中的单机锁到分布式锁

时间:2024年08月21日

作者:小蒋聊技术

邮箱:wei_wei10@163.com

微信:wei_wei10

音频地址:https://xima.tv/1_yMOS63?_sonic=0

希望大家帮个忙!如果大家有工作机会,希望帮小蒋内推一下,小蒋希望遇到一个认真做事的团队,一起努力。需要简历可以加我微信。

大家好,欢迎来到小蒋聊技术,小蒋准备和大家一起聊聊技术的那些事。

今天小蒋准备和大家一起聊的这个技术就厉害了!那就是Redis中的锁!

我是小蒋。今天我们继续上次的主题,但这次我们要把焦点从Java的锁转向Redis的锁,特别是从单机锁到分布式锁的演变。Redis锁不仅在分布式系统中扮演了至关重要的角色,而且它的底层逻辑和实现机制也是技术人绕不开的话题。

一、Redis单机锁:从简单到复杂的第一步

        1.Redis单机锁的概念

我们先从最简单的场景说起:Redis的单机锁。假设你是一位图书管理员,负责给图书馆中的每本书上锁,确保在你登记的时候,没人能借走这本书。Redis单机锁的原理其实和这个场景类似:一个人执行了某个操作后,其他人就不能再操作同一个资源,直到这个锁被释放。

        2.SETNX命令:单机锁的核心

在Redis中,SETNX命令是实现单机锁的关键。SETNX全称是“SET if Not Exists”,意思是“如果这个键不存在,就设置这个键”,相当于你给图书馆的某本书加了一个锁。

        3.代码实现

实现Redis单机锁的代码非常简单:


String lockKey = "resource_lock";
boolean locked = jedis.setnx(lockKey, "value") == 1;
if (locked) {
    // 成功获取锁,进行业务操作
}

如果你成功获取到这个锁,你就可以进行你的操作了。否则,你得等到别人释放了锁之后才能继续操作。

补充

Jedis 是 Redis 官方推荐的Java客户端开发包,后来SpringBoot对Redis客户端进行了封装使用了 redisTemplate,springBoot1.x底层使用的还是Jedis,而到了springBoot2.x时,具体实现变成了lettuce。redisTemplate的好处就是基于springBoot自动装配的原理,使得整合redis时比较简单。但单从执行效率上来讲,jedis完爆redisTemplate。有人测试过jedis效率要10-30倍的高于redisTemplate的执行效率。

        4.自动过期的必要性

不过,锁不能一直占用资源,所以我们通常会为锁设置一个过期时间,避免死锁的发生:

jedis.expire(lockKey, 10); // 10秒后自动释放锁

这样,即使某个客户端崩溃了,锁也会在10秒后自动释放。

小贴士

在使用Redis单机锁时,要小心锁过期时间的设置。如果设得太短,可能锁在业务逻辑还没执行完就自动释放了,导致别的客户端误以为锁已释放。如果设得太长,则可能会导致资源长时间被占用。

二、分布式锁:协调多个节点的难题

        1.为什么需要分布式锁?

在单机场景下,SETNX命令足够好用,但当你的应用规模扩大到多个节点时,单机锁的局限性就暴露出来了。想象一下,你的图书馆不再是一栋楼,而是扩展到全市范围。你如何确保多个图书馆中的同一本书不会被重复借出呢?这时候,就需要Redis的分布式锁登场了。Redis分布式锁来保证在多台机器之间实现互斥。

        2.Redis分布式锁的实现方式

Redis的分布式锁是通过SET命令配合一些条件参数来实现的。使用SET命令时,你可以同时设置一个键的值和过期时间,并且指定只有在键不存在的情况下才进行设置。

String lockKey = "distributed_lock";
String lockValue = UUID.randomUUID().toString();
boolean locked = jedis.set(lockKey, lockValue, SetParams.setParams().nx().px(10000));
if (locked) {
    // 成功获取锁,执行相关操作
} else {
    // 获取锁失败,稍后重试
}

在这个场景下,NX参数保证了只有当锁不存在时才能设置,而PX参数则是锁的过期时间,防止长时间占用。

        3.释放锁:保持锁的唯一性

拿到锁之后,你肯定想要最终释放它。释放锁时,必须确保只有拥有锁的那个客户端才能释放它,否则可能会误释放别人的锁。

Copy code
if (lockValue.equals(jedis.get(lockKey))) {
    jedis.del(lockKey);
}

这里的逻辑是:只有当当前持锁的客户端仍然持有锁的唯一标识时,才允许释放锁。

三、Redlock算法:分布式锁的进阶版(已废弃)

        1.Redlock算法简介

Redlock算法是由Redis的创始人Antirez提出的,用于解决分布式环境中锁的一致性问题。它的核心思想是在多个Redis实例中分别尝试加锁,如果在大多数实例上成功加锁,则认为整体加锁成功。

        2.实现步骤

Redlock的步骤如下:

  • 在5个(或更多)独立的Redis实例上尝试获取锁。
  • 设置一个唯一的标识和过期时间,确保每个实例的锁都是独立的。
  • 如果在大多数实例上成功获取到锁(例如5个中的至少3个),则认为整体加锁成功。
  • 如果获取锁失败或锁已过期,则释放所有实例上的锁。
boolean acquireLock(List<Jedis> instances, String lockKey, String lockValue, long ttl) {
    int quorum = instances.size() / 2 + 1;
    int acquired = 0;

    for (Jedis instance : instances) {
        if (instance.set(lockKey, lockValue, SetParams.setParams().nx().px(ttl))) {
            acquired++;
        }
    }

    if (acquired >= quorum) {
        return true;
    } else {
        instances.forEach(instance -> instance.del(lockKey));
        return false;
    }
}

        3.Redlock的优缺点

RedLock 会对集群的每个节点进行加锁,如果大多数(N/2+1)加锁成功了,则认为获取锁成功。曾经Redlock算法被广泛推荐,然而,随着时间推移,社区发现Redlock并不总是能保证强一致性,尤其是在网络分区、时钟漂移等复杂环境下,它的可靠性会受到质疑。

简而言之,Redlock在某些极端情况下会出现锁的多重持有问题,即多个客户端都认为自己持有锁,这就破坏了锁的互斥性。因此,Redlock逐渐被认为是不适合强一致性要求的场景

四、更优的分布式锁实现方式

在实践中,结合Redis的Watch机制和Lua脚本,我们可以实现一种更加稳健的分布式锁。这种方法避免了Redlock的弊端,同时利用Lua脚本的原子性,确保锁的获取和释放操作是不可分割的。

-- Lua脚本实现分布式锁的获取
local key = KEYS[1]
local value = ARGV[1]
local ttl = ARGV[2]

if redis.call("EXISTS", key) == 0 then
    return redis.call("SET", key, value, "PX", ttl)
else
    return nil
end
String lockKey = "lock:myLock";
String lockValue = UUID.randomUUID().toString();
String luaScript = "..."; // 上面的Lua脚本内容

Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), 
                           Arrays.asList(lockValue, "30000"));

if ("OK".equals(result)) {
    try {
        // 执行业务逻辑
    } finally {
        // 释放锁
        jedis.del(lockKey);
    }
} else {
    // 获取锁失败
}

这个方案不仅解决了锁的多重持有问题,还可以通过Lua脚本的原子性保证锁的安全性。

对于Spring开发者来说,将上述Lua脚本集成到Spring项目中可能有些繁琐。幸运的是,我们有现成的库可以帮助实现,比如 Redisson 或者更底层的 Spring Data Redis。

Redisson 是一个非常强大的Redis客户端,提供了很多高级功能,其中就包括分布式锁。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.4</version>
</dependency>

配置RedissonClient:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

使用分布式锁:

@Service
public class MyService {

    @Autowired
    private RedissonClient redissonClient;

    public void execute() {
        RLock lock = redissonClient.getLock("myLock");
        lock.lock();
        try {
            // 执行业务逻辑
        } finally {
            lock.unlock();
        }
    }
}

Redisson的优势在于它已经为我们封装了很多复杂的细节,确保了锁的稳定性和高可用性。

六、总结:Redis锁的价值

今天我们从Redis的单机锁讲到分布式锁,分析了从Redlock到Lua脚本的演进,最后结合Spring的最佳实践,推荐了Redisson的使用。整个过程,我们注重实用性和可靠性,摒弃了过时的方案,力求给大家提供最优的解决方案。

锁的概念看似简单,实际上却有着丰富的内涵和多样的实现方式。希望通过这次分享,大家能够更好地理解Redis中的锁机制,并在实际开发中灵活运用,写出更加高效、安全的代码。

今天小蒋先分享到这,我们下次再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小蒋聊技术

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

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

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

打赏作者

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

抵扣说明:

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

余额充值