Redis分布式鎖

設(shè)計思路
基于 Redis 的 Setnx 命令:在指定的 key 不存在時,為 key 設(shè)置指定的值。

具體思路和實現(xiàn)步驟,詳見代碼。

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Redis分布式鎖
 *
 * @author Alisallon
 * Created on 2021/4/25 9:34.
 */
@Component
public class RedisLock {
    /**
     * 保存鎖以及過期時間,用于解決釋放鎖造成的問題
     */
    private static final Map<String, Long> LOCK_MAP = new HashMap<>();
    private final StringRedisTemplate redisTemplate;

    public RedisLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 嘗試獲取分布式鎖(加鎖)
     *
     * @param lock   鎖名稱
     * @param expire 鎖過期時間
     * @return 是否獲取到
     */
    public boolean lock(String lock, long expire) {
        try {
            AtomicLong expireAt = new AtomicLong();
            Object result = redisTemplate.execute((RedisCallback<Object>) connection -> {
                // 嘗試給鎖設(shè)置值(保存的是未來的過期時間)
                expireAt.set(System.currentTimeMillis() + expire + 1);
                Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt.get()).getBytes());
                if (Optional.ofNullable(acquire).orElse(false)) {
                    // 設(shè)置值成功,即獲取鎖成功(加鎖成功)
                    return true;
                }
                // 設(shè)置值失敗,即沒有獲取到鎖,獲取鎖的對應(yīng)的值(過期時間)
                byte[] value = connection.get(lock.getBytes());
                if (Objects.nonNull(value) && value.length > 0) {
                    // 獲取鎖的對應(yīng)的值(過期時間)成功
                    long expireTime = Long.parseLong(new String(value));
                    // 判斷鎖是否過期
                    if (expireTime < System.currentTimeMillis()) {
                        // 鎖已經(jīng)過期,表示沒有其他程序在占用鎖(不能排除占用鎖的程序,因為邏輯復(fù)雜造成執(zhí)行時間太長或者程序掛掉了,還沒來得及釋放鎖)
                        // 這里為了防止死鎖,直接對已過期的鎖重新設(shè)置過期時間,同時獲得設(shè)置新值之前的舊過期時間
                        expireAt.set(System.currentTimeMillis() + expire + 1);
                        byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(expireAt.get()).getBytes());
                        if (Optional.ofNullable(oldValue).isPresent()) {
                            // 重新判斷設(shè)置新值之前的舊過期時間是否真的過期,因為可能會同時存在多個程序在競爭該鎖
                            // 如果oldValue還未過期,說明該鎖被其他程序搶走了
                            // 如果oldValue已過期,說明該鎖未被占用,當(dāng)前程序可以獲得該鎖
                            return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
                        }
                    }
                }
                // 鎖的對應(yīng)的值失敗,返回獲取鎖失敗
                return false;
            });
            if (Optional.ofNullable(result).map(t -> (Boolean) result).orElse(false)) {
                // 獲取鎖成功
                // 在當(dāng)前程序中保存該鎖和過期時間,會在釋放鎖時使用
                LOCK_MAP.put(lock, expireAt.longValue());
                return true;
            }
            // 獲取鎖失敗
            return false;
        } catch (Exception e) {
            // 獲取鎖異常,返回獲取鎖失敗
            return false;
        }
    }

    /**
     * 釋放鎖
     * 必須和上面的lock方法成對出現(xiàn)
     * 需要注意,如果lock方法后面的執(zhí)行邏輯里有try-catch,一定要在finally中釋放鎖
     *
     * @param lock 鎖名稱
     */
    public void release(String lock) {
        // 當(dāng)當(dāng)前占用鎖的程序因為邏輯復(fù)雜造成執(zhí)行時間太長(執(zhí)行正常無誤),超過了鎖的超時時間,這時鎖可能會被其他程序搶走
        // 如果直接delete,可能會把其他程序搶走的鎖釋放,并且被另一個程序搶走,這會造成多個程序同一種業(yè)務(wù)邏輯并發(fā)執(zhí)行,可能會造成數(shù)據(jù)不一致的問題
        // 為了解決這個問題,引入了LOCK_MAP
        // 如果LOCK_MAP中存在該鎖,需要判斷該鎖的超時時間
        if (LOCK_MAP.containsKey(lock)) {
            // 已存在該鎖
            long expireAt = LOCK_MAP.get(lock);
            if (expireAt <= System.currentTimeMillis()) {
                // 該鎖已過期,此時無需手動釋放鎖,因為該鎖可能已經(jīng)被其他程序搶走了
                // 如果釋放了鎖,可能釋放的不是本程序獲得的鎖,而是別的程序已搶走的鎖,就可能會出現(xiàn)上面說的數(shù)據(jù)不一致的問題
                return;
            }
            // 該鎖還未過期,可以釋放鎖,因為能主動調(diào)用release方法的一定是已獲得鎖的程序
        }
        try {
            // 釋放鎖
            redisTemplate.delete(lock);
            // 當(dāng)前程序移除鎖
            LOCK_MAP.remove(lock);
        } catch (Exception e) {
            // 釋放鎖異常,可以無視
        }
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 大家好,我是walking,原文首發(fā)于公眾號編程大道。感謝你打開這篇文章,請認(rèn)真閱讀下去吧。今天我們聊聊redis...
    BiggerBoy閱讀 714評論 0 8
  • 一 前言 我為什么要寫分布式鎖呢,最近工作中寫一個查詢接口,因為邏輯復(fù)雜,不希望用戶不停的點擊,需要過濾掉重復(fù)的請...
    漫慢行閱讀 341評論 0 0
  • 基于Spring Boot AOP 實現(xiàn)分布式鎖 AOP AOP 的全稱為 Aspect Oriented Pro...
    卡斯特梅的雨傘閱讀 375評論 0 1
  • 為什么需要分布式鎖? 在傳統(tǒng)單體應(yīng)用單機部署的情況下,可以使用Java并發(fā)相關(guān)的鎖,如ReentrantLcok或...
    空語閱讀 371評論 0 0
  • 基于單Redis節(jié)點的分布式鎖 組件依賴 首先我們要通過Maven引入Jedis開源組件,在pom.xml文件加入...
    GeekerLou閱讀 876評論 0 7

友情鏈接更多精彩內(nèi)容