1. 程式人生 > >Redis分散式鎖

Redis分散式鎖

前言

1、Jedis分散式鎖

1.1、鎖的工具類

package com.hlj.redis.lock.utils;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
 * @Desc:
 * @Author HealerJean
 * @Date 2018/9/13  上午11:31.
 */
public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX"
; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * 嘗試獲取分散式鎖 * * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @param expireTime 超期時間 * @return 是否獲取成功 * 第一個為key,我們使用key來當鎖,因為key是唯一的。 第二個為value,我們傳的是requestId, 很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什麼還要用到value?原因就是我們在上面講到可靠性時,分散式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用UUID.randomUUID().toString()方法生成。 第三個為nxxx,這個引數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作; 第四個為expx,這個引數我們傳的是PX,意思是我們要給這個key加一個過期的設定,具體時間由第五個引數決定。 第五個為time,與第四個引數相呼應,代表key的過期時間。 */
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return
false; } /** * 釋放分散式鎖 * * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @return 是否釋放成功 * 那麼這段Lua程式碼的功能是什麼呢?其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那麼為什麼要使用Lua語言來實現呢?因為要確保上述操作是原子性的。關於非原子性會帶來什麼問題,可以閱讀【解鎖程式碼-錯誤示例2】 。那麼為什麼執行eval()方法可以確保原子性,源於Redis的特性,下面是官網對eval命令的部分解釋: */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { // 第一行程式碼,我們寫了一個簡單的Lua指令碼程式碼 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; // 引數KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua程式碼交給Redis服務端執行。 Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }

1.2、進行測試

package com.hlj.redis.lock.service;

import com.hlj.redis.lock.utils.RedisTool;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @Desc:
 * @Author HealerJean
 * @Date 2018/9/13  上午11:32.
 */
public class RedisLockConsumerService {

    //庫存個數
    static int goodsCount = 10;

    //賣出個數
    static int saleCount = 0;

    public static void main(String[] args) {
        JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 1000);

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {Thread.sleep(2);} catch (InterruptedException e) {}
                Jedis jedis = jedisPool.getResource();
                boolean lock = false;
                while (!lock) {
                    lock = RedisTool.tryGetDistributedLock(jedis, "goodsCount", Thread.currentThread().getName(), 10);
                }
                if (lock) {
                    if (goodsCount > 0) {
                        goodsCount--;
                        System.out.println("剩餘庫存:" + goodsCount + " 賣出個數" + ++saleCount);
                    }
                }
                RedisTool.releaseDistributedLock(jedis, "goodsCount", Thread.currentThread().getName());
                jedis.close();
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        jedisPool.close();
    }
}

2、redisTemplate鎖

2.1、定義鎖的介面

package com.hlj.redis.lock.utils;

/**
 * redis鎖,原地址https://gitee.com/itopener/springboot/tree/master
 */
public interface DistributedLock {


    public static final long TIMEOUT_MILLIS = 30000; //超時時間

    public static final int RETRY_TIMES = Integer.MAX_VALUE; //重試次數

    public static final long SLEEP_MILLIS = 500; //重試時 執行緒休眠次數

    public boolean lock(String key);

    public boolean lock(String key, int retryTimes);

    public boolean lock(String key, int retryTimes, long sleepMillis);

    public boolean lock(String key, long expire);

    public boolean lock(String key, long expire, int retryTimes);

    public boolean lock(String key, long expire, int retryTimes, long sleepMillis);

    public boolean releaseLock(String key);
}



2、2、實現上述介面的抽象類

package com.hlj.redis.lock.utils;

/**
 * redis鎖,原地址https://gitee.com/itopener/springboot/tree/master
 */
public abstract class AbstractDistributedLock implements DistributedLock {


    @Override
    public boolean lock(String key) {
        return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
    }

    @Override
    public boolean lock(String key, int retryTimes) {
        return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
    }

    @Override
    public boolean lock(String key, int retryTimes, long sleepMillis) {
        return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
    }

    @Override
    public boolean lock(String key, long expire) {
        return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
    }

    @Override
    public boolean lock(String key, long expire, int retryTimes) {
        return lock(key, expire, retryTimes, SLEEP_MILLIS);
    }

}

2.3、具體服務層實現分散式鎖

package com.hlj.redis.lock.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * redis鎖,原地址https://gitee.com/itopener/springboot/tree/master
 */
@Service("redisDistributedLock")
public class RedisDistributedLock extends AbstractDistributedLock {

    private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);

    private RedisTemplate<Object, Object> redisTemplate;

    private ThreadLocal<String> lockFlag = new ThreadLocal<String>();

    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {
        super();
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        // 如果獲取鎖失敗,按照傳入的重試次數進行重試
        while ((!result) && retryTimes-- > 0) {
            try {
                logger.debug("lock failed, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedis(key, expire);
        }
        return result;
    }

    private boolean setRedis(String key, long expire) {
        try {
            String result = redisTemplate.execute(new RedisCallback<String>() {
                @Override
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                    String uuid = UUID.randomUUID().toString();
                    lockFlag.set(uuid);
                    //PX millionSecond
                    return commands.set(key, uuid, "NX", "PX", expire);
                }
            });
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }

    @Override
    public boolean releaseLock(String key) {
        // 釋放鎖的時候,有可能因為持鎖之後方法執行時間大於鎖的有效期,此時有可能已經被另外一個執行緒持有鎖,所以不能直接刪除
        try {
            List<String> keys = new ArrayList<String>();
            keys.add(key);
            List<String> args = new ArrayList<String>();
            args.add(lockFlag.get());

            // 使用lua指令碼刪除redis中匹配value的key,可以避免由於方法執行時間過長而redis鎖自動過期失效的時候誤刪其他執行緒的鎖
            // spring自帶的執行指令碼方法中,叢集模式直接丟擲不支援執行指令碼的異常,所以只能拿到原redis的connection來執行指令碼

            Long result = redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    // 叢集模式和單機模式雖然執行指令碼的方法一樣,但是沒有共同的介面,所以只能分開執行
                    // 叢集模式
                    if (nativeConnection instanceof JedisCluster) {
                        return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                    }

                    // 單機模式
                    else if (nativeConnection instanceof Jedis) {
                        return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                    }
                    return 0L;
                }
            });

            return result != null && result > 0;
        } catch (Exception e) {
            logger.error("release lock occured an exception : key = {}", key, e);
        } finally {
            // 清除掉ThreadLocal中的資料,避免記憶體溢位
            lockFlag.remove();
        }
        return false;
    }

}

2.4、進行測試

package com.hlj.redis.lock;

import com.hlj.redis.lock.utils.DistributedLock;
import com.hlj.redis.lock.utils.RedisTool;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import java.util.Date;

/**
 * @Desc:
 * @Author HealerJean
 * @Date 2018/9/13  下午12:04.
 */
@RequestMapping("redis/lock")
@Controller
public class LockController {


    //庫存個數
     int goodsCount = 10;

    //賣出個數
     int saleCount = 0;

    /**
     * 快取key-使用者體力鎖
     */
    public static final String TEST_LOCK = "test_lock:";

    @Resource
    private DistributedLock lock;

    @GetMapping("test")
    @ResponseBody
    public void lockRedis(){
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {Thread.sleep(2);} catch (InterruptedException e) {}
                if (lock.lock(TEST_LOCK , 3000l, 5, 500)) {
                    if (goodsCount > 0) {
                        goodsCount--;
                        System.out.println("剩餘庫存:" + goodsCount + " 賣出個數" + ++saleCount);
                    }
                }
                lock.releaseLock(TEST_LOCK);

            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


}

2.5、測試結果

剩餘庫存:9 賣出個數1
剩餘庫存:8 賣出個數2
剩餘庫存:7 賣出個數3
剩餘庫存:6 賣出個數4
剩餘庫存:5 賣出個數5
剩餘庫存:4 賣出個數6
剩餘庫存:3 賣出個數7
剩餘庫存:2 賣出個數8
剩餘庫存:1 賣出個數9
剩餘庫存:0 賣出個數10

如果滿意,請打賞博主任意金額,感興趣的在微信轉賬的時候,新增博主微信哦, 請下方留言吧。可與博主自由討論哦

支付包 微信 微信公眾號
支付寶 微信 微信公眾號