【分布式系统】分布式锁实现之Redis

news2025/1/18 10:43:09

锁有资源竞争问题就有一定有锁的存在,存储系统MySQL中,有锁机制保证数据并发访问。而编程语言层面Java中有JUC并发工具包来实现,那么锁解决的问题是什么?主要是在多线程环境下,对共享资源的互斥。从而保证数据一致性。

单机锁

单机锁,一般就直接使用syn或者lock。两种属于不同的机制,一种是jvm内置的机制,另一种是AQS原理机制来保证锁机制的。本质上都属于同一个进程内。但是当系统采用集群模式部署的时候,是跨机器、跨进程调用。这个时候就需要采用一个共享的存储系统来保存锁状态。

分布式锁

分布式锁一般实现方式有Redis、ZK、MySQL等本篇主要介绍Redis实现方式。
分布式锁需要具备以下几点

  • 独占性:任何时刻都只能有一个线程持有。
  • 高可用:如果是单机点Redis有单点宕机的可能,造成锁无法释放和获取。为保证高可用,一般需要集群模式
  • 防止死锁:如果出现锁无法释放的情况下,那么需要有超时控制机制,进行兜底方案
  • 不乱入:A锁进行释放不能释放成B锁,B锁释放不能释放成A锁
  • 重入性:同一个节点的同一个线程获取后,再次读写共享资源不需要再次获取锁。

如何设置分布式锁 ?
在这里插入图片描述
在这里插入图片描述
因为setnx + expire 不是原子性操作,所以不推荐使用。

场景设计:秒杀设计。

V1:单机版没加锁 初始只是单个系统处理用户请求。因为没有加锁控制,会出现同一时间内,多个线程会处理同一个票的情况。出现了同一张票被卖出多次。业务上是不允许的。因此进行加锁,而在单体系统中,我们的程序都是在一个进程内执行的,也既这种锁就是JVM 进程内的。一般来说,我们可以采用syn和lock,具体如何权衡需要我们自己来分析。

V2:Nginx分布式微服务架构 在V1的基础上,加锁。但是一般系统为了更好的提供服务给用 户,需要多个模块集群部署。而对于用户来说,具体那一台服务器处理请求是完全透明的, 所以需要在前面加一台反向代理服务器Nginx,将用户请求通过某一种策略达到服务器上。但是通过Jmter压测后,还是发现同一个票会被卖多次。我们来分析一下,为什么呢,虽然,我们加锁了。但是这个锁只是进程内的锁,并不能保证跨进程之间共享数据的操作。所以,我们需要进一步采用分布式锁来解决这个问题。 setnx

V3:释放锁 虽然采用了分布式setnx进行处理,但是我们无法保证程序执行到最后一定会释 放锁,所以将释放锁的code放在finally块中。保证不管程序是否状态与否,一定会释放锁。 其他线程可以抢到锁。

V4:单点宕机 虽然V3可以保证程序一定可以释放锁,但是如果程序执行到一半,此时redis 宕机,那么锁就无法释放,而别的线程也无法获取锁。因此需要给一把锁加上一个过期时间。

V5:过期时间与设置key 对于设置key和过期时间,因为是两个不同的语句,无法保证原子性,因此有可能设置了key,但是过期时间没有设置上,所以需要保证原子操作。直接使用 redis提供的命令。

V6:只释放属于自己的锁 虽然我们保证了一定会释放锁,在极端情况下,比如A线程尝试获取锁,获取到RedLock,执行了30S后过期时间到了,但是线程A并没有执行完毕任务。线程 B去获取锁,获取到了。线程B往下执行。此时线程A走到释放锁的模块,直接释放锁,B在执行的过程中被A释放锁。就会出现业务数据的不一致。所以 删除锁的之后,应该先判断一 下,属于自己的锁才删除。

V7:判断锁和删除锁非原子性 可以使用Redis的事务进行保证,但是推荐使用Lua脚本。

V8:过期时间和业务处理时间 需要进行续期操作,如果没有执行完毕,延⻓过期时间。

V9:Redis的高可用 数据一致性 CAP强制规范下,Redis是一个高可用AP 而ZK是CP,通常, 我们搭建一个Redis的主从集群,而数据的一致性用ZK来保证。

V10:Redisson 虽然,我们使用redis相关命令可以实现分布式锁,但是还是推荐使用 redisson来实现,编程更加简易化。


/**
 * @author i
 * @create 2020/6/20 14:55
 * @Description
 *      加锁操作
 *      1.使用setbx命令保证互斥性
 *      2.需要设置锁的过期时间,避免死锁
 *      3.setnx和设置过期时间需要保持原子性,避免在设置setnx成功之后在设置过期时间客户端崩溃导致死锁
 *      4.加锁的value值为一个唯一标示,可以采用UUID作为一个唯一标示,加锁成功后需要把唯一标示返回给客户端来用客户端进行解锁操作
 *      解锁操作
 *      1.需要拿加锁成功的唯一标识进行解锁,从而保证加锁和解锁的是同一个客户端
 *      2.解锁操作需要比较唯一标识是否相等,相等在执行删除操作,这两个操作可以采用lua脚本方式使用2个命令的原子性
 *
 */
public interface DistributedLock {

    /***
     * 获取锁
     * @return
     */
    String acquire();


    /***
     * 释放锁
     * @param identifier
     * @return
     */
    boolean release(String identifier);

}

@Slf4j
public class RedisDistributedLock implements DistributedLock {

    private static final String LOCK_SUCCESS = "OK"; //上锁成功
    private static final Long RELEASE_SUCCESS = 1L; //释放成功
    private static final String SET_IF_NOT_EXIST = "NX"; //不存在
    private static final String SET_WITH_EXPIRE_TIME = "PX"; //

    /**
     * redis 客户端
     */
    private Jedis jedis;

    /**
     * 分布式锁的键值
     */
    private String lockKey;

    /**
     * 锁的超时时间 10s
     */
    int expireTime = 10 * 1000;

    /**
     * 锁等待,防止线程饥饿
     */
    int acquireTimeout = 1 * 1000;

    /**
     * 获取指定键值的锁
     *
     * @param jedis   jedis Redis客户端
     * @param lockKey 锁的键值
     */
    public RedisDistributedLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
    }

    /**
     * 获取指定键值的锁,同时设置获取锁超时时间
     *
     * @param jedis          jedis Redis客户端
     * @param lockKey        锁的键值
     * @param acquireTimeout 获取锁超时时间
     */
    public RedisDistributedLock(Jedis jedis, String lockKey, int acquireTimeout) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.acquireTimeout = acquireTimeout;
    }

    /**
     * 获取指定键值的锁,同时设置获取锁超时时间和锁过期时间
     *
     * @param jedis          jedis Redis客户端
     * @param lockKey        锁的键值
     * @param acquireTimeout 获取锁超时时间
     * @param expireTime     锁失效时间
     */
    public RedisDistributedLock(Jedis jedis, String lockKey, int acquireTimeout, int expireTime) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.acquireTimeout = acquireTimeout;
        this.expireTime = expireTime;
    }

    /***
     * 获取锁
     * 成功 返回唯一的token
     * 失败 返回null
     * @return
     */
    @Override
    public String acquire() {
        try {
            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            // 随机生成一个value
            String requireToken = UUID.randomUUID().toString();
            //在固定时间内 不断重试获取锁 如果获取成功直接返回
            //如果获取失败 并且超过获取锁的时间 直接返回null
            //超过时间自动释放获取锁的流程
            while (System.currentTimeMillis() < end) {
                //lockKey:分布式锁的键值
                //requireToken:生成一个随机token
                //SET_IF_NOT_EXIST:如果不存在
                //SET_WITH_EXPIRE_TIME 设定过期时间
                //锁的超时时间
                String result = jedis.set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
                //获取锁成功 返回给每个客户端一个不唯一的token标识
                if (LOCK_SUCCESS.equals(result)) {
                    return requireToken;
                }
                //否则不断重试,在规定的超时时间内。
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception e) {
            log.error("acquire lock due to error", e);
        }
        //没有获取锁 返回null
        return null;
    }

    /**
     * 释放锁
     *
     * @param identify
     * @return
     */
    @Override
    public boolean release(String identify) {
        //如果用户标志位null 返回false
        if (identify == null) {
            return false;
        }

        //lua脚本 从一定程度上可以保证释放锁的原子性操作
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = new Object();
        try {
            //执行脚本
            result = jedis.eval(script, Collections.singletonList(lockKey),
                    Collections.singletonList(identify));
            //释放锁成功 返回true
            if (RELEASE_SUCCESS.equals(result)) {
                log.info("release lock success, requestToken:{}", identify);
                return true;
            }
        } catch (Exception e) {
            log.error("release lock due to error", e);
        } finally {
            //释放连接资源
            if (jedis != null) {
                jedis.close();
            }
        }
        //释放锁失败
        log.info("release lock failed, requestToken:{}, result:{}", identify, result);
        return false;
    }
}
public class RedisDistributedLockTest {

    static int n = 500;
    public static void secskill() {
        System.out.println(--n);
    }

    public static void main(String[] args) {
        Runnable runnable = () -> {
            DistributedLock lock = null;
            String unLockIdentify = null;//释放锁标志
            try {
                Jedis conn = new Jedis("127.0.0.1",6379);
                lock = new RedisDistributedLock(conn, "test1");
                unLockIdentify = lock.acquire();
                System.out.println(Thread.currentThread().getName() + "正在运行");
                secskill();
            } finally {
                if (lock != null) {
                    lock.release(unLockIdentify);
                }
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

Redis高可靠分布式锁

readLock是为了避免Redis实例因故障而导致无法工作的问题。

基本思想就是客户端发送的锁请求从多个Redis实例中依次请求,如果客户端可以获取半数以上的实例节点,那么就可以认为加锁成功。这样就可以避免因其中部分节点宕机导致获取不到锁,加锁失败。

ReadLock算法核心步骤
1.客户端获取当前时间
2.客户端按照顺序依次向N个Redis实例执行加锁操作。
3.一旦客户端完成了和所有Redis实例加锁操作,客户端计算整个过程总耗时。
当满足以下两个条件时,认为加锁成功

  • 条件一:客户端获取了半数以上Redis实例的加锁成功结果。
  • 条件二:客户端获取锁的时间没有超过锁的过期时间。
    其中如果执行业务逻辑的时间没有来得及进行处理处理,那么也会加锁成功,直接释放锁。而这个耗时就是锁过期时间减去锁的加锁时间。
    我们可以采用 Lua 脚本执行释放锁操作,通过 Redis 原子性地执行 Lua 脚本,来保证释放锁操作的原子性。

小结

本篇主要介绍了分布式锁,单机以及Redis实现方式。引入分布式虽然可以提高系统的吞吐量和稳定性,但是也引入了很多分布式问题,比如分布式共识、分布式锁、事务等,而这就是trade-off艺术。而分布式锁可以通过Redis、ZK、MySQL等实现。但是一般来说推荐使用Redis,主要是大多数互联网项目都使用Redis作为缓存,直接使用不需要再次引入ZK。但是具体场景也需要具体分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/556719.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SVG在前端中的常见应用

SVG在前端中的常见应用 一、svg标签1. svg2. g 二、描边属性三、模糊和阴影效果1. 模糊2. 阴影效果 四、线性渐变和径向渐变1. 线性渐变2. 径向渐变 五、绘制1. 内置形状元素2. 绘制矩形3. 绘制圆形4. 绘制椭圆5. 绘制线条6. 绘制多边形7. 绘制多线条8. 绘制文本9. 绘制路径 只…

【C/C++】动态内存管理/泛型编程

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;命运给你一个低的起点&#xff0c;是想看你精彩的翻盘&#xff0c;而不是让你自甘堕落&#xff0c;脚下的路虽然难走&#xff0c;但我还能走&#xff0c;比起向阳而生&#xff0c;我更想尝试逆风…

SQLlite教程(第一篇)

SQLlite教程(第一篇 SQLlite是什么?SQLlite工作原理是什么?SQLlite有什么功能和特性?使用SQLlite有哪些注意事项?附加资料 SQLlite是什么? SQLite&#xff0c;是一款轻型的数据库&#xff0c;是遵守ACID的关系型数据库管理系统&#xff0c;它包含在一个相对小的C库中。它是…

Mysql审核查询平台Archery部署

目录 1 Archery产品介绍2 基于docker搭建Archery2.1 系统环境2.2 安装 Docker2.2.1 安装 Docker Compose2.2.2 下载Archery2.2.3 安装并启动2.2.4 表结构初始化2.2.5 数据初始化2.2.6 创建管理用户2.2.7 退出重启2.2.8 日志查看和问题排查2.2.9 启动成功查看2.2.10 端口占用情况…

基于Maven的profiles多环境配置

一个项目通常都会有多个不同的运行环境&#xff0c;例如开发环境&#xff0c;测试环境、生产环境等。而不同环境的构建过程很可能是不同的&#xff0c;例如数据源配置、插件、以及依赖的版本等。每次将项目部署到不同的环境时&#xff0c;都需要修改相应的配置&#xff0c;这样…

day07_数组初识

数组的概述 数组就是用于存储数据的长度固定的容器&#xff0c;保证多个数据的数据类型要一致。 数组适合做一批同种类型数据的存储 数组中的元素可以是基本数据类型&#xff0c;也可以是引用数据类型。当元素是引用数据类型是&#xff0c;我们称为对象数组。 容器&#xff…

从0开始学C语言的个人心得笔记(10w字)

大学的计算机相关专业第一门教学的计算机语言就是c语言&#xff0c;很多大学生面对从未接触过的计算机语言&#xff0c;可能会觉得很难以上门&#xff0c;从而放弃学习c语言。这篇博客写的主要是个人学习C语言时候的知识总结点&#xff0c;不能保证全部是正确的&#xff0c;如有…

Kafka灵魂28问

第 1 题 Kafka 数据可靠性如何保证&#xff1f; 对于 kafka 来说&#xff0c;以下几个方面来保障消息分发的可靠性&#xff1a; 消息发送的可靠性保障(producer) 消息消费的可靠性保障(consumer) Kafka 集群的可靠性保障&#xff08;Broker&#xff09; 生产者 目前生产者…

Leetcode每日一题——“用队列实现栈”

各位CSDN的uu们你们好呀&#xff0c;好久没有更新本专栏啦&#xff0c;甚是想念&#xff01;&#xff01;&#xff01;今天&#xff0c;小雅兰的学习内容是用队列实现栈&#xff0c;下面&#xff0c;让我们进入Leetcode的世界吧&#xff01;&#xff01;&#xff01; 这是小雅兰…

本地 docker 发布 java 项目,连接本地 redis 配置

1、本地项目 install 相应的 jar 包到 target 目录下&#xff0c;jar 包的路径步骤 2 要填写 2、项目根目录下创建 Dockerfile 文件 # 使用官方的 Java 11 镜像作为基础镜像 FROM openjdk:11-jdk# 设置工作目录 WORKDIR /app# 复制应用程序 JAR 文件到镜像中的 /app 目录下 C…

用LangChain实现一个ChatBlog

文章目录 前言环境一、构建知识库二、将知识库向量化三、召回四、利用LLM做阅读理解五、效果总结 前言 通过本文, 你将学会如何使用langchain来构建一个自己的知识库问答 其实大多数类chatpdf产品的原理都差不多, 我将其简单粗暴地分为以下四步: 构建知识库将知识库向量化召回…

vue diff算法与虚拟dom知识整理(11) 书写patch父级新旧为同一节点 子节点与文字交换逻辑实现

上文我们简单描述了patch处理同一节点的大体逻辑 这次 我们就来看一下text替换的情况 我们更改案例入口文件 src下的 index.js 代码如下 import h from "./snabbdom/h"; import patch from "./snabbdom/patch";const container document.getElementById(…

Maven概念及搭建

1.为什么我们要学习 maven? maven 还未出世的时候&#xff0c;我们有很多痛苦的经历 。 痛点 1&#xff1a; jar 包难以寻找 痛点 2&#xff1a; jar 包依赖的问题 痛点 3&#xff1a; jar 不方便管理 痛点 4&#xff1a;项目编译 2.Maven 简介 Maven 是 Apache 软件基金…

Golang中的管道(channel) 、goroutine与channel实现并发、单向管道、select多路复用以及goroutine panic处理

目录 管道&#xff08;channel&#xff09; 无缓冲管道 有缓冲管道 需要注意 goroutine与channel实现并发 单向管道 定义单向管道 将双向管道转换为单向管道 单向管道作为函数参数 单向管道的代码示例 select多路复用 案例演示 goroutine panic处理 案例演示 管道…

APP服务端架构的演变

大家好&#xff0c;我是易安&#xff01; 早期2013年的时候&#xff0c;随着智能设备的普及和移动互联网的发展&#xff0c;移动端逐渐成为用户的新入口&#xff0c;各个电商平台都开始聚焦移动端App&#xff0c;如今经历了10年的发展&#xff0c;很多电商APP早已经没入历史的洪…

日语文法PPT截图31-45

31 形式名词 とき ところ 作为形式名词的话&#xff0c;一般是要写假名不写汉字的 相对时态 如果是一般时/将来时とき&#xff0c;就是先做后面的动作&#xff0c;在做前面的动作。 出教室的时候&#xff0c;关灯。 如果是过去时とき那么&#xff0c;是先做前面的动作&#…

Linux安装elk

稍后补充。 目录 01【安装elk】 es单机 es集群 esHead插件 kibana logstash elastic search:https://www.elastic.co/cn/downloads/elasticsearchlogstash:https://www.elastic.co/cn/downloads/logstashkibana:https://www.elastic.co/cn/downloads/kibana linux下安装E…

vector的介绍

vector的介绍&#xff1a;(vector翻译是向量&#xff0c;但是表示的是顺序表) vector是表示可以改变大小的数组的序列容器。 就像数组一样&#xff0c;vector对其元素使用连续的存储位置&#xff0c;这意味着也可以使用指向其元素的常规指针上的偏移量来访问它们的元素&#xf…

前端代码规范配置

前端代码规范配置 涉及到了eslint、prettier、husky、lint-staged等工具包的使用。 代码规则校验 使用eslint定义代码风格 安装eslint并在.eslintrc.js文件中配置。 npm i eslint -D这个代码风格可以使用公司团队内的规范&#xff0c;如果没有可以在github中找到一些主流的…

主机访问不到虚拟机(centos7)web服务的解决办法

目录 一、背景 二、解决办法 2.1、配置虚拟机防火墙 2.2、修改虚拟机网络编辑器 一、背景 主机可以访问外网&#xff0c;虚拟机使用命令&#xff1a;curl http://网址&#xff0c;可以访问到web服务 &#xff0c;主机使用http://网址&#xff0c;访问不到虚拟机&#xff08…