面试常问:Redis分布式锁如何解决锁超时问题?

面试常问:Redis分布式锁如何解决锁超时问题?

AI乔治 2020-11-23 19:09:35 227 收藏 2
分类专栏: java 面试 文章标签: Java 架构 面试
版权

一、前言
关于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依赖

    <!--redis-->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>1.6.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.7.3</version>
    </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<Serializable, Object> 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的

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

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

往期100篇回顾:一百期面试题汇总

redis

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

第5个请求, 获取到锁

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

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

往期100篇回顾:一百期面试题汇总

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<Serializable, Object> 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;
}

@Override
public 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仍存在, 说明延时成功

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

看一下控制台打印

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

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

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

最新2020整理收集的一些面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友扫描下方二维码免费获取:暗号:【CSDN】

å¨è¿éæå¥å¾çæè¿°

看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

关注公众号 『 java烂猪皮 』,不定期分享原创知识。

同时可以期待后续文章ing🚀

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值