精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

分布式鎖,原來這么簡單!

原創(chuàng) 精選
數(shù)據(jù)庫 Redis
現(xiàn)在的服務(wù)往往都是多節(jié)點(diǎn),在一些特定的場景下容易產(chǎn)生并發(fā)問題,比如扣減庫存,送完即止活動(dòng),中臺(tái)的批量導(dǎo)入(有唯一校驗(yàn)要求)等等。這時(shí),我們可以通過分布式鎖解決這些問題。

作者 | 蔡柱梁

審校 | 重樓

目錄

  1. 分布式鎖介紹
  2. 如何實(shí)現(xiàn)分布式鎖
  3. 實(shí)現(xiàn)分布式鎖

1 分布式鎖介紹

現(xiàn)在的服務(wù)往往都是多節(jié)點(diǎn),在一些特定的場景下容易產(chǎn)生并發(fā)問題,比如扣減庫存,送完即止活動(dòng),中臺(tái)的批量導(dǎo)入(有唯一校驗(yàn)要求)等等。這時(shí),我們可以通過分布式鎖解決這些問題。

2 如何實(shí)現(xiàn)分布式鎖

實(shí)現(xiàn)的方式有很多種,如:

  • 基于 MySQL 等數(shù)據(jù)庫實(shí)現(xiàn)
  • 基于 ZooKeeper 實(shí)現(xiàn)
  • 基于 Redis 實(shí)現(xiàn)不管采用什么技術(shù)棧實(shí)現(xiàn),但是邏輯流程都是大體不差的。下面是筆者自己在工作中基于Redis 實(shí)踐過的流程圖

3 實(shí)現(xiàn)分布式鎖

其實(shí)可以不用自己手寫,現(xiàn)在有一個(gè)中間件Redisson 相當(dāng)好用,十分推薦。這里的實(shí)現(xiàn)更多是用于學(xué)習(xí)。

3.1 Redis 是單節(jié)點(diǎn)的情況下實(shí)現(xiàn)的分布式鎖

需要使用分布式鎖的業(yè)務(wù)代碼如下

package com.example.demo.test.utils;

import com.example.demo.utils.RedisLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author CaiZhuliang
 * @date 2023/8/31
 */
@Slf4j
@SpringBootTest
public class RedisLockUtilTest {
 @Autowired
 private RedisLockUtil redisLockUtil;

 @Test
 public void simpleLockTest() {
 String key = "redis:lock:" + System.currentTimeMillis();
 boolean result = redisLockUtil.lock(key, 8_000L);
 if (result) {
 try {
 // do something
 } catch (Exception e) {
 log.error("simpleLockTest - 系統(tǒng)異常!", e);
 } finally {
 boolean unlock = redisLockUtil.unlock(key);
 if (!unlock) {
 log.error("simpleLockTest - 釋放鎖失敗,key : {}", key);
 }
 }
 }
 }
}

分布式鎖工具類代碼如下

package com.example.demo.utils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author CaiZhuliang
 * @date 2023/8/31
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisLockUtil {
 private static final ScheduledExecutorService EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(50,
 new BasicThreadFactory.Builder()
 .namingPattern("redisLockUtil-schedule-pool-%d")
 .daemon(true)
 .build());

 private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

 private final RedisTemplate<String, String> redisTemplate;

 /**
 * 釋放鎖
 * <p>必須和RedisLockUtil#simpleLock是同一個(gè)線程</p>
 * @param key 需要釋放鎖的key
 * @return true-成功 false-失敗
 */
 public boolean releaseSimpleLock(String key) {
 String token = THREAD_LOCAL.get();
 try {
 String remoteToken = redisTemplate.opsForValue().get(key);
 if (!token.equals(remoteToken)) {
 // 當(dāng)前線程不再持有鎖
 return false;
 }
 // 是自己持有鎖才能釋放
 return Boolean.TRUE.equals(redisTemplate.delete(key));
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 釋放鎖發(fā)生異常,key : {}", key, e);
 return false;
 } finally {
 THREAD_LOCAL.remove();
 }
 }

 /**
 * 這個(gè)方法不考慮Redis的集群架構(gòu),不考慮腦裂問題,當(dāng)只有一個(gè)Redis來考慮。
 * @param key 需要上鎖的key
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @return true-成功 false-失敗
 */
 public boolean simpleLock(String key, Long expireTime) {
 if (StringUtils.isBlank(key)) {
 log.warn("非cluster模式簡單分布式鎖 - key is blank");
 return false;
 }
 if (null == expireTime || expireTime <= 0) {
 expireTime = 0L;
 }
 String token = UUID.randomUUID().toString();
 // 續(xù)約周期,單位納秒
 long renewPeriod = expireTime / 2 * 1000_000;
 try {
 // 設(shè)置鎖
 Boolean result = redisTemplate.opsForValue().setIfAbsent(key, token, expireTime, TimeUnit.MILLISECONDS);
 if (Boolean.FALSE.equals(result)) {
 return false;
 }
 // 上鎖成功后將令牌綁定當(dāng)前線程
 THREAD_LOCAL.set(token);
 if (renewPeriod > 0) {
 // 續(xù)約任務(wù)
 renewTask(key, token, expireTime, renewPeriod);
 }
 return true;
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 上鎖失敗。", e);
 THREAD_LOCAL.remove();
 return false;
 }
 }

 /**
 * 鎖續(xù)約任務(wù)
 * @param key 需要續(xù)命的key
 * @param token 成功獲鎖的線程持有的令牌
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @param renewPeriod 續(xù)約周期,單位:納秒
 */
 private void renewTask(String key, String token, long expireTime, long renewPeriod) {
 EXECUTOR_SERVICE.schedule(() -> {
 ValueOperations<String, String> valueOperator = redisTemplate.opsForValue();
 String val = valueOperator.get(key);
 if (token.equals(val)) {
 // 是自己持有鎖才能續(xù)約
 try {
 Boolean result = valueOperator.setIfPresent(key, token, expireTime, TimeUnit.MILLISECONDS);
 if (Boolean.TRUE.equals(result)) {
 // 續(xù)約成功
 log.debug("非cluster模式簡單分布式鎖 - 鎖續(xù)約成功,key : {}", key);
 // 開啟下一次續(xù)約任務(wù)
 renewTask(key, token, expireTime, renewPeriod);
 } else {
 log.error("非cluster模式簡單分布式鎖 - 鎖續(xù)約失敗,key : {}", key);
 }
 } catch (Exception e) {
 // 這里異常是拋不出去的,所以需要 catch 打印
 log.error("非cluster模式簡單分布式鎖 - 鎖續(xù)約發(fā)生異常,key : {}", key, e);
 }
 } else {
 log.error("非cluster模式簡單分布式鎖 - 鎖續(xù)約失敗,不再持有token,key : {}", key);
 }
 }, renewPeriod, TimeUnit.NANOSECONDS);
 }
}

這就是一個(gè)最簡單的實(shí)現(xiàn)方式。不過這里存在著許多問題:

  • 續(xù)約任務(wù)

這里判斷是否持有令牌和續(xù)約這兩個(gè)動(dòng)作不在同一個(gè)事務(wù)里,可能發(fā)生覆蓋現(xiàn)象。假設(shè)A線程判斷自己持有令牌,但是一直沒有請求 Redis 導(dǎo)致鎖過期。B線程成功獲鎖,這時(shí)A線程往下執(zhí)行 Redis 請求,結(jié)果A線程搶了B線程的鎖。

  • 釋放鎖

這里判斷是否持有令牌和刪除key這兩個(gè)動(dòng)作不在同一個(gè)事務(wù)里,可能出現(xiàn)誤刪現(xiàn)象。假設(shè)A線程現(xiàn)在要釋放鎖,通過了令牌判斷,準(zhǔn)備刪除 key 但是還沒執(zhí)行。這時(shí) key 過期了,B線程成功獲鎖。接著A線程執(zhí)行刪除 key 導(dǎo)致了 B 線程的鎖被刪除。

因此,判斷持有令牌與續(xù)約/刪除key這兩個(gè)動(dòng)作是需要原子性的,我們可以通過 lua 來實(shí)現(xiàn)。

擴(kuò)展,了解管道與 lua 的區(qū)別

  • pipeline(多用于命令簡單高效,無關(guān)聯(lián)的場景)

優(yōu)點(diǎn):使用簡單,有效減少網(wǎng)絡(luò)IO

缺點(diǎn):本質(zhì)還是發(fā)送命令請求Redis 服務(wù),如果效率過低,就會(huì)阻塞 Redis,導(dǎo)致 Redis 無法處理其他請求

  • lua(多用于命令復(fù)雜,命令間有關(guān)聯(lián)的場景)

優(yōu)點(diǎn):

  1. Redis 支持 lua 腳本,Redis 服務(wù)執(zhí)行 lua 的同時(shí)是可以處理別的請求的,不會(huì)產(chǎn)生阻塞
  2. 命令都在腳本中,有效減少網(wǎng)絡(luò)IO
  3. 具有原子性

缺點(diǎn):

有一定的學(xué)習(xí)成本

3.1.1 使用 lua 進(jìn)行優(yōu)化

RedisLockUtil 代碼如下:

package com.example.demo.utils;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author CaiZhuliang
 * @date 2023/8/31
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisLockUtil {
 private static final ScheduledExecutorService EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(50,
 new BasicThreadFactory.Builder()
 .namingPattern("redisLockUtil-schedule-pool-%d")
 .daemon(true)
 .build());

 private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
 private static final String SUCCESS = "1";
 /**
 * 允許當(dāng)前token續(xù)約
 */
 private static final Integer CAN_RENEW = 0;
 /**
 * 記錄token的狀態(tài),0-可以續(xù)約,其他情況均不能續(xù)約
 */
 private static final Map<String, Integer> TOKEN_STATUS = Maps.newConcurrentMap();

 private final RedisTemplate<String, String> redisTemplate;

 /**
 * 釋放鎖,這個(gè)方法與 com.example.demo.utils.RedisLockUtil#simpleLock(java.lang.String, java.lang.Long) 配對。
 * <p>必須和RedisLockUtil#simpleLock是同一個(gè)線程</p>
 * @param key 需要釋放鎖的key
 * @return true-成功 false-失敗
 */
 public boolean releaseSimpleLock(String key) {
 String token = THREAD_LOCAL.get();
 if (null != token) {
 TOKEN_STATUS.put(token, 1);
 }
 try {
 String lua = "if (redis.call('get', KEYS[1]) == ARGV[1]) " +
 "then redis.call('expire', KEYS[1], 0) return '1' end " +
 "return '0'";
 DefaultRedisScript<String> luaScript = new DefaultRedisScript<>(lua, String.class);
 String result = redisTemplate.execute(luaScript, Lists.newArrayList(key), token);
 log.info("非cluster模式簡單分布式鎖 - 釋放key: {}, result : {}, token : {}", key, result, token);
 return SUCCESS.equals(result);
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 釋放鎖發(fā)生異常,key : {}", key, e);
 return false;
 } finally {
 THREAD_LOCAL.remove();
 if (null != token) {
 TOKEN_STATUS.remove(token);
 }
 }
 }

 /**
 * 簡單分布式鎖實(shí)現(xiàn),續(xù)約周期是 expireTime 的一半。舉個(gè)例子, expireTime = 8000,那么鎖續(xù)約將會(huì)是每 4000 毫秒續(xù)約一次
 * <p>這個(gè)方法不考慮Redis的集群架構(gòu),不考慮腦裂問題,當(dāng)只有一個(gè) Redis來考慮。</p>
 * <p>這個(gè)方法使用 com.example.demo.utils.RedisLockUtil#releaseSimpleLock(java.lang.String) 來釋放鎖</p>
 * @param key 需要上鎖的key
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @return true-成功 false-失敗
 */
 public boolean simpleLock(String key, Long expireTime) {
 if (StringUtils.isBlank(key)) {
 log.warn("非cluster模式簡單分布式鎖 - key is blank");
 return false;
 }
 if (null == expireTime || expireTime <= 0) {
 expireTime = 0L;
 }
 // 續(xù)約周期,單位納秒
 long renewPeriod = expireTime / 2 * 1000_000;
 try {
 String token = System.currentTimeMillis() + ":" + UUID.randomUUID();
 // 設(shè)置鎖
 Boolean result = redisTemplate.opsForValue().setIfAbsent(key, token, expireTime, TimeUnit.MILLISECONDS);
 if (Boolean.FALSE.equals(result)) {
 return false;
 }
 log.info("非cluster模式簡單分布式鎖 - 上鎖成功,key : {}, token : {}", key, token);
 // 上鎖成功后將令牌綁定當(dāng)前線程
 THREAD_LOCAL.set(token);
 TOKEN_STATUS.put(token, 0);
 if (renewPeriod > 0) {
 // 續(xù)約任務(wù)
 renewTask(key, token, expireTime, renewPeriod);
 }
 return true;
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 上鎖發(fā)生異常,key : {}", key, e);
 String token = THREAD_LOCAL.get();
 if (StringUtils.isNotBlank(token)) {
 if (!releaseSimpleLock(key)) {
 log.warn("非cluster模式簡單分布式鎖 - 釋放鎖發(fā)生失敗,key : {}, token : {}", key, token);
 }
 }
 return false;
 }
 }

 /**
 * 鎖續(xù)約任務(wù)
 * @param key 需要續(xù)命的key
 * @param token 成功獲鎖的線程持有的令牌
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @param renewPeriod 續(xù)約周期,單位:納秒
 */
 private void renewTask(String key, String token, long expireTime, long renewPeriod) {
 if (CAN_RENEW.equals(TOKEN_STATUS.get(token))) {
 EXECUTOR_SERVICE.schedule(() -> {
 if (CAN_RENEW.equals(TOKEN_STATUS.get(token))) {
 try {
 String lua = "if (redis.call('get', KEYS[1]) == ARGV[1]) " +
 "then " +
 " if (redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])) " +
 " then return '1' else return redis.call('get', KEYS[1]) end " +
 "end " +
 "return redis.call('get', KEYS[1])";
 DefaultRedisScript<String> luaScript = new DefaultRedisScript<>(lua, String.class);
 String result = redisTemplate.execute(luaScript, Lists.newArrayList(key), token, String.valueOf(expireTime));
 if (SUCCESS.equals(result)) {
 // 續(xù)約成功
 log.debug("非cluster模式簡單分布式鎖 - 鎖續(xù)約成功,key : {}", key);
 // 開啟下一次續(xù)約任務(wù)
 renewTask(key, token, expireTime, renewPeriod);
 } else {
 // 打印下 result,看下是否因?yàn)椴辉俪钟辛钆茖?dǎo)致的續(xù)約失敗
 log.warn("非cluster模式簡單分布式鎖 - 鎖續(xù)約失敗,key : {}, token : {}, result : {}", key, token, result);
 }
 } catch (Exception e) {
 // 這里異常是拋不出去的,所以需要 catch 打印
 log.error("非cluster模式簡單分布式鎖 - 鎖續(xù)約發(fā)生異常,key : {}", key, e);
 }
 }
 }, renewPeriod, TimeUnit.NANOSECONDS);
 }
 }
}

這里還有一個(gè)問題:如果redis.call('get', KEYS[1]) == ARGV[1] 成立,但是執(zhí)行redis.call('expire', KEYS[1], 0) 失敗,怎么辦?我這里已經(jīng)執(zhí)行了THREAD_LOCAL.remove(),想重復(fù)釋放是不可能的了,但是我這里不能不 remove 或者僅當(dāng) Redis 釋放鎖成功才 remove,這樣存在內(nèi)存泄漏的風(fēng)險(xiǎn)。要怎么處理呢?

這是優(yōu)化后的代碼

package com.example.demo.utils;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author CaiZhuliang
 * @date 2023/8/31
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisSimpleLockUtil {
 private static final ScheduledExecutorService EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(50,
 new BasicThreadFactory.Builder()
 .namingPattern("redisSimpleLockUtil-schedule-pool-%d")
 .daemon(true)
 .build());

 private static final ThreadLocal<String> THREAD_LOCAL_TOKEN = new ThreadLocal<>();
 private static final String SUCCESS = "1";
 /**
 * 允許當(dāng)前token續(xù)約
 */
 private static final Integer CAN_RENEW = 0;
 /**
 * 記錄token的狀態(tài),0-可以續(xù)約,其他情況均不能續(xù)約
 */
 private static final Map<String, Integer> TOKEN_STATUS = Maps.newConcurrentMap();

 private final RedisTemplate<String, String> redisTemplate;

 /**
 * 釋放鎖
 * <p>必須和 RedisSimpleLockUtil#lock 是同一個(gè)線程</p>
 * @param key key 需要釋放鎖的key
 * @param token 持有的令牌
 * @return true-成功 false-失敗
 */
 public boolean releaseLock(String key, String token) {
 if (StringUtils.isBlank(token)) {
 return false;
 }
 TOKEN_STATUS.put(token, 1);
 try {
 String lua = "if (redis.call('get', KEYS[1]) == ARGV[1]) " +
 "then redis.call('expire', KEYS[1], 0) return '1' end " +
 "return '0'";
 DefaultRedisScript<String> luaScript = new DefaultRedisScript<>(lua, String.class);
 String result = redisTemplate.execute(luaScript, Lists.newArrayList(key), token);
 log.info("非cluster模式簡單分布式鎖 - 釋放key: {}, result : {}, token : {}", key, result, token);
 if (SUCCESS.equals(result)) {
 return true;
 }
 String remoteToken = redisTemplate.opsForValue().get(key);
 if (token.equals(remoteToken)) {
 log.warn("非cluster模式簡單分布式鎖 - 釋放鎖失敗,key : {}, token : {}", key, token);
 return false;
 }
 return true;
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 釋放鎖發(fā)生異常,key : {}, token : {}", key, token, e);
 return false;
 } finally {
 THREAD_LOCAL_TOKEN.remove();
 TOKEN_STATUS.remove(token);
 }
 }

 /**
 * 簡單分布式鎖實(shí)現(xiàn),續(xù)約周期是 expireTime 的一半。舉個(gè)例子, expireTime = 8000,那么鎖續(xù)約將會(huì)是每 4000 毫秒續(xù)約一次
 * <p>這個(gè)方法不考慮Redis的集群架構(gòu),不考慮腦裂問題,當(dāng)只有一個(gè)Redis來考慮。</p>
 * @param key 需要上鎖的key
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @return 上鎖成功返回令牌,失敗則返回空串
 */
 public String lock(String key, Long expireTime) {
 if (StringUtils.isBlank(key)) {
 log.warn("非cluster模式簡單分布式鎖 - key is blank");
 return StringUtils.EMPTY;
 }
 if (null == expireTime || expireTime <= 0) {
 expireTime = 0L;
 }
 // 續(xù)約周期,單位納秒
 long renewPeriod = expireTime * 500_000;
 try {
 String token = System.currentTimeMillis() + ":" + UUID.randomUUID();
 // 設(shè)置鎖
 Boolean result = redisTemplate.opsForValue().setIfAbsent(key, token, expireTime, TimeUnit.MILLISECONDS);
 if (Boolean.FALSE.equals(result)) {
 return StringUtils.EMPTY;
 }
 log.info("非cluster模式簡單分布式鎖 - 上鎖成功,key : {}, token : {}", key, token);
 // 上鎖成功后將令牌綁定當(dāng)前線程
 THREAD_LOCAL_TOKEN.set(token);
 TOKEN_STATUS.put(token, 0);
 if (renewPeriod > 0) {
 // 續(xù)約任務(wù)
 log.info("非cluster模式簡單分布式鎖 - 添加續(xù)約任務(wù),key : {}, token : {}, renewPeriod : {}納秒", key, token, renewPeriod);
 renewTask(key, token, expireTime, renewPeriod);
 }
 return token;
 } catch (Exception e) {
 String token = THREAD_LOCAL_TOKEN.get();
 log.error("非cluster模式簡單分布式鎖 - 上鎖發(fā)生異常,key : {}, token : {}", key, token, e);
 return StringUtils.isBlank(token) ? StringUtils.EMPTY : token;
 }
 }

 /**
 * 鎖續(xù)約任務(wù)
 * @param key 需要續(xù)命的key
 * @param token 成功獲鎖的線程持有的令牌
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @param renewPeriod 續(xù)約周期,單位:納秒
 */
 private void renewTask(String key, String token, long expireTime, long renewPeriod) {
 try {
 EXECUTOR_SERVICE.schedule(() -> {
 try {
 String lua = "if (redis.call('get', KEYS[1]) == ARGV[1]) " +
 "then " +
 " if (redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])) " +
 " then return '1' else return redis.call('get', KEYS[1]) end " +
 "end " +
 "return redis.call('get', KEYS[1])";
 DefaultRedisScript<String> luaScript = new DefaultRedisScript<>(lua, String.class);
 String result = redisTemplate.execute(luaScript, Lists.newArrayList(key), token, String.valueOf(expireTime));
 if (SUCCESS.equals(result)) {
 // 續(xù)約成功
 log.debug("非cluster模式簡單分布式鎖 - 鎖續(xù)約成功,key : {}, token : {}", key, token);
 // 這里加判斷是為了減少定時(shí)任務(wù)
 if (CAN_RENEW.equals(TOKEN_STATUS.get(token))) {
 // 開啟下一次續(xù)約任務(wù)
 renewTask(key, token, expireTime, renewPeriod);
 }
 } else {
 // 這里加判斷是為了防止誤打印warn日志
 if (CAN_RENEW.equals(TOKEN_STATUS.get(token))) {
 log.warn("非cluster模式簡單分布式鎖 - 鎖續(xù)約失敗,key : {}, token : {}, result : {}", key, token, result);
 }
 }
 } catch (Exception e) {
 // 這里異常是拋不出去的,所以需要 catch 打印
 log.error("非cluster模式簡單分布式鎖 - 鎖續(xù)約發(fā)生異常,key : {}, token : {}", key, token, e);
 }
 }, renewPeriod, TimeUnit.NANOSECONDS);
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 添加鎖續(xù)約任務(wù)發(fā)生異常,key : {}, token : {}", key, token, e);
 }
 }
}
package com.example.demo.utils;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author CaiZhuliang
 * @date 2023/8/31
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisSimpleLockUtil {
 private static final ScheduledExecutorService EXECUTOR_SERVICE = new ScheduledThreadPoolExecutor(50,
 new BasicThreadFactory.Builder()
 .namingPattern("redisSimpleLockUtil-schedule-pool-%d")
 .daemon(true)
 .build());

 private static final ThreadLocal<String> THREAD_LOCAL_TOKEN = new ThreadLocal<>();
 private static final String SUCCESS = "1";
 /**
 * 允許當(dāng)前token續(xù)約
 */
 private static final Integer CAN_RENEW = 0;
 /**
 * 記錄token的狀態(tài),0-可以續(xù)約,其他情況均不能續(xù)約
 */
 private static final Map<String, Integer> TOKEN_STATUS = Maps.newConcurrentMap();

 private final RedisTemplate<String, String> redisTemplate;

 /**
 * 釋放鎖
 * <p>必須和 RedisSimpleLockUtil#lock 是同一個(gè)線程</p>
 * @param key key 需要釋放鎖的key
 * @param token 持有的令牌
 * @return true-成功 false-失敗
 */
 public boolean releaseLock(String key, String token) {
 if (StringUtils.isBlank(token)) {
 return false;
 }
 TOKEN_STATUS.put(token, 1);
 try {
 String lua = "if (redis.call('get', KEYS[1]) == ARGV[1]) " +
 "then redis.call('expire', KEYS[1], 0) return '1' end " +
 "return '0'";
 DefaultRedisScript<String> luaScript = new DefaultRedisScript<>(lua, String.class);
 String result = redisTemplate.execute(luaScript, Lists.newArrayList(key), token);
 log.info("非cluster模式簡單分布式鎖 - 釋放key: {}, result : {}, token : {}", key, result, token);
 if (SUCCESS.equals(result)) {
 return true;
 }
 String remoteToken = redisTemplate.opsForValue().get(key);
 if (token.equals(remoteToken)) {
 log.warn("非cluster模式簡單分布式鎖 - 釋放鎖失敗,key : {}, token : {}", key, token);
 return false;
 }
 return true;
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 釋放鎖發(fā)生異常,key : {}, token : {}", key, token, e);
 return false;
 } finally {
 THREAD_LOCAL_TOKEN.remove();
 TOKEN_STATUS.remove(token);
 }
 }

 /**
 * 簡單分布式鎖實(shí)現(xiàn),續(xù)約周期是 expireTime 的一半。舉個(gè)例子, expireTime = 8000,那么鎖續(xù)約將會(huì)是每 4000 毫秒續(xù)約一次
 * <p>這個(gè)方法不考慮Redis的集群架構(gòu),不考慮腦裂問題,當(dāng)只有一個(gè)Redis來考慮。</p>
 * @param key 需要上鎖的key
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @return 上鎖成功返回令牌,失敗則返回空串
 */
 public String lock(String key, Long expireTime) {
 if (StringUtils.isBlank(key)) {
 log.warn("非cluster模式簡單分布式鎖 - key is blank");
 return StringUtils.EMPTY;
 }
 if (null == expireTime || expireTime <= 0) {
 expireTime = 0L;
 }
 // 續(xù)約周期,單位納秒
 long renewPeriod = expireTime * 500_000;
 try {
 String token = System.currentTimeMillis() + ":" + UUID.randomUUID();
 // 設(shè)置鎖
 Boolean result = redisTemplate.opsForValue().setIfAbsent(key, token, expireTime, TimeUnit.MILLISECONDS);
 if (Boolean.FALSE.equals(result)) {
 return StringUtils.EMPTY;
 }
 log.info("非cluster模式簡單分布式鎖 - 上鎖成功,key : {}, token : {}", key, token);
 // 上鎖成功后將令牌綁定當(dāng)前線程
 THREAD_LOCAL_TOKEN.set(token);
 TOKEN_STATUS.put(token, 0);
 if (renewPeriod > 0) {
 // 續(xù)約任務(wù)
 log.info("非cluster模式簡單分布式鎖 - 添加續(xù)約任務(wù),key : {}, token : {}, renewPeriod : {}納秒", key, token, renewPeriod);
 renewTask(key, token, expireTime, renewPeriod);
 }
 return token;
 } catch (Exception e) {
 String token = THREAD_LOCAL_TOKEN.get();
 log.error("非cluster模式簡單分布式鎖 - 上鎖發(fā)生異常,key : {}, token : {}", key, token, e);
 return StringUtils.isBlank(token) ? StringUtils.EMPTY : token;
 }
 }

 /**
 * 鎖續(xù)約任務(wù)
 * @param key 需要續(xù)命的key
 * @param token 成功獲鎖的線程持有的令牌
 * @param expireTime 過期時(shí)間,單位:毫秒
 * @param renewPeriod 續(xù)約周期,單位:納秒
 */
 private void renewTask(String key, String token, long expireTime, long renewPeriod) {
 try {
 EXECUTOR_SERVICE.schedule(() -> {
 try {
 String lua = "if (redis.call('get', KEYS[1]) == ARGV[1]) " +
 "then " +
 " if (redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])) " +
 " then return '1' else return redis.call('get', KEYS[1]) end " +
 "end " +
 "return redis.call('get', KEYS[1])";
 DefaultRedisScript<String> luaScript = new DefaultRedisScript<>(lua, String.class);
 String result = redisTemplate.execute(luaScript, Lists.newArrayList(key), token, String.valueOf(expireTime));
 if (SUCCESS.equals(result)) {
 // 續(xù)約成功
 log.debug("非cluster模式簡單分布式鎖 - 鎖續(xù)約成功,key : {}, token : {}", key, token);
 // 這里加判斷是為了減少定時(shí)任務(wù)
 if (CAN_RENEW.equals(TOKEN_STATUS.get(token))) {
 // 開啟下一次續(xù)約任務(wù)
 renewTask(key, token, expireTime, renewPeriod);
 }
 } else {
 // 這里加判斷是為了防止誤打印warn日志
 if (CAN_RENEW.equals(TOKEN_STATUS.get(token))) {
 log.warn("非cluster模式簡單分布式鎖 - 鎖續(xù)約失敗,key : {}, token : {}, result : {}", key, token, result);
 }
 }
 } catch (Exception e) {
 // 這里異常是拋不出去的,所以需要 catch 打印
 log.error("非cluster模式簡單分布式鎖 - 鎖續(xù)約發(fā)生異常,key : {}, token : {}", key, token, e);
 }
 }, renewPeriod, TimeUnit.NANOSECONDS);
 } catch (Exception e) {
 log.error("非cluster模式簡單分布式鎖 - 添加鎖續(xù)約任務(wù)發(fā)生異常,key : {}, token : {}", key, token, e);
 }
 }
}

下面是并發(fā)單元測試代碼

@Test
 public void concurrencyTest() {
 String[] nums = {"1", "2", "3", "4", "5"};
 List<CompletableFuture<Void>> list = Lists.newArrayListWithExpectedSize(100);
 for (int i = 0; i < 50; i++) {
 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
 for (int count = 0; count < 10; count++) {
 int random = new Random().nextInt(100) % 5;
 String key = "test_" + nums[random];
 while (true) {
 String token = redisSimpleLockUtil.lock(key, 3_000L);
 if (StringUtils.isNotBlank(token)) {
 log.info("concurrencyTest - key : {}", key);
 try {
 Thread.sleep(new Random().nextInt(1500));
 } catch (Exception e) {
 log.error("concurrencyTest - 發(fā)生異常, key : {}", key, e);
 } finally {
 boolean unlock = redisSimpleLockUtil.releaseLock(key, token);
 if (!unlock) {
 log.error("concurrencyTest - 釋放鎖失敗,key : {}", key);
 }
 }
 break;
 }
 }
 }
 });
 list.add(future);
 }
 CompletableFuture<?>[] futures = new CompletableFuture[list.size()];
 list.toArray(futures);
 CompletableFuture.allOf(futures).join();
 }

3.2 紅鎖

一般公司使用Redis 時(shí)都不可能是單節(jié)點(diǎn)的,要么主從+哨兵架構(gòu),要么就是 cluster 架構(gòu)。面對集群,我們不得不思考如何應(yīng)對腦裂這個(gè)問題。而 Redlock 是Redis官方網(wǎng)站給出解決方案。

下面看下針對這兩種集群架構(gòu)的處理方式:

  1. 主從+哨兵

通過訪問哨兵獲取當(dāng)前 master 節(jié)點(diǎn),統(tǒng)計(jì)票數(shù),超過半數(shù)的 master 節(jié)點(diǎn)就是真的 master。我們可以對比我們成功上鎖的節(jié)點(diǎn)是否是真的 master node,從而避免腦裂問題。

  1. cluster
  2. 上鎖需要在集群中半數(shù)以上的 master 操作成功了才算成功。

3.2.1 紅鎖的問題

鎖通過過半原則來規(guī)避腦裂,但是這就讓我們不得不考慮訪問節(jié)點(diǎn)的等待超時(shí)時(shí)間應(yīng)該要多長。而且,也會(huì)降低Redis 分布式鎖的吞吐量。如果有半數(shù)節(jié)點(diǎn)不可用,那么分布式鎖也將變得不可用。因此,實(shí)際使用中,我們還要結(jié)合自己實(shí)際的業(yè)務(wù)場景來權(quán)衡要不要用紅鎖或者修改實(shí)現(xiàn)方案。

作者介紹

蔡柱梁,51CTO社區(qū)編輯,從事Java后端開發(fā)8年,做過傳統(tǒng)項(xiàng)目廣電BOSS系統(tǒng),后投身互聯(lián)網(wǎng)電商,負(fù)責(zé)過訂單,TMS,中間件等。


責(zé)任編輯:華軒 來源: 51CTO
相關(guān)推薦

2021-06-10 06:57:39

Redis存儲(chǔ)數(shù)據(jù)庫

2021-02-02 16:37:25

Redis分布式

2018-10-28 17:54:00

分布式事務(wù)數(shù)據(jù)

2023-10-10 18:26:58

分布式緩存

2021-11-11 07:47:03

Redis分布式

2019-06-19 15:40:06

分布式鎖RedisJava

2021-04-19 05:42:51

Mmap文件系統(tǒng)

2022-03-08 07:22:48

Redis腳本分布式鎖

2018-07-17 08:14:22

分布式分布式鎖方位

2021-07-16 07:57:34

ZooKeeperCurator源碼

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2022-08-04 08:45:50

Redisson分布式鎖工具

2018-11-27 16:17:13

分布式Tomcat

2021-11-26 06:43:19

Java分布式

2023-11-01 14:49:07

2021-07-06 08:37:29

Redisson分布式

2021-10-25 10:21:59

ZK分布式鎖ZooKeeper

2023-08-21 19:10:34

Redis分布式

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2017-10-24 11:28:23

Zookeeper分布式鎖架構(gòu)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

18禁男女爽爽爽午夜网站免费| 91精品国自产在线观看| 亚洲最大成人网站| 成人精品高清在线视频| 亚洲欧美一区二区视频| 国产99在线免费| 天码人妻一区二区三区在线看| 欧美亚洲国产激情| 日韩女优av电影在线观看| 97成人在线观看视频| 老司机福利在线视频| 成人一区二区三区在线观看| 国产成人av网| 在线免费看av网站| 美国成人xxx| 欧美男男青年gay1069videost | 成人精品在线播放| 久久精品123| 欧美华人在线视频| 五月天综合视频| 亚洲成人五区| 欧美丝袜丝nylons| 国产真人做爰毛片视频直播 | 久久久久久久久久久久久久久99| 成人国产精品免费视频| www亚洲视频| 欧美精品日本| 中文字幕久热精品视频在线| 亚洲天堂av网站| 先锋影音网一区二区| 精品成人久久av| 精品久久免费观看| 少妇激情av一区二区| 国产精品一区二区三区四区| 国产精品吹潮在线观看| 日本网站免费观看| 亚洲精品2区| 影音先锋日韩有码| 日本一区二区在线免费观看| 日韩精品免费视频一区二区三区| 91黄色在线观看| 国产91xxx| 性爱视频在线播放| 亚洲欧美日韩电影| 亚洲精品在线视频观看| 国产三级在线免费观看| 99re热这里只有精品视频| 肥熟一91porny丨九色丨| 国产精品无码免费播放| 免费成人av在线播放| 国产97在线视频| 国产婷婷色一区二区在线观看 | 色综合天天狠天天透天天伊人| 黄色av片三级三级三级免费看| 中文精品一区二区| 亚洲国产精品一区二区三区| 亚洲国产精品狼友在线观看| 涩爱av色老久久精品偷偷鲁| 日韩一二在线观看| 丰满少妇中文字幕| 亚洲午夜精品| 精品久久久久久无| 亚洲视频天天射| 伊人精品久久| 精品成人一区二区三区四区| 亚洲av午夜精品一区二区三区| 免费精品一区二区三区在线观看| 91精品免费观看| 中文字幕在线视频一区二区| 午夜视频一区二区在线观看| 日韩欧美国产三级电影视频| 涩视频在线观看| 欧美理伦片在线播放| 日韩国产精品一区| 亚洲成人黄色av| 日韩大片在线观看| 久久久国产精品一区| 免费成年人视频在线观看| 欧美人成网站| 91国内揄拍国内精品对白| 日韩一区二区视频在线| 日韩电影在线免费观看| 国产欧美精品日韩精品| 精品人妻少妇嫩草av无码专区| 国产91丝袜在线播放九色| 国产一区二区三区黄| 青青国产在线| 日本一区二区三区四区在线视频 | 在线这里只有精品| 福利视频999| 成人看片黄a免费看视频| 日韩精品中文字幕久久臀| a级黄色免费视频| 91精品国产麻豆国产在线观看| 欧美激情a在线| 亚洲精品男人的天堂| 九色|91porny| 国产一区二区在线网站| 成人免费一区二区三区视频网站| 亚洲男帅同性gay1069| 精品少妇人妻av免费久久洗澡| 在线天堂新版最新版在线8| 欧美视频自拍偷拍| 亚洲av永久无码精品| 日韩中文欧美| 97久久精品人搡人人玩| 怡红院成永久免费人全部视频| 国产不卡在线播放| 日韩精品久久久免费观看| 伊人福利在线| 欧美中文字幕一二三区视频| 国产免费无码一区二区| 成人免费在线播放| 性视频1819p久久| 91麻豆成人精品国产| 99精品热视频| 国产911在线观看| 日韩天堂在线| 亚洲国产精品久久久| 亚洲 欧美 变态 另类 综合| 免费看亚洲片| 国产99午夜精品一区二区三区| 3d成人动漫在线| 欧美日韩在线一区| 亚洲成人福利视频| 日韩一区欧美| 国产精品第8页| 天天操天天射天天舔| 国产精品理伦片| 精品一卡二卡三卡| 成人自拍在线| 免费不卡欧美自拍视频| 一区精品在线观看| 久久婷婷综合激情| 成人免费视频91| 久久久国产精品入口麻豆| 亚洲香蕉av在线一区二区三区| 精品无码免费视频| 国产成人av网站| 亚洲一区免费看| 日本精品网站| 亚洲欧美一区二区激情| 日韩av大片在线观看| 成人av电影免费观看| 国产性生活免费视频| 国产高清精品二区| 日韩视频永久免费观看| 中文字幕一区二区三区波野结| 久久午夜免费电影| 久久人妻精品白浆国产| 美女亚洲一区| 欧美专区在线观看| 日中文字幕在线| 一本大道久久精品懂色aⅴ| 三级电影在线看| 国产精品入口| 日韩啊v在线| 国产福利亚洲| 久久精品国产综合| 国产成人精品亚洲精品色欲| 亚洲综合丝袜美腿| 国产黑丝在线观看| 午夜亚洲视频| 日本在线观看一区二区三区| 精品裸体bbb| 久久精品国产成人精品| 国产夫妻性生活视频| 亚洲高清免费视频| 日韩人妻无码一区二区三区| 日韩在线一区二区| 亚洲午夜精品久久久久久浪潮| 国产精品中文| 国模精品视频一区二区| 欧美一区二区视频| 欧美丝袜丝交足nylons| 免费三片在线播放| 97精品国产97久久久久久久久久久久 | 原创真实夫妻啪啪av| 激情婷婷久久| 欧美精品在线一区| 国产精品原创视频| 久久国产精彩视频| 国产 欧美 自拍| 黄色成人av网| 无码人妻丰满熟妇啪啪欧美| 奇米综合一区二区三区精品视频| 亚洲一区综合| 精品一区二区三区中文字幕视频| 久久视频在线看| 亚洲精品视频专区| 婷婷开心久久网| 久久美女免费视频| 狂野欧美一区| 欧洲金发美女大战黑人| 精品欧美午夜寂寞影院| 日韩av电影手机在线| 99免在线观看免费视频高清| 欧美成人福利视频| 美女又爽又黄免费视频| 中文字幕一区二区三区不卡在线| 91福利视频免费观看| 乱人伦精品视频在线观看| 色一情一乱一伦一区二区三欧美 | 成人国产精品免费视频| 欧美男男tv网站在线播放| 一区二区国产精品视频| 国产成人精品a视频| 岛国精品视频在线播放| 鲁丝一区二区三区| 国产精品911| 无码人妻h动漫| 伊人久久大香线蕉综合四虎小说| 91文字幕巨乱亚洲香蕉| 国产精品亚洲d| 欧美风情在线观看| www在线播放| 精品毛片乱码1区2区3区| 中文字幕免费观看视频| 中文天堂在线视频| 国产揄拍国内精品对白| www.爱色av.com| 欧美色婷婷久久99精品红桃| 亚洲综合中文字幕在线| 一级毛片久久久| 久久国产精品网站| 色哟哟免费在线观看| 亚洲国产美女精品久久久久∴| 中文天堂在线播放| 精品动漫一区二区| 国产无遮挡又黄又爽| 国产精品国产三级国产普通话99| 国产人成视频在线观看| 精品一区二区三区久久| 黄色片免费在线观看视频| 亚洲人metart人体| 热舞福利精品大尺度视频| 国产精品18hdxxxⅹ在线| 国产精品视频yy9099| 欧美精选视频一区二区| 91高清视频在线免费观看| 成人在线免费看黄| 亚洲精品视频中文字幕| 三级网站在线看| 日韩欧美二区三区| 国产精品视频一二区| 欧美视频第一页| 国产精品老女人| 亚洲亚洲精品在线观看| 久久久久久视频| 久久久久久久久岛国免费| 熟女少妇一区二区三区| av中文字幕亚洲| xxxx视频在线观看| 日本va欧美va欧美va精品| 东京热加勒比无码少妇| 亚洲人成毛片在线播放女女| 日韩精品福利片午夜免费观看| 亚洲区小说区图片区qvod| 明星裸体视频一区二区| 久久国产精品免费精品3p| 亚洲专区国产精品| 国产欧美88| 国产99在线免费| 国产 日韩 欧美 综合 一区| 亚洲在线www| 亚洲一区二区三区在线免费| 古典武侠综合av第一页| 91成人在线精品视频| 成人免费观看网站| 婷婷丁香久久| 国产精品欧美日韩| 婷婷激情一区| 国产精品久在线观看| 亚洲在线资源| 91久色国产| 成人自拍在线| 久久99精品久久久久子伦| 欧美亚洲国产精品久久| 影音先锋欧美资源| 在线国产一区二区| 国产成人精品视频免费看| 性高湖久久久久久久久| 成人三级视频在线播放| 免费精品视频最新在线| 色婷婷一区二区三区在线观看| 国产乱对白刺激视频不卡| 日韩大尺度视频| 成熟亚洲日本毛茸茸凸凹| 国产jk精品白丝av在线观看| 国产精品美女久久久久久久网站| 任我爽在线视频| 亚洲国产欧美日韩另类综合| 99热在线观看免费精品| 欧美在线一区二区三区| 国产精品一品二区三区的使用体验| 亚洲成人亚洲激情| 欧美美女搞黄| 精品国产美女在线| 伊人久久av| 91精品久久久久久久久久久久久久| 96sao精品免费视频观看| 成人动漫在线视频| 成人影院天天5g天天爽无毒影院| 日本道在线视频| 亚洲深夜影院| 无码国产精品一区二区高潮| 91在线丨porny丨国产| 人妻互换一区二区激情偷拍| 亚洲一区视频在线观看视频| 伊人网中文字幕| 亚洲国产欧美一区| 国产片在线观看| 97久久精品在线| 亚洲成人高清| 九九99久久| 欧美欧美天天天天操| 午夜精品久久久内射近拍高清 | 在线观看日韩欧美| 高清免费电影在线观看| 国产成人综合av| 视频精品二区| 色综合电影网| 一区在线播放| 国产又粗又猛又爽又黄| 久久久久久久久久久黄色| 欧美日韩精品在线观看视频| 欧美日韩在线三级| 天天干天天草天天射| xxx欧美精品| 99久久er| 久久综合色一本| 欧美视频导航| 国产成人无码一二三区视频| 成人午夜碰碰视频| 777777国产7777777| 日韩欧美在线视频观看| 亚洲aⅴ乱码精品成人区| 久久综合伊人77777尤物| 欧美精品高清| 精品日韩美女| 欧美三区在线| 亚洲天堂av一区二区三区| 欧美激情一区二区三区蜜桃视频| 国产精品成人av久久| 欧美中文字幕久久| 欧美孕妇孕交| 91成人福利在线| 亚洲国产视频二区| 黄色录像特级片| 韩国精品久久久| 日韩欧美黄色网址| 在线观看日韩毛片| 色鬼7777久久| 69**夜色精品国产69乱| 亚洲97av| 97超碰青青草| 不卡在线观看av| 国产精品一区无码| 日韩精品在线观看一区| 波多野结衣在线播放| 国产在线一区二区三区欧美| 伊人久久亚洲美女图片| 久久人妻少妇嫩草av蜜桃| 午夜伊人狠狠久久| 视频污在线观看| 国内精品久久久久影院优| 日韩大胆成人| 久久久噜噜噜www成人网| 99精品视频中文字幕| 国产精品久久久免费视频| 亚洲欧美国产另类| 中文字幕在线高清| 精品欧美一区二区久久久伦| 日韩影院免费视频| 精品手机在线视频| 91精品国产aⅴ一区二区| sm国产在线调教视频| 7777精品伊久久久大香线蕉语言| 亚洲午夜视频| 在线xxxxx| 欧美日韩国产在线播放| 自拍视频在线播放| 91色琪琪电影亚洲精品久久| 欧美三级乱码| 中文字幕第20页| 欧美伦理视频网站| 青青草原国产在线| 欧美一区二区福利| 麻豆精品一区二区综合av| 日韩成人短视频| 精品国产91乱码一区二区三区 | 秋霞在线观看一区二区三区| 欧美xxxx黑人又粗又长| 亚洲一级一级97网| 欧美特黄色片| 欧美三级午夜理伦三级老人| 国产一区二区三区免费观看| 青青草精品在线视频| 天堂成人国产精品一区| 天天躁夜夜躁狠狠是什么心态|