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,一经查实,立即删除!

相关文章

全国计算机等级考试题库二级C操作题100套(第50套)

第50套&#xff1a; 给定程序中&#xff0c;函数fun的功能是:有NN矩阵&#xff0c;以主对角线为对称线&#xff0c;对称元素相加并将结果存放在左下三角元素中&#xff0c;右上三角元素置为0。例如&#xff0c;若N3&#xff0c;有下列矩阵&#xff1a; 1 2 3 4 5 6 7 8 9 计算…

《CLR via C#》读书笔记 之 参数

第九章 参数 2013-02-27 9.3 以传引用的方式向方法传递参数 默认情况下&#xff0c;CLR假定所有方法参数都是传值的。当传递引用类型的对象时&#xff0c;也默认是传值的&#xff0c;只不过这个值是引用&#xff08;指针&#xff09;本身。 CLR允许以传引用的方式传递参数。在C…

学习笔记整理之模式化方法

第一步 要分清要用那个不变的参数去实现 &#xff08;比如我现在要用 ID进行验证 则&#xff0c;验证完毕后实现的抽象方法的参数是 操作数&#xff0c;所以操作 的方法的参数是操作数&#xff09;先new 个要实现的方法比如 New StudentManger(id,name) 此方法要把继承的抽象的…

php组合查询,PHP组合查询多条件查询实例代码第1/2页

先向大家说明需求&#xff1a;按照我们系统的要求&#xff0c;我们将通过部门名称、员工姓名、PC名称、IP地址等等字段来进行组合查询从而得到想要的数据结果。那么&#xff0c;为了简单起见&#xff0c;我们用两个条件(部门名称、员工姓名)的组合查询来向大家说明这一技术技巧…

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

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

解决linux下source /etc/profile关闭终端失效问题

本来想配置环境变量的&#xff0c;看网上和博客上很多说改/etc/profile&#xff0c;然后source /etc/profile之后就可以永久保存使环境变量生效&#xff0c;但是终端一关闭&#xff0c;就环境变量就失效了&#xff0c;其他终端也用不了。网上有说在当前用户目录下创建.bash_pro…

bind php,PHP – bind_result到数组

我正在为一个返回多个结果的查询使用一个预准备语句,我想在一个数组中使用它.但是bind_result不能用于数组,所以我就是这样做的&#xff1a;$read_items $db->stmt_init();$read_items->prepare("SELECT item_id, item_name FROM items");$read_items->exe…

SQL Server遍历表中记录的2种方法

SQL Server遍历表一般都要用到游标&#xff0c;SQL Server中可以很容易的用游标实现循环&#xff0c;实现SQL Server遍历表中记录。本文将介绍利用使用表变量和游标实现数据库中表的遍历。 表变量来实现表的遍历 以下代码中&#xff0c;代码块之间的差异已经用灰色的背景标记。…

全国计算机等级考试题库二级C操作题100套(第51套)

第51套&#xff1a; 给定程序中&#xff0c;函数fun的功能是&#xff1a;计算出形参s所指字符串中包含的单词个数, 作为函数值返回。为便于统计&#xff0c;规定各单词之间用空格隔开。 例如&#xff0c;形参s所指的字符串为&#xff1a;This is a C language program.&#x…

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

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

php柱形图 数据sql,ThinkPHP 5.1 读取数据库中的图片

如果一个图片直接存在数据库中&#xff0c;可以用以下方法读出来。环境ThinkPHP 5.1 ,sqlsrv&#xff0c;pdo_sqlsrv代码//pdo 方式$pdonew PDO(sqlsrv:Serverlocalhost;DatabaseSD31022_Sample, sa, Sql2008);$stmt$pdo->prepare(select picture from crm_affixinfo where …

Babelfish

题目描述 You have just moved from Waterloo to a big city. The people here speak an incomprehensible dialect of a foreign language. Fortunately, you have a dictionary to help you understand them.输入 Input consists of up to 100,000 dictionary entries, follo…

ajax跨域实现

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

全国计算机等级考试题库二级C操作题100套(第52套)

第52套&#xff1a; 给定程序中&#xff0c;函数fun的功能是&#xff1a;将NN矩阵中元素的值按列右移1个位置&#xff0c; 右边被移出矩阵的元素绕回左边。例如&#xff0c;N3&#xff0c;有下列矩阵 1 2 3 4 5 6 7 8 9 计算结果为 3 1 2 6 4 5 9 7 8 请在程序的下划线处填入正…

python leetcode_leetcode 刷题经验,主力 python

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

普及几个小常识,新手技能补充

一&#xff0c;如何设置门户或者论坛为首页&#xff1f;后台界面》导航管理&#xff0c;看向每个导航的最后面&#xff0c;有选择是否设置为首页&#xff0c;选中的即为打开之后的首页。二&#xff0c;如何添加二级导航&#xff0c;后台界面》导航管理&#xff0c;添加二级导航…

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

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

全国计算机等级考试题库二级C操作题100套(第53套)

第53套&#xff1a; 函数fun的功能是&#xff1a;计算请在程序的下划线处填入正确的内容并把下划线删除&#xff0c;使程序得出正确的结果。 注意&#xff1a;源程序存放在考生文件夹下的BLANK1.C中。 不得增行或删行&#xff0c;也不得更改程序的结构&#xff01; 给定源程序…

python中hashmap的方法_如何为Java的HashMap模拟Python的dict的“ items(...

我知道我应该学习如何使用Java进行高效编程,而不再认为它是Python.但是问题在于,这种事情在Java中变成了一场噩梦. 我在Python中具有以下函数,该函数将字符串作为参数&#xff1a; def decodeL(input): L [] for i in range(len(input)): for j in x.items(): // Where "…

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

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