首頁 > 軟體

Redis超詳細分析分散式鎖

2022-07-27 22:07:22

分散式鎖

為了保證一個方法在高並行情況下的同一時間只能被同一個執行緒執行,在傳統單體應用單機部署的情況下,可以使用Java並行處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。但是,隨著業務發展的需要,原單體單機部署的系統被演化成分散式系統後,由於分散式系統多執行緒、多程序並且分佈在不同機器上,這將使原單機部署情況下的並行控制鎖策略失效,為了解決這個問題就需要一種跨JVM的互斥機制來控制共用資源的存取,這就是分散式鎖要解決的問題。

應用場景

1、處理效率提升:應用分散式鎖,可以減少重複任務的執行,避免資源處理效率的浪費;

2、資料準確性保障:使用分散式鎖可以放在資料資源的並行存取,避免資料不一致情況,甚至資料損失等。

例如:

分散式任務排程平臺保證任務的冪等性。

分散式全域性id的生成

使用Redis 實現分散式鎖

思路:Redis實現分散式鎖基於SetNx命令,因為在Redis中key是保證是唯一的。所以當多個執行緒同時的建立setNx時,只要誰能夠建立成功誰就能夠獲取到鎖。

Set 命令: 每次 set 時,可以修改原來舊值;

SetNx命令:每次SetNx檢查該 key是否已經存在,如果已經存在的話不會執行任何操作。返回為0 如果已經不存在的話直接新增該key。

1:新增key成功, 0:失敗

獲取鎖的時候:當多個執行緒同時建立SetNx k,只要誰能夠建立成功誰就能夠獲取到鎖。

釋放鎖:可以對該key設定一個有效期可以避免死鎖的現象。

單機版Redis實現分散式鎖

使用原生Jedis實現

1、增加maven依賴

<dependency>
   <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

2、編寫Jedis連線Redis工具類

public class RedisClientUtil {
    //protected static Logger logger = Logger.getLogger(RedisUtil.class);
    private static String IP = "www.kaicostudy.com";
    //Redis的埠號
    private static int PORT = 6379;
    //可用連線範例的最大數目,預設值為8;
    //如果賦值為-1,則表示不限制;如果pool已經分配了maxActive個jedis範例,則此時pool的狀態為exhausted(耗盡)。
    private static int MAX_ACTIVE = 100;
    //控制一個pool最多有多少個狀態為idle(空閒的)的jedis範例,預設值也是8。
    private static int MAX_IDLE = 20;
    //等待可用連線的最大時間,單位毫秒,預設值為-1,表示永不超時。如果超過等待時間,則直接丟擲JedisConnectionException;
    private static int MAX_WAIT = 3000;
    private static int TIMEOUT = 3000;
    //在borrow一個jedis範例時,是否提前進行validate操作;如果為true,則得到的jedis範例均是可用的;
    private static boolean TEST_ON_BORROW = true;
    //在return給pool時,是否提前進行validate操作;
    private static boolean TEST_ON_RETURN = true;
    private static JedisPool jedisPool = null;
    /**
     * redis過期時間,以秒為單位
     */
    public final static int EXRP_HOUR = 60 * 60; //一小時
    public final static int EXRP_DAY = 60 * 60 * 24; //一天
    public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一個月
    /**
     * 初始化Redis連線池
     */
    private static void initialPool() {
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(MAX_ACTIVE);
            config.setMaxIdle(MAX_IDLE);
            config.setMaxWaitMillis(MAX_WAIT);
            config.setTestOnBorrow(TEST_ON_BORROW);
            jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "123456");
        } catch (Exception e) {
            //logger.error("First create JedisPool error : "+e);
            e.getMessage();
        }
    }
    /**
     * 在多執行緒環境同步初始化
     */
    private static synchronized void poolInit() {
        if (jedisPool == null) {
            initialPool();
        }
    }
    /**
     * 同步獲取Jedis範例
     *
     * @return Jedis
     */
    public synchronized static Jedis getJedis() {
        if (jedisPool == null) {
            poolInit();
        }
        Jedis jedis = null;
        try {
            if (jedisPool != null) {
                jedis = jedisPool.getResource();
            }
        } catch (Exception e) {
            e.getMessage();
            // logger.error("Get jedis error : "+e);
        }
        return jedis;
    }
    /**
     * 釋放jedis資源
     *
     * @param jedis
     */
    public static void returnResource(final Jedis jedis) {
        if (jedis != null && jedisPool != null) {
            jedisPool.returnResource(jedis);
        }
    }
    public static Long sadd(String key, String... members) {
        Jedis jedis = null;
        Long res = null;
        try {
            jedis = getJedis();
            res = jedis.sadd(key, members);
        } catch (Exception e) {
            //logger.error("sadd  error : "+e);
            e.getMessage();
        }
        return res;
    }
}

3、編寫Redis鎖的工具類

public class RedisLock {
    private static final int setnxSuccss = 1;
    /**
     * 獲取鎖
     *
     * @param lockKey        定義鎖的key
     * @param notLockTimeOut 沒有獲取鎖的超時時間
     * @param lockTimeOut    使用鎖的超時時間
     * @return
     */
    public String getLock(String lockKey, int notLockTimeOut, int lockTimeOut) {
        // 獲取Redis連線
        Jedis jedis = RedisClientUtil.getJedis();
        // 定義沒有獲取鎖的超時時間
        Long endTimeOut = System.currentTimeMillis() + notLockTimeOut;
        while (System.currentTimeMillis() < endTimeOut) {
            String lockValue = UUID.randomUUID().toString();
            // 如果在多執行緒情況下誰能夠setnx 成功返回0 誰就獲取到鎖
            if (jedis.setnx(lockKey, lockValue) == setnxSuccss) {
                jedis.expire(lockKey, lockTimeOut / 1000);
                return lockValue;
            }
            // 否則情況下 在超時時間內繼續迴圈
        }
        try {
            if (jedis != null) {
                jedis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 釋放鎖 其實就是將該key刪除
     *
     * @return
     */
    public Boolean unLock(String lockKey, String lockValue) {
        Jedis jedis = RedisClientUtil.getJedis();
        // 確定是對應的鎖 ,才刪除
        if (lockValue.equals(jedis.get(lockKey))) {
            return jedis.del(lockKey) > 0 ? true : false;
        }
        return false;
    }
}

4、測試方法

 private RedisLock redisLock = new RedisLock();
 private String lockKey = "kaico_lock";
 /**
  * 測試Jedis實現分散式鎖
  * @return
  */
 @GetMapping("/restLock1")
 public String restLock1(){
     // 1.獲取鎖
     String lockValue = redisLock.getLock(lockKey, 5000, 5000);
     if (StringUtils.isEmpty(lockValue)) {
         System.out.println(Thread.currentThread().getName() + ",獲取鎖失敗!");
         return "獲取鎖失敗";
     }
     // 2.獲取鎖成功執行業務邏輯
     System.out.println(Thread.currentThread().getName() + ",獲取成功,lockValue:" + lockValue);
     // 3.釋放lock鎖
     redisLock.unLock(lockKey, lockValue);
     return "";
 }

使用Springboot實現

依賴於之前的專案

1、編寫鎖的工具類方法

@Component
public class SpringbootRedisLockUtil {
    @Autowired
    public RedisTemplate redisTemplate;
    //    解鎖原子性操作指令碼
    public static final String unlockScript="if redis.call("get",KEYS[1]) == ARGV[1]n"
            + "thenn"
            + "    return redis.call("del",KEYS[1])n"
            + "elsen"
            + "    return 0n"
            + "end";
    /**
     * 加鎖,有阻塞
     * @param name
     * @param expire
     * @param timeout
     * @return
     */
    public String lock(String name, long expire, long timeout) throws UnsupportedEncodingException {
        long startTime=System.currentTimeMillis();
        String token;
        do{
            token=tryLock(name,expire);
            if(token==null){
                //設定等待時間,若等待時間過長則獲取鎖失敗
                if((System.currentTimeMillis()-startTime)>(timeout-50)){
                    break;
                }
                try {
                    Thread.sleep(50);//try it again per 50
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }while (token==null);
        return token;
    }
    /**
     * 解鎖
     * @param name
     * @param token
     * @return
     */
    public Boolean unlock(String name, String token) throws UnsupportedEncodingException {
        byte[][] keyArgs=new byte[2][];
        keyArgs[0]= name.getBytes(Charset.forName("UTF-8"));
        keyArgs[1]= token.getBytes(Charset.forName("UTF-8"));
        RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
        RedisConnection connection = connectionFactory.getConnection();
        try{
            Long result = connection.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keyArgs);
            if(result!=null&&result>0){
                return true;
            }

        }finally {
            RedisConnectionUtils.releaseConnection(connection,connectionFactory);
        }
        return false;
    }
    /**
     * 加鎖,無阻塞
     * @param name
     * @param expire
     * @return
     */
    public String tryLock(String name, long expire) throws UnsupportedEncodingException {
        String token= UUID.randomUUID().toString();
        RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
        RedisConnection connection = connectionFactory.getConnection();
        try{
            Boolean result = connection.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),
                    Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
            if(result!=null&&result){
                return token;
            }
        }
        finally {
            RedisConnectionUtils.releaseConnection(connection,connectionFactory);
        }
        return null;
    }
}

2、測試類

		@Autowired
    private SpringbootRedisLockUtil springbootRedisLockUtil;
    @PostMapping("/restLock1")
    public void restLock2() throws UnsupportedEncodingException {
        String token;
        token=springbootRedisLockUtil.lock(Thread.currentThread().getName(),1000,11000);
        if(token!=null){
            System.out.println("我拿到鎖了哦!");
        }
        else{
            System.out.println("我沒有拿到鎖!");
        }
        springbootRedisLockUtil.unlock(Thread.currentThread().getName(),token);
    }

到此這篇關於Redis超詳細分析分散式鎖的文章就介紹到這了,更多相關Redis分散式鎖內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com