springmvc單Redis實例實現分佈式鎖(解決鎖超時問題)

1、前言

關於redis分佈式鎖, 查了不少資料, 發現不少只是實現了最基礎的功能, 可是, 並無解決當鎖已超時而業務邏輯還未執行完的問題, 這樣會致使: A線程超時時間設爲10s(爲了解決死鎖問題), 但代碼執行時間可能須要30s, 而後redis服務端10s後將鎖刪除, 此時, B線程剛好申請鎖, redis服務端不存在該鎖, 能夠申請, 也執行了代碼, 那麼問題來了, A、B線程都同時獲取到鎖並執行業務邏輯, 這與分佈式鎖最基本的性質相違背: 在任意一個時刻, 只有一個客戶端持有鎖, 即獨享java

爲了解決這個問題, 本文將用完整的代碼和測試用例進行驗證, 但願能給小夥伴帶來一點幫助git

2、準備工做

  1. 壓測工具jmeter
    下載連接
    提取碼: 8f2agithub

  2. redis-desktop-manager客戶端
    下載連接
    提取碼: 9bhfredis

  3. postman
    下載連接
    提取碼: vfu7算法

也能夠直接官網下載, 我這邊都整理到網盤了spring

須要postman是由於我還沒找到jmeter多開窗口的辦法, 哈哈緩存

3、說明

  1. springmvc項目安全

  2. maven依賴mvc

<!--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>
  1. 核心類
  • 分佈式鎖工具類: DistributedLockdom

  • 測試接口類: PcInformationServiceImpl

  • 鎖延時守護線程類: PostponeTask

4、實現思路

  1. 先測試在不開啓鎖延時線程的狀況下, A線程超時時間設爲10s, 執行業務邏輯時間設爲30s, 10s後, 調用接口, 查看是否可以獲取到鎖, 若是獲取到, 說明存在線程安全性問題

  2. 同上, 在加鎖的同時, 開啓鎖延時線程, 調用接口, 查看是否可以獲取到鎖, 若是獲取不到, 說明延時成功, 安全性問題解決

5、實現

  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的
image.png

4)、啓動jmeter, 調用接口測試
設置5個線程同時訪問, 在10s的超時時間內查看redis, add_information_lock存在, 屢次調接口, 只有一個線程可以獲取到鎖

redis
image.png

1-4個請求, 都未獲取到鎖
image.png

第5個請求, 獲取到鎖
image.png

OK, 目前爲止, 一切正常, 接下來測試10s以後, A仍在執行業務邏輯, 看別的線程是否能獲取到鎖
image.png

能夠看到, 操做成功, 說明A和B同時執行了這段本應該獨享的代碼, 須要優化

  1. 版本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仍存在, 說明延時成功
image.png

此時用postman再次請求, 發現獲取不到鎖
image.png

看一下控制檯打印
image.png

image.png

A線程在19:09:11獲取到鎖, 在10 * 2 / 3 = 6s後進行延時, 成功, 保證了業務邏輯未執行完畢的狀況下不會釋放鎖

A線程執行完畢, 鎖釋放, 其餘線程又能夠競爭鎖

OK, 目前爲止, 解決了鎖超時而業務邏輯仍在執行的鎖衝突問題, 還很簡陋, 而最嚴謹的方式仍是使用官方的 Redlock 算法實現, 其中 Java 包推薦使用 redisson, 思路差很少其實, 都是在快要超時時續期, 以保證業務邏輯未執行完畢不會有其餘客戶端持有鎖

後面學習redisson, 看一下大神是怎麼實現的

若是有什麼不對的或者能夠優化的但願小夥伴多多指教, 留言評論什麼的, 謝謝

參考文章: http://www.javashuo.com/article/p-ejcdlfga-x.html

相關文章
相關標籤/搜索