带你轻松掌握Redis分布式锁

目录
  • 1. 什么是分布式锁
  • 2. 分布式锁该具备的特性
  • 3. 基于数据库做分布式锁
  • 4. 基于Redis做分布式锁
    • 4.1 超时问题
    • 4.2 可重入锁
    • 4.3 集群环境的缺陷
    • 4.4 Redlock

目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。

基于 CAP理论,任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。通常大家都会采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 0
end
//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<Map<String, Integer>> 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<String, Integer> getLockers() {
        Map<String, Integer> refs = lockers.get();
        if (refs != null) {
            return refs;
        }
        lockers.set(Maps.newHashMap());
        return lockers.get();
    }

    public boolean lock(String key) {
        Map<String, Integer> 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<String, Integer> 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。

引用资料

How to do distributed locking

Redlock的实现

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot整合Redisson实现分布式锁

    目录 一.添加依赖 二.redis配置文件 三.新建配置类 四.使用分布式锁 可重入锁 读写锁 信号量(Semaphore) 闭锁(CountDownLatch) Redisson是架设在redis基础上的一个Java驻内存数据网格(In-Memory Data Grid).充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类.使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低

  • Redis分布式锁Redlock的实现

    目录 普通实现 Redlock实现 Redlock源码 用法 唯一ID 获取锁 释放锁 普通实现 说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value px milliseconds nx.后一种方式的核心实现命令如下: - 获取锁(unique_value可以是UUID等) SET resource_name unique_value NX PX 30000 - 释放锁(lua脚本中,一定要比较value,防止误解锁) if redis.call("g

  • Springboot中如何使用Redisson实现分布式锁浅析

    目录 前言 1. 概述 2. Redisson 在 Springboot 中的使用 2.1 引入依赖 2.2 在 Springboot 配置中配置Redis 2.3 Demo代码 3. 综述 前言 在分布式场景下为了保证数据最终一致性.在单进程的系统中,存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步(lock-synchronized),使其在修改这种变量时能够线性执行消除并发修改变量.但分布式系统是多部署.多进程的,开发语言提供的并发处理API在此场景下就无能为

  • redis分布式锁优化的实现

    对于单机的应用来说,可以直接使用synchronized关键字或着Lock工具类来加锁:但是对于分布式应用我们需要凭借一些工具来实现加锁: 加锁流程通俗来解释就是:         1. 占坑         2. 执行逻辑         3. 填坑 我们可以使用redis来完成占坑这个操作: 基础版加锁 //通过占坑的方式获取锁 boolean lock = redis.setIfAbsent(key, value); if (lock) { //业务逻辑 //填坑 redis.delete

  • Redis如何实现分布式锁

    目录 一.前言 二.正文 今天我们来聊一聊分布式锁的那些事. 相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行加锁操作.在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制. 比如你去相亲,发现你和一大哥同时和一个女的相亲,那怎么行呢...,搞不好还要被揍一顿. 那什么是分布式锁呢.当多个客户端需要争抢锁时,我们就需要分布式锁.这把锁不能是某个客户端本地的锁

  • 带你轻松掌握Redis分布式锁

    目录 1. 什么是分布式锁 2. 分布式锁该具备的特性 3. 基于数据库做分布式锁 4. 基于Redis做分布式锁 4.1 超时问题 4.2 可重入锁 4.3 集群环境的缺陷 4.4 Redlock 目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题. 基于 CAP理论,任何一个分布式系统都无法同时满足一致性(Consistency).可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项. 我们为

  • 一文带你搞懂Redis分布式锁

    目录 1.分布式锁简介 2.setnx 3.Redis-分布式锁-阶段1 4.Redis-分布式锁-阶段2 5.Redis-分布式锁-阶段3 6.Redis-分布式锁-阶段4 7.Redis-分布式锁-阶段5 1.分布式锁简介 分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现.如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性. 业界流行的分布式锁实现,一般有这3种方式: 基于数据库实现的分布式锁 基于Redis实现的分布式锁 基于

  • java语言描述Redis分布式锁的正确实现方式

    分布式锁一般有三种实现方式:1.数据库乐观锁:2.基于Redis的分布式锁:3.基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性.在任意时刻,只有一个客户端能持有锁. 不会发生死锁.即使有一个客户端在持有锁的期间

  • 浅谈Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个

  • Java Redis分布式锁的正确实现方式详解

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性.在任意时刻,只有一个客户端能持有锁. 不会发生死锁.即使有一个客户端在

  • Redis分布式锁的正确实现方法总结

    分布式锁一般有三种实现方式: 1.数据库乐观锁: 2.基于Redis的分布式锁: 3.基于ZooKeeper的分布式锁. 本文将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 1.互斥性.在任意时刻,只有一个客户端能持有锁. 2.不会发生死锁.即使有一个客户端在

  • Redis分布式锁的使用和实现原理详解

    模拟一个电商里面下单减库存的场景. 1.首先在redis里加入商品库存数量. 2.新建一个Spring Boot项目,在pom里面引入相关的依赖. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <gr

  • 详解RedisTemplate下Redis分布式锁引发的系列问题

    自己的项目因为会一直抓取某些信息,但是本地会和线上经常一起跑,造成冲突.这其实就是我们常说的分布式集群的问题了,本地和线上的服务器构成了集群以及QPS为2的小并发(其实也不叫并发,不知道拿什么词形容?). 首先,分布式集群的问题大家都知道,会造成数据库的插入重复问题,会造成一系列的并发性问题. 解决的方式呢也大概如下几点,百度以及谷歌上都能搜到的解决方式: 1:数据库添加唯一索引 2:设计接口幂等性 3:依靠中间件使用分布式锁,而分布式锁又分为Redis和Zookeeper 由于Zookeepe

  • 详解redis分布式锁的这些坑

    一.白话分布式 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面,本来一个程序员可以完成一个项目:需求->设计->编码->测试 但是项目多的时候,一个人也扛不住,这就需要不同的人进行分工合作了 这就是一个简单的分布式协同工作了: 二.分布式锁 首先看一个问题,如果说某个环节被终止或者别侵占,就会发生不可知的事情 这就会出现,设计好的或者设计的半成品会被破坏,导致后面环节出错: 这时候,我们就需要引入分布式锁的概念: 何为分布式锁 当在分布式模型下,

  • Redis分布式锁详细介绍

    目录 分布式锁 redis实现分布式锁的原理 死锁问题 超时问题 锁误放问题 可重入性 Redlock 分布式锁 在单进程应用中,当一段代码同一时间内只能由一个线程执行时, 多线程下可能会出错,例如两个线程同时对一个数字做累加,两个线程同时拿到了该数字,例如40,一个线程加了10,一个线程加了20,正确结果应该是70, 但由于两个线程在自己的内存中一个算出的是50,一个算出的是60,此时二者都将自己的结果往该数字原本的地方写(保存), 这时候,肯定会有一个线程的值会被覆盖,因为读取->计算->

随机推荐