千家信息网

Redis分布式锁有什么用

发表于:2024-11-22 作者:千家信息网编辑
千家信息网最后更新 2024年11月22日,这篇文章给大家分享的是有关Redis分布式锁有什么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。1. 什么是分布式锁分布式与单机情况下最大的不同在于其不是多线程而是多进程
千家信息网最后更新 2024年11月22日Redis分布式锁有什么用

这篇文章给大家分享的是有关Redis分布式锁有什么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

1. 什么是分布式锁

分布式与单机情况下最大的不同在于其不是多线程而是多进程,而数据只有一份(或有限制),也就是说单机的共享内存已解决不了一致性写问题,此时需要利用锁的技术控制某一时刻修改数据的进程数。

当在分布式模型下,分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存(Redis、Memcache)。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

2. 分布式锁该具备的特性

  • 最好是可重入锁(避免死锁)

  • 最好是一把阻塞锁(根据业务需求决定)

  • 最好是一把公平锁(根据业务需求决定)

  • 有高可用、高性能的获取锁和释放锁功能

3. 基于数据库做分布式锁

  • 基于乐观锁,CAS,但如果是insert的情况采用主键冲突防重,在大并发情况下有可能会造成锁表现象

  • 基于悲观锁,也就是排他锁,会有各种各样的问题(操作数据库需要一定的开销,使用数据库的行级锁并不一定靠谱,性能不靠谱)

如果按分布式该具备的特性来逐条匹配,特别是高可用(存在单点)、高性能是硬伤

4. 基于Redis做分布式锁

一般都使用 setnx(set if not exists) 指令,只允许被一个客户端占有,先来先得, 用完后再通过 del 指令释放。

如果中间逻辑执行时发生异常,可能会导致 del 指令没有被执行,这样就会陷入死锁,怎么破?

对,给锁加个过期时间(即使出现异常也可以保证几秒之后锁会自动释放)!

但setnx 和 expire 之间redis服务器突然挂掉,怎么破?

其实该问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。为了解决这个疑难,Redis 开源社区涌现了一堆分布式锁的 解决方案。为了治理这个乱象,Redis 2.8 版本中加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。

总之,setnx 和 expire 组合就是分布式锁的奥义所在。

4.1 超时问题

如果在加锁和释放锁之间的逻辑执行的太长,超出了超时限制,怎么破?

也就是说第一个线程持有的锁过期了但临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致每个请求执行临界区代码时不能严格的串行执行。

Redis 的分布式锁不能解决超时问题,建议分布式锁不要用于较长时间的任务。

稍微安全一点的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,一致的话再删除 key,这是可以确保当前线程占有的锁不会被其它线程释放,但是并不能解决锁被redis服务器自动释放的。

int tag = random.nextint()//随机数boolean nx=true;int ex=5;if(redis.set(key, tag, nx, ex)){    do_something()    redis.delifequals(key, tag)//不存在这样的命令}

但是匹配 value 和删除 key 不是一个原子操作,怎么破?

需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

#delifequals.lua文件,下面的是社区热门代码if redis.call('get', KEYS[1]) == ARGV[1] then    return redis.call('del', KEYS[1])else    return 0end//java调用public void delifequals(){    String script = readScript("delifequals.lua");    int tag = 5;    String key = "key";    Object eval = jedis.eval(script, Lists.newArrayList(key), Lists.newArrayList(tag));    System.out.println(eval);}

4.2 可重入锁

redis有类似Java 语言里有个 ReentrantLock 就是可重入锁吗?

要支持可重入,需要对jedis 的 set 方法进行包装,思路是:使用 Threadlocal 存储当前持有锁的计数。可重入锁加重了客户端的复杂性,精确一点还需要考虑内存锁计数的过期时间,代码复杂度将会继续升高。

public class JedisWithReentrantLock {    private Jedis jedis;    /**     * 当前线程的锁及计数     */    private ThreadLocal> lockers = new ThreadLocal<>();    public JedisWithReentrantLock(Jedis jedis) {        this.jedis = jedis;    }    private boolean set(String key) {        return jedis.set(key, "", "nx", "ex", 5L) != null;    }    private void del(String key) {        jedis.del(key);    }    private Map getLockers() {        Map refs = lockers.get();        if (refs != null) {            return refs;        }        lockers.set(Maps.newHashMap());        return lockers.get();    }     public boolean lock(String key) {        Map refs = getLockers();        Integer refCount = refs.get(key);        if (refCount != null) {            refs.put(key, refCount + 1);            return true;        }        if (!this.set(key)) {            return false;        }        refs.put(key, 1);        return true;    }     public boolean unlock(String key) {        Map refs = getLockers();        Integer refCount = refs.get(key);        if (refCount == null) {            return false;        }        refCount -= 1;        if (refCount > 0) {            refs.put(key, refCount);        } else {            refs.remove(key);            this.del(key);        }        return true;    }}    @Test    public void runJedisWithReentrantLock() {        JedisWithReentrantLock redis = new JedisWithReentrantLock(jedis);        System.out.println(redis.lock("alex"));        System.out.println(redis.lock("alex"));        System.out.println(redis.unlock("alex"));        System.out.println(redis.unlock("alex"));    }

4.3 集群环境的缺陷

在集群环境下,这种方式是有缺陷的(数据不一致的情况)。比如在 Sentinel 集群中,主节点挂掉时(原先第一个客户端在主节点中申请成功了一把锁),从节点A 会取而代之并晋升为主(但是这把锁还没有来得及同步),虽然客户端上却并没有明显感知,但是这时另一个客户端过来请求 从节点A 可以成功加锁,这样就会导致系统中同样一把锁被两个客户端同时持有。

主从发生故障转移,一般持续时间极短,数据不一致的情况基本上都是小概率事件。

4.4 Redlock

上面的集群同步问题导致的缺陷,难道就没有解决方案吗?

为此Antirez 发明了 Redlock 算法,它的流程比较复杂,不过已经有了很多开源的实现。

原理

使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用少数服从多数。

加锁时,它会向过半节点发送 set(key, value, nx, ex) 指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。缺陷:因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些。

注:Redlock算法还需要考虑出错重试、时钟漂移等很多细节问题

使用场景

如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,那就应该考虑 redlock。

感谢各位的阅读!关于"Redis分布式锁有什么用"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

0