分布式鎖與數(shù)據(jù)庫事務問題記錄

? 前段時間爆价,做了一個線上會議室預約的項目,需求是這樣的:有500個會議室媳搪,支持并發(fā)預約铭段,且會議不能跨天,并且要求會議越離散越好秦爆。

? 這個需求首先會議室預約時間不能沖突序愚,而且還需要滿足會議時間間隔越大越好,同時還需要支持并發(fā)預約等限。因此設計了一個會議室分配算法爸吮,采用最優(yōu)離散分配(具體可以看前面的博客),而且需要支持并發(fā)預約望门,因為服務是一個多臺機器組合的集群系統(tǒng)形娇,因此考慮分布式鎖。同時會議預約成功情況下筹误,需要修改數(shù)據(jù)庫數(shù)據(jù)桐早,因此考慮數(shù)據(jù)庫事務,保證數(shù)據(jù)的一致性纫事。

? 考慮到預約會議是按照天為單位的勘畔,在分布式加鎖的時候所灸,可以按照當天的日期作為Key的一部分進行鎖定丽惶。

? 具體的代碼如下:

@Transactional(rollbackFor = Exception.class)
  public MeetingOnlineRoomBookingDetail assignOnlineRoom(Date startTime, Date endTime) throws Exception {
      String key = "assign_room_lock";
      SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
      String key = "assign_room_lock_" + format.format(startTime);
      MeetingOnlineRoomBookingDetail detail;
      //加鎖
      String lock = distributedLock.lock(key, 10, TimeUnit.SECONDS);
      try {
          Optional<Long> room = meetingOnlineRoomService.queryAssignableMeetingOnlineRoom(startTime, endTime);
          Preconditions.checkArgument(room.isPresent(), "會議室已全部分配完成,請更換預定時間");

          MeetingOnlineRoom meetingOnlineRoom = meetingOnlineRoomMapper.selectById(room.get());
          String password = UUID.randomUUID().toString().substring(0, 15);
          //TODO 調用zoom分配接口

          detail = new MeetingOnlineRoomBookingDetail()
                  .setStartTime(startTime)
                  .setEndTime(endTime)
                  .setRoomId(room.get())
                  .setZoomId(meetingOnlineRoom.getZoomId())
                  .setPassword(password);

          this.saveMeetingOnlineRoomBookingDetail(detail);
      } catch (IllegalArgumentException e) {
          log.error("assignOnlineRoom IllegalArgumentException:", e);
          throw new BusinessException(ApiCode.NOT_FOUND.getCode(), e.getMessage());
      } catch (Exception e) {
          log.error("assignOnlineRoom Exception:", e);
          throw new BusinessException(500, "網絡異常爬立,請稍后重試");
      }finally {
          distributedLock.release(key, lock);
      }
      return detail;
  }

? 初看上面的代碼沒問題钾唬,而且在使用單線程接口測試的情況下也正常。但是當開啟100個線程侠驯,隨機預約一個月內的會議時抡秆,發(fā)現(xiàn)了同一個會議時,預約的會議時間重復了吟策。

是什么原因導致會議室被重復預約了呢儒士?第一個想法是不是分布式鎖出現(xiàn)了問題,因此

首先檩坚,對于分布式鎖進行了測試着撩,發(fā)現(xiàn)是正常诅福,能夠阻斷其他同一天預約的會議。具體代碼如下:

@Component("distributedLock")
public class DistributedLock {

    /**
     * 默認的超時時間為20s
     */
    private static final long DEFAULT_MILLISECOND_TIMEOUT = 20000L;

    public final static Long TIMEOUT = 10000L;

    private static final String LOCK_PREFIX = "distribute_lock_";

    private static final long LOCK_EXPIRE = 1000L;

    /**
     * redis的字符串類型模板
     */
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 釋放鎖的lua腳本
     */
    private DefaultRedisScript<Long> releaseLockScript;


    public DistributedLock(StringRedisTemplate stringRedisTemplate) {
        this.releaseLockScript = new DefaultRedisScript<>();
        this.releaseLockScript.setResultType(Long.class);
        this.releaseLockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/release_lock.lua")));
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * key為null或空直接拋出異常
     */
    private void ifEmptyThrowException(String key) {
        int keyLen;
        if (key == null || (keyLen = key.length()) == 0) {
            throw new IllegalArgumentException("key is not null and empty!");
        }
        for (int i = 0; i < keyLen; i++) {
            if (!Character.isWhitespace(key.charAt(i))) {
                return;
            }
        }
        throw new IllegalArgumentException("key is not null and not empty!");
    }

    /**
     * 加鎖
     *
     * @param key 鍵
     * @return value key對應的值, 釋放鎖時需要用到
     */
    public String lock(String key) {
        return this.lock(key, DEFAULT_MILLISECOND_TIMEOUT);
    }

    /**
     * 加鎖
     *
     * @param key 鍵
     * @param time 超時時間
     * @param unit 時間單位
     * @return value key對應的值, 釋放鎖時需要用到
     */
    public String lock(String key, long time, TimeUnit unit) {
        return this.lock(key, unit.toMillis(time));
    }

    /**
     * 加鎖
     *
     * @param key 鍵
     * @param msTimeout 超時時間, 單位為ms
     * @return value key對應的值, 釋放鎖時需要用到
     */
    public String lock(String key, long msTimeout) {
        ifEmptyThrowException(key);
        // 值
        String value = UUID.randomUUID().toString();
        // 是否是第一次嘗試獲取鎖
        boolean isFirst = true;
        // 命令執(zhí)行的結果
        Boolean result = false;
        do {
            // 不是第一次嘗試獲取鎖則要睡眠20ms
            if (!isFirst) {
                try {
                    Thread.sleep(20);
                } catch (Exception e) {
                    log.error("DistributedLock lock sleep error", e);
                }
            } else {
                isFirst = false;
            }
            result = stringRedisTemplate.opsForValue().setIfAbsent(key, value, msTimeout, TimeUnit.MILLISECONDS);
        } while (result == null || Boolean.FALSE.equals(result));
        return value;
    }



    /**
     * 釋放鎖
     *
     * @param key 鍵
     * @param value 值
     */
    public void release(String key, String value) {
        ifEmptyThrowException(key);
        try {
            stringRedisTemplate.execute(releaseLockScript, Collections.singletonList(key), value);
        } catch (Exception e) {
            log.error("DistributedLock release lock error", e);
        }
    }
}

lua腳本如下:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

在驗證了分布式鎖正常的情況下拖叙,開始思考是什么原因導致的氓润。

最后考慮到一種可能,是否是數(shù)據(jù)庫事務未提交的情況下薯鳍,然后用戶釋放了鎖咖气,由于數(shù)據(jù)庫采用的Mysql,而且數(shù)據(jù)庫事務的隔離級別為可重復讀挖滤。

隔離級別 第一類丟失更新 第二類丟失更新 臟讀 不可重復讀 幻讀
SERIALIZABLE (串行化) 避免 避免 避免 避免 避免
REPEATABLE READ(可重復讀) 避免 避免 避免 避免 允許
READ COMMITTED (讀已提交) 避免 允許 避免 允許 允許
READ UNCOMMITTED(讀未提交) 避免 允許 允許 允許 允許

從表中可以看出崩溪,可重復讀會產生幻讀的情況。下面解析下出現(xiàn)幻讀的過程:

? 假設斩松,A打算預約2020-06-11 18:00 - 2020-06-11 19:00 時段的會議悯舟,B也打算預約了2020-06-11 18:00 -2020-06-11 19:00時段的會議。

然后A先搶占到了分布式鎖砸民,B則等待A鎖的釋放抵怎。假設A發(fā)現(xiàn)會議室Id=1的這個時間段未被預約,因此預約這個時段岭参,預約完成后反惕,A釋放鎖,但是A的事務還未來得及提交演侯。

? 由于鎖已經釋放了姿染,因此B也能進行預約,B也進行加鎖秒际,然后B也發(fā)現(xiàn)會議室id=1的這個時段也沒有被預約悬赏,因此B也預約的該時段。

? 此時A提交了事務娄徊,然后B釋放鎖闽颇,并且也提交了事務。最終發(fā)現(xiàn)會議室Id=1的寄锐,同時被2場會議預約了成功了兵多。

? 其實解決這個問題也很簡單,將加鎖的的操作橄仆,放在事務的外層剩膘,保證事務提交成功后,才能進行鎖的釋放盆顾,后面也是這樣修改的怠褐,最終測試結果再也沒有出現(xiàn)時間沖突的問題了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末您宪,一起剝皮案震驚了整個濱河市奈懒,隨后出現(xiàn)的幾起案子具温,更是在濱河造成了極大的恐慌,老刑警劉巖筐赔,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铣猩,死亡現(xiàn)場離奇詭異,居然都是意外死亡茴丰,警方通過查閱死者的電腦和手機达皿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贿肩,“玉大人峦椰,你說我怎么就攤上這事√妫” “怎么了汤功?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長溜哮。 經常有香客問我滔金,道長,這世上最難降的妖魔是什么茂嗓? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任餐茵,我火速辦了婚禮,結果婚禮上述吸,老公的妹妹穿的比我還像新娘忿族。我一直安慰自己,他們只是感情好蝌矛,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布道批。 她就那樣靜靜地躺著,像睡著了一般入撒。 火紅的嫁衣襯著肌膚如雪隆豹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天衅金,我揣著相機與錄音噪伊,去河邊找鬼。 笑死氮唯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的姨伟。 我是一名探鬼主播惩琉,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼夺荒!你這毒婦竟也來了瞒渠?” 一聲冷哼從身側響起良蒸,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伍玖,沒想到半個月后嫩痰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡窍箍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年串纺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椰棘。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡纺棺,死狀恐怖,靈堂內的尸體忽然破棺而出邪狞,到底是詐尸還是另有隱情祷蝌,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布帆卓,位于F島的核電站巨朦,受9級特大地震影響,放射性物質發(fā)生泄漏剑令。R本人自食惡果不足惜罪郊,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尚洽。 院中可真熱鬧悔橄,春花似錦、人聲如沸腺毫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潮酒。三九已至睛挚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間急黎,已是汗流浹背扎狱。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留勃教,地道東北人淤击。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像故源,于是被迫代替她去往敵國和親污抬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355