redis desktop manager_面试官:Redis分布式锁如何解决锁超时问题?

Java面试笔试面经、Java技术每天学习一点

84c780eb2b67e279b52ef7c09b2e8f3e.png

Java面试

关注不迷路

作者:wangzaiplus

来源:https://www.jianshu.com/u/8cb4591440ca

4b93462797e8fea35a668c0f52a6665b.png

一、前言

关于redis分布式锁, 查了很多资料, 发现很多只是实现了最基础的功能, 但是, 并没有解决当锁已超时而业务逻辑还未执行完的问题, 这样会导致: A线程超时时间设为10s(为了解决死锁问题), 但代码执行时间可能需要30s, 然后redis服务端10s后将锁删除, 此时, B线程恰好申请锁, redis服务端不存在该锁, 可以申请, 也执行了代码, 那么问题来了, A、B线程都同时获取到锁并执行业务逻辑, 这与分布式锁最基本的性质相违背: 在任意一个时刻, 只有一个客户端持有锁, 即独享

为了解决这个问题, 本文将用完整的代码和测试用例进行验证, 希望能给小伙伴带来一点帮助

二、准备工作

压测工具jmeter

https://pan.baidu.com/share/init?surl=NN0c0tDYQjBTTPA-WTT3yg
提取码: 8f2a

redis-desktop-manager客户端

https://pan.baidu.com/share/init?surl=NoJtZZZOXsk45aQYtveWbQ
提取码: 9bhf

postman

https://pan.baidu.com/share/init?surl=28sGJk4zxoOknAd-47hE7w
提取码: vfu7

也可以直接官网下载, 我这边都整理到网盘了

需要postman是因为我还没找到jmeter多开窗口的办法, 哈哈

三、说明

1、springmvc项目

2、maven依赖

        
        <dependency>
            <groupId>org.springframework.datagroupId>
            <artifactId>spring-data-redisartifactId>
            <version>1.6.5.RELEASEversion>
        dependency>
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>2.7.3version>
        dependency>

3、核心类

  • 分布式锁工具类: DistributedLock

  • 测试接口类: PcInformationServiceImpl

  • 锁延时守护线程类: PostponeTask

四、实现思路

先测试在不开启锁延时线程的情况下, A线程超时时间设为10s, 执行业务逻辑时间设为30s, 10s后, 调用接口, 查看是否能够获取到锁, 如果获取到, 说明存在线程安全性问题

同上, 在加锁的同时, 开启锁延时线程, 调用接口, 查看是否能够获取到锁, 如果获取不到, 说明延时成功, 安全性问题解决

五、实现

1、版本01代码

1)、DistributedLock

package com.cn.pinliang.common.util;

import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.io.Serializable;
import java.util.Collections;

@Component
public class DistributedLock {

    @Autowired
    private RedisTemplate redisTemplate;private static final Long RELEASE_SUCCESS = 1L;private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "EX";// 解锁脚本(lua)private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";/**
     * 分布式锁
     * @param key
     * @param value
     * @param expireTime 单位: 秒
     * @return
     */public boolean lock(String key, String value, long expireTime) {return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_SUCCESS.equals(result)) {return Boolean.TRUE;
            }return Boolean.FALSE;
        });
    }/**
     * 解锁
     * @param key
     * @param value
     * @return
     */public Boolean unLock(String key, String value) {return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));if (RELEASE_SUCCESS.equals(result)) {return Boolean.TRUE;
            }return Boolean.FALSE;
        });
    }
}

说明: 就2个方法, 加锁解锁, 加锁使用jedis setnx方法, 解锁执行lua脚本, 都是原子性操作

2)、PcInformationServiceImpl

    public JsonResult add() throws Exception {
        String key = "add_information_lock";
        String value = RandomUtil.produceStringAndNumber(10);
        long expireTime = 10L;

        boolean lock = distributedLock.lock(key, value, expireTime);
        String threadName = Thread.currentThread().getName();
        if (lock) {
            System.out.println(threadName + " 获得锁...............................");
            Thread.sleep(30000);
            distributedLock.unLock(key, value);
            System.out.println(threadName + " 解锁了...............................");
        } else {
            System.out.println(threadName + " 未获取到锁...............................");
            return JsonResult.fail("未获取到锁");
        }

        return JsonResult.succeed();
    }

说明: 测试类很简单, value随机生成, 保证唯一, 不会在超时情况下解锁其他客户端持有的锁

3)、打开redis-desktop-manager客户端, 刷新缓存, 可以看到, 此时是没有add_information_lock的key的

ac43826997cec06d68d3bf920119ae28.png

4)、启动jmeter, 调用接口测试

设置5个线程同时访问, 在10s的超时时间内查看redis, add_information_lock存在, 多次调接口, 只有一个线程能够获取到锁

redis

b2453a0730542272785d7c187a2a92bf.png

1-4个请求, 都未获取到锁

6aced9b844c2d92158210ed51fed2ef8.png

第5个请求, 获取到锁

10449324e2c30eeb76ddf84188bebbbe.png

OK, 目前为止, 一切正常, 接下来测试10s之后, A仍在执行业务逻辑, 看别的线程是否能获取到锁

5fd8ee84b9f074303eabd93b403edb46.png可以看到, 操作成功, 说明A和B同时执行了这段本应该独享的代码, 需要优化。

2、版本02代码

1)、DistributedLock

package com.cn.pinliang.common.util;

import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.io.Serializable;
import java.util.Collections;

@Component
public class DistributedLock {

    @Autowired
    private RedisTemplate redisTemplate;private static final Long RELEASE_SUCCESS = 1L;private static final Long POSTPONE_SUCCESS = 1L;private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "EX";// 解锁脚本(lua)private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 延时脚本private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";/**
     * 分布式锁
     * @param key
     * @param value
     * @param expireTime 单位: 秒
     * @return
     */public boolean lock(String key, String value, long expireTime) {// 加锁Boolean locked = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_SUCCESS.equals(result)) {return Boolean.TRUE;
            }return Boolean.FALSE;
        });if (locked) {// 加锁成功, 启动一个延时线程, 防止业务逻辑未执行完毕就因锁超时而使锁释放
            PostponeTask postponeTask = new PostponeTask(key, value, expireTime, this);
            Thread thread = new Thread(postponeTask);
            thread.setDaemon(Boolean.TRUE);
            thread.start();
        }return locked;
    }/**
     * 解锁
     * @param key
     * @param value
     * @return
     */public Boolean unLock(String key, String value) {return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));if (RELEASE_SUCCESS.equals(result)) {return Boolean.TRUE;
            }return Boolean.FALSE;
        });
    }/**
     * 锁延时
     * @param key
     * @param value
     * @param expireTime
     * @return
     */public Boolean postpone(String key, String value, long expireTime) {return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));if (POSTPONE_SUCCESS.equals(result)) {return Boolean.TRUE;
            }return Boolean.FALSE;
        });
    }
}

说明: 新增了锁延时方法, lua脚本, 自行脑补相关语法

2)、PcInformationServiceImpl不需要改动

3)、PostponeTask

package com.cn.pinliang.common.thread;

import com.cn.pinliang.common.util.DistributedLock;

public class PostponeTask implements Runnable {

    private String key;
    private String value;
    private long expireTime;
    private boolean isRunning;
    private DistributedLock distributedLock;

    public PostponeTask() {
    }

    public PostponeTask(String key, String value, long expireTime, DistributedLock distributedLock) {
        this.key = key;
        this.value = value;
        this.expireTime = expireTime;
        this.isRunning = Boolean.TRUE;
        this.distributedLock = distributedLock;
    }

    @Overridepublic void run() {
        long waitTime = expireTime * 1000 * 2 / 3;// 线程等待多长时间后执行
        while (isRunning) {
            try {
                Thread.sleep(waitTime);
                if (distributedLock.postpone(key, value, expireTime)) {
                    System.out.println("延时成功...........................................................");
                } else {
                    this.stop();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void stop() {
        this.isRunning = Boolean.FALSE;
    }

}

说明: 调用lock同时, 立即开启PostponeTask线程, 线程等待超时时间的2/3时间后, 开始执行锁延时代码, 如果延时成功, add_information_lock这个key会一直存在于redis服务端, 直到业务逻辑执行完毕, 因此在此过程中, 其他线程无法获取到锁, 也即保证了线程安全性

下面是测试结果

10s后, 查看redis服务端, add_information_lock仍存在, 说明延时成功

a7118cb834a77d3098c273727353911f.png

此时用postman再次请求, 发现获取不到锁

7222f74e10517b08c6ec5bc4dd0aeee6.png

看一下控制台打印

92f3fe306a2378c9f6f223c523ad07fe.png

cd15082b33a6c4bc69753994acbdfa8b.png

A线程在19:09:11获取到锁, 在10 * 2 / 3 = 6s后进行延时, 成功, 保证了业务逻辑未执行完毕的情况下不会释放锁

A线程执行完毕, 锁释放, 其他线程又可以竞争锁

OK, 目前为止, 解决了锁超时而业务逻辑仍在执行的锁冲突问题, 还很简陋, 而最严谨的方式还是使用官方的 Redlock 算法实现, 其中 Java 包推荐使用 redisson, 思路差不多其实, 都是在快要超时时续期, 以保证业务逻辑未执行完毕不会有其他客户端持有锁

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

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

相关文章

python print 换行_Python学习 | Python的基础语法

Python 语言与 Perl&#xff0c;C 和 Java 等语言有许多相似之处。但是&#xff0c;也存在一些差异&#xff0c;编写Paython程序之前需要对语法有所了解&#xff0c;才能编写规范的Python程序。一、行和缩进Python最大的特点之一就是Python 的代码块不使用大括号 {}了&#xff…

python 创建文件_Python入学首次项目,新手必看,简单易操作

继昨天文章python软件pycharm安装教程之后&#xff0c;今天则给新手小白们分享一哈&#xff0c;怎么制作并创建文件。print “hello world”&#xff1b;如后期需要资料文件的则可以私信留言&#xff0c;领取首次项目资料。本节知识点&#xff1a;python项目的创建pycharm的使用…

ajax跨域实现

2019独角兽企业重金招聘Python工程师标准>>> 我们都知道ajax是不能跨域的&#xff0c;那么怎么实现ajax跨域呢&#xff1f; 看了看jquery&#xff0c;当然&#xff0c;jquery封装的很好&#xff0c;$.ajax就可以实现跨域&#xff0c;只需要在参数中配置一下即可&am…

python leetcode_leetcode 刷题经验,主力 python

1. 树的先序遍历可以求高度&#xff0c;后序遍历可以求深度。剑指 Offer 55 - II. 平衡二叉树​leetcode-cn.com2. 二叉搜索树的中序遍历可以递增地返回所有元素。逆序的中序遍历&#xff08;即先右子节点&#xff0c;再根节点&#xff0c;再左子节点&#xff09;可以递减的返回…

sqlldr 导入乱码,Oracle客户端字符集问题

2019独角兽企业重金招聘Python工程师标准>>> 1&#xff0c;查Oracle数据库创建时候的字符集&#xff1a; Oracle服务器端执行 SQL> select name, value$ from sys.props$ where name like NLS%; NAME VALUE$ ------------------------------ -------------------…

Python在mysql中进行操作是十分容易和简洁的

首先声明一下&#xff0c;我用的是Windows系统&#xff01; 1、在Python中对mysql数据库进行操作首先要导入pymysql模块&#xff0c;默认情况下&#xff0c;Python中是没有安装这个模块的&#xff0c; 可以在Windows的命令行中用pip install pymysql来安装&#xff08;注意要连…

讲php fpm的书,细说PHP-fpm

最近在研究PHP的源码&#xff0c;有时候会延伸到很多东西。这里就专程找了下php-fpm的内容学习下。是什么&#xff1f;在理解php-fpm之前&#xff0c;我们要先搞清楚几个关键词以及他们之间的关系:CGIFastCGIphp-fpmphp-cgi.CGI:(Common Gateway Interface)&#xff0c;即通用网…

安卓微软雅黑字体ttf_618巨献丨精致的悦黑5字重小字体

悦黑字体简介去年双十一当天&#xff0c;小编应大家要求&#xff0c;分享了一款悦黑小字体&#xff0c;苹果和安卓都有&#xff1a;双十一巨献&#xff1a;令人瑟瑟发抖的5字重悦黑小字体大半年时间过去了&#xff0c;一直没有更新&#xff0c;今天抽空更新一下。悦黑是由造字工…

伪静态隐藏域名后缀_你想知道的动态URL、静态URl、伪静态URL概念及区别都在这里!...

【小宅按】我们说url的动态、静态、伪静态三种形式&#xff0c;其实从严格分类上来说&#xff0c;伪静态也是动态的一种&#xff0c;只是表现形式为静态。参考&#xff1a;动态url、静态url和伪静态url的详细讲解 - 好文分享动态URl动态页面的特征1、以ASP、PHP、JSP、ASP.NET …

700多位老人的“智慧”养老记

“智慧”养老&#xff0c;受益的不只是居住养老公寓的老年人&#xff0c;养老机构本身也受益匪浅。 2012年12月&#xff0c;由汇晨养老公司机构管理有限公司(简称汇晨养老公司)与NEC中国共同开发的智能老年公寓信息化系统投入运行&#xff0c;生活在北京昌平区北七家汇晨老年公…

docker 安装nginx_docker安装nginx搭建简单文件共享服务

使用nginx开启目录浏览功能&#xff0c;实现简单的http文件共享服务。一、 首先拉取nginx镜像&#xff0c;我使用的是arm32v7/nginx镜像。docker pull arm32v7/nginx二、运行一个临时的nginx实例&#xff0c;复制容器内的nginx.conf配置文件到主机上docker run --name tmp-ngin…

java _web之Servlet简单应用

Servlet是一种独立于平台和协议的服务器端的Java应用程序&#xff0c;可以生成动态的web页面。它担当Web浏览器或其他http客户程序发出请求、与http服务器上的数据库或应用程序之间交互的中间层。 所谓动态网页&#xff0c;就是在不同时刻或不同条件下访问Web服务器上的同一个页…

mysql redis hbase_MySQL之基本介绍

MySQL基本介绍在学习任何一款编程语言的过程中&#xff0c;我们会发现如果我们想要存储一些数据到本地硬盘的时候无疑是有些麻烦的。今天&#xff0c;我们就学一个和储存数据有关的数据库管理系统——MySQL。为啥要学会使用数据库呢&#xff1f;因为数据库可以方便的将数据存放…

linux mysql 修改root密码_Mac下重置mysql的root密码

php中文网最新课程每日17点准时技术干货分享我的mysql版本 MYSQL V5.7.9&#xff0c;旧版本请使用&#xff1a;UPDATE mysql.user SET PasswordPASSWORD(新密码) WHERE Userroot;Mac OS X - 重置 MySQL Root密码密码太多记不住&#xff1f;&#xff1f;你是否忘记了Mac OS 的My…

DHCP option 150与option 66的区别

转载于:https://blog.51cto.com/jaymimijay/1150777

python期末知识点_史上最全的Python知识点整理之基本语法

一、程序的格式框架 1.缩进 缩进是指每行语句前的空白区域&#xff0c;用来表示Python程序间的包含和层次关系。 一般语句不需要缩进&#xff0c;顶行书写且不留空白。 当表示分支、循环、函数、类等含义&#xff0c;在if&#xff0c;while&#xff0c;for&#xff0c;def&…

php网页多个倒计时,怎么实现一个页面有多个倒计时同时进行

后台返回时间&#xff0c;一个页面中有多个表格每个表格中有一行用来显示后台给的时间&#xff0c;比如说后台给一号表格10分钟的倒计时时间&#xff0c;给2号表格15分钟的倒计时时间&#xff0c;那我要怎么做才能使得这两个倒计时都能进行&#xff1f;&#xff1f;我自己模拟了…

opencv在python环境下的安装_python环境下安装opencv库的方法

注意&#xff1a;安装opencv以前须要先安装numpy&#xff0c;matplotlib等python 1、安装方法windows 方法1、在线安装函数 1.先安装opencv-python测试 pip install opencv-python --userspa个人python版本是3.6.8&#xff0c;能够看到opencv安装的默认版本是 opencv_python-4.…

接口的创建及使用

接口是全局变量和公共的抽象方法集合。它也是一种定义数据类型的方式&#xff0c;与类相比 相同之处&#xff1a;都是成员变量和成员方法也可以形成继承关系。 不同之处&#xff1a;接口中的属性都是常量&#xff08;final&#xff09;接口中的方法是抽象方法&#xff08;没有方…

movielens推荐系统_基于内容推荐(二)

A content-based movie recommender system using MovieLens tags &#xff08;用标签构建一个简单的电影推荐系统&#xff09;现在有很多电影。如果没有某种推荐系统&#xff0c;您会担心&#xff0c;随着时间的流逝&#xff0c;用户可能会被他们不关心的电影所淹没。因此&…