<ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>


      本案例主要講解Redis實(shí)現(xiàn)分布式鎖的兩種實(shí)現(xiàn)方式:Jedis實(shí)現(xiàn)、Redisson
      實(shí)現(xiàn)。網(wǎng)上關(guān)于這方面講解太多了,Van自認(rèn)為文筆沒(méi)他們好,還是用示例代碼說(shuō)明。

      一、jedis 實(shí)現(xiàn)

      該方案只考慮Redis單機(jī)部署的場(chǎng)景

      1.1 加鎖

      1.1.1 原理
      jedis.set(String key, String value, String nxxx, String expx, int time)
      * key: 使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的;
      * value: 我傳的是唯一值(UUID),很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?原因是分布式鎖要滿(mǎn)足解鈴還須系鈴人
      :通過(guò)給value賦值為requestId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候要驗(yàn)證value值,不能誤解鎖;
      * nxxx: 這個(gè)參數(shù)我填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時(shí),我們進(jìn)行set操作;若key已經(jīng)存在,則不做任何操作;
      * expx: 這個(gè)參數(shù)我傳的是PX,意思是我們要給這個(gè)key加一個(gè)過(guò)期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定;
      * time: 與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間。
      1.1.2 小結(jié)

      * set()加入了NX參數(shù),可以保證如果已有key存在,則函數(shù)不會(huì)調(diào)用成功,也就是只有一個(gè)客戶(hù)端能持有鎖,滿(mǎn)足互斥性;
      * 其次,由于我們對(duì)鎖設(shè)置了過(guò)期時(shí)間,即使鎖的持有者后續(xù)發(fā)生崩潰而沒(méi)有解鎖,鎖也會(huì)因?yàn)榈搅诉^(guò)期時(shí)間而自動(dòng)解鎖(即key被刪除),不會(huì)發(fā)生死鎖;
      * 最后,因?yàn)槲覀儗alue賦值為requestId,代表加鎖的客戶(hù)端請(qǐng)求標(biāo)識(shí),那么在客戶(hù)端在解鎖的時(shí)候就可以進(jìn)行校驗(yàn)是否是同一個(gè)客戶(hù)端。
      1.2 釋放鎖

      釋放鎖時(shí)需要驗(yàn)證value值,也就是說(shuō)我們?cè)讷@取鎖的時(shí)候需要設(shè)置一個(gè)value,不能直接用del key這種粗暴的方式,因?yàn)橹苯觗el key
      任何客戶(hù)端都可以進(jìn)行解鎖了,所以解鎖時(shí),我們需要判斷鎖是否是自己的(基于value值來(lái)判斷)

      * 首先,寫(xiě)了一個(gè)簡(jiǎn)單Lua腳本代碼,作用是:獲取鎖對(duì)應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖);
      * 然后,將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()
      方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。
      1.3 案例(家庭多人領(lǐng)取獎(jiǎng)勵(lì)的場(chǎng)景)

      這里放出的是關(guān)鍵代碼,詳細(xì)可運(yùn)行的代碼可至文末地址下載示例代碼。

      1.3.1 準(zhǔn)備

      該案例模擬家庭內(nèi)多人通過(guò)領(lǐng)取一個(gè)獎(jiǎng)勵(lì),但是只能有一個(gè)人能領(lǐng)取成功,不能重復(fù)領(lǐng)?。ㄖ白鲞^(guò)獎(jiǎng)勵(lì)模塊的需求)

      * family_reward_record表 CREATE TABLE `family_reward_record` ( `id` bigint(10)
      NOT NULL AUTO_INCREMENT COMMENT '主鍵id', `family_id` bigint(20) NOT NULL DEFAULT
      '0' COMMENT '商品名稱(chēng)', `reward_type` int(10) NOT NULL DEFAULT '1' COMMENT
      '商品庫(kù)存數(shù)量', `state` int(1) NOT NULL DEFAULT '0' COMMENT '商品狀態(tài)', `create_time`
      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入庫(kù)時(shí)間', `update_time`
      timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY
      KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=270 DEFAULT CHARSET=utf8mb4
      COLLATE=utf8mb4_unicode_ci COMMENT='家庭領(lǐng)取獎(jiǎng)勵(lì)表(家庭內(nèi)多人只能有一個(gè)人能領(lǐng)取成功,不能重復(fù)領(lǐng)取)';
      * application.yml spring: datasource: url: jdbc:mysql://47.98.178.84:3306/dev
      username: dev password: password driver-class-name: com.mysql.jdbc.Driver
      redis: host: 47.98.178.84 port: 6379 password: password timeout: 2000 # mybatis
      mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package:
      cn.van.mybatis.demo.entity
      1.3.2 核心實(shí)現(xiàn)

      * Jedis 單機(jī)配置類(lèi) - RedisConfig.java @Configuration public class RedisConfig
      extends CachingConfigurerSupport { @Value("${spring.redis.host}") private
      String host; @Value("${spring.redis.port}") private int port;
      @Value("${spring.redis.password}") private String password;
      @Value("${spring.redis.timeout}") private int timeout; @Bean public JedisPool
      redisPoolFactory() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
      if (StringUtils.isEmpty(password)) { return new JedisPool(jedisPoolConfig,
      host, port, timeout); } return new JedisPool(jedisPoolConfig, host, port,
      timeout, password); } @Bean(name = "redisTemplate") public
      RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
      redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new
      RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory);
      ObjectMapper objectMapper = new ObjectMapper();
      objectMapper.setVisibility(PropertyAccessor.ALL, Visibility.ANY);
      objectMapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
      Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new
      Jackson2JsonRedisSerializer(Object.class);
      jsonRedisSerializer.setObjectMapper(objectMapper);
      redisTemplate.setDefaultSerializer(jsonRedisSerializer);
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      redisTemplate.afterPropertiesSet(); return redisTemplate; } }
      * 分布式鎖工具類(lèi) - RedisDistributedLock.java @Component public class
      RedisDistributedLock { /** * 成功獲取鎖標(biāo)示 */ private static final String
      LOCK_SUCCESS = "OK"; /** * 成功解鎖標(biāo)示 */ private static final Long RELEASE_SUCCESS
      = 1L; @Autowired private JedisPool jedisPool; /** * redis 數(shù)據(jù)存儲(chǔ)過(guò)期時(shí)間 */ final int
      expireTime = 500; /** * 嘗試獲取分布式鎖 * @param lockKey 鎖 * @param lockValue 請(qǐng)求標(biāo)識(shí) *
      @return 是否獲取成功 */ public boolean tryLock(String lockKey, String lockValue) {
      Jedis jedis = null; try{ jedis = jedisPool.getResource(); String result =
      jedis.set(lockKey, lockValue, "NX", "PX", expireTime); if
      (LOCK_SUCCESS.equals(result)) { return true; } } finally { if(jedis != null){
      jedis.close(); } } return false; } /** * 釋放分布式鎖 * @param lockKey 鎖 * @param
      lockValue 請(qǐng)求標(biāo)識(shí) * @return 是否釋放成功 */ public boolean unLock(String lockKey, String
      lockValue) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String
      script = "if redis.call('get', KEYS[1]) == ARGV[1] then return
      redis.call('del', KEYS[1]) else return 0 end"; Object result =
      jedis.eval(script, Collections.singletonList(lockKey),
      Collections.singletonList(lockValue)); if (RELEASE_SUCCESS.equals(result)) {
      return true; } } finally { if(jedis != null){ jedis.close(); } } return false;
      } }
      * 不加鎖時(shí):模擬 familyId = 1 的家庭同時(shí)領(lǐng)取獎(jiǎng)勵(lì) @Override public HttpResult receiveAward() {
      Long familyId = 1L; Map<String, Object> params = new HashMap<String,
      Object>(16); params.put("familyId", familyId); params.put("rewardType", 1); int
      count = familyRewardRecordMapper.selectCountByFamilyIdAndRewardType(params); if
      (count == 0) { FamilyRewardRecordDO recordDO = new
      FamilyRewardRecordDO(familyId,1,0,LocalDateTime.now()); int num =
      familyRewardRecordMapper.insert(recordDO); if (num == 1) { return
      HttpResult.success(); } return HttpResult.failure(-1, "記錄插入失敗"); } return
      HttpResult.success("該記錄已存在"); }
      * 加鎖的實(shí)現(xiàn):模擬 familyId = 2 的家庭同時(shí)領(lǐng)取獎(jiǎng)勵(lì) @Override public HttpResult
      receiveAwardLock() { Long familyId = 2L; Map<String, Object> params = new
      HashMap<String, Object>(16); params.put("familyId", familyId);
      params.put("rewardType", 1); int count =
      familyRewardRecordMapper.selectCountByFamilyIdAndRewardType(params); if (count
      == 0) { // 沒(méi)有記錄則創(chuàng)建領(lǐng)取記錄 FamilyRewardRecordDO recordDO = new
      FamilyRewardRecordDO(familyId,1,0,LocalDateTime.now()); // 分布式鎖的key(familyId +
      rewardType) String lockKey = recordDO.getFamilyId() + "_" +
      recordDO.getRewardType(); // 分布式鎖的value(唯一值) String lockValue = createUUID();
      boolean lockStatus = redisLock.tryLock(lockKey, lockValue); // 鎖被占用 if
      (!lockStatus) { log.info("鎖已經(jīng)占用了"); return HttpResult.failure(-1,"失敗"); } //
      不管多個(gè)請(qǐng)求,加鎖之后,只會(huì)有一個(gè)請(qǐng)求能拿到鎖,進(jìn)行插入操作
      log.info("拿到了鎖,當(dāng)前時(shí)刻:{}",System.currentTimeMillis()); int num =
      familyRewardRecordMapper.insert(recordDO); if (num != 1) { log.info("數(shù)據(jù)插入失?。?quot;);
      return HttpResult.failure(-1, "數(shù)據(jù)插入失?。?quot;); } log.info("數(shù)據(jù)插入成功!準(zhǔn)備解鎖..."); boolean
      unLockState = redisLock.unLock(lockKey,lockValue); if (!unLockState) {
      log.info("解鎖失敗!"); return HttpResult.failure(-1, "解鎖失敗!"); } log.info("解鎖成功!");
      return HttpResult.success(); } log.info("該記錄已存在"); return
      HttpResult.success("該記錄已存在"); } private String createUUID() { UUID uuid =
      UUID.randomUUID(); String str = uuid.toString().replace("-", "_"); return str; }
      1.3.3 測(cè)試

      我采用的是JMeter工具進(jìn)行測(cè)試,加鎖和不加鎖的情況都設(shè)置成:五次并發(fā)請(qǐng)求。

      1.3.3.1 不加鎖
      /** * 家庭成員領(lǐng)取獎(jiǎng)勵(lì)(不加鎖) * @return */ @PostMapping("/receiveAward") public
      HttpResult receiveAward() { return redisLockService.receiveAward(); }
      * 請(qǐng)求方式:POST
      * 請(qǐng)求地址:http://localhost:8080/redisLock/receiveAward
      <http://localhost:8080/redisLock/receiveAward>
      * 返回結(jié)果:插入了五條記錄


      1.3.3.2 加鎖
      /** * 家庭成員領(lǐng)取獎(jiǎng)勵(lì)(加鎖) * @return */ @PostMapping("/receiveAwardLock") public
      HttpResult receiveAwardLock() { return redisLockService.receiveAwardLock(); }
      * 請(qǐng)求方式:POST
      * 請(qǐng)求地址:http://localhost:8080/redisLock/receiveAwardLock
      <http://localhost:8080/redisLock/receiveAwardLock>
      * 返回結(jié)果:只插入了一條記錄


      通過(guò)對(duì)比,說(shuō)明分布式鎖起作用了。

      1.4 小結(jié)

      我上家使用的就是這種加鎖方式,看上去很OK,實(shí)際上在Redis集群的時(shí)候會(huì)出現(xiàn)問(wèn)題,比如:

      A客戶(hù)端在Redis的master節(jié)點(diǎn)上拿到了鎖,但是這個(gè)加鎖的key還沒(méi)有同步到slave節(jié)點(diǎn),master故障,發(fā)生故障轉(zhuǎn)移,一個(gè)slave節(jié)點(diǎn)升級(jí)為
      master節(jié)點(diǎn),B客戶(hù)端也可以獲取同個(gè)key的鎖,但客戶(hù)端A也已經(jīng)拿到鎖了,這就導(dǎo)致多個(gè)客戶(hù)端都拿到鎖。

      正因?yàn)槿绱?,Redis作者antirez基于分布式環(huán)境下提出了一種更高級(jí)的分布式鎖的實(shí)現(xiàn)方式:Redlock。

      二、Redlock實(shí)現(xiàn)

      2.1 原理

      antirez提出的Redlock算法大概是這樣的:

      在Redis的分布式環(huán)境中,我們假設(shè)有N個(gè)Redis master。這些節(jié)點(diǎn)完全互相獨(dú)立,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制。我們確保將在N個(gè)實(shí)例上使用與在
      Redis單實(shí)例下相同方法獲取和釋放鎖?,F(xiàn)在我們假設(shè)有5個(gè)Redis master節(jié)點(diǎn),同時(shí)我們需要在5臺(tái)服務(wù)器上面運(yùn)行這些Redis
      實(shí)例,這樣保證他們不會(huì)同時(shí)都宕掉。

      2.1.1 加鎖

      為了取到鎖,客戶(hù)端應(yīng)該執(zhí)行以下操作(RedLock算法加鎖步驟):

      * 獲取當(dāng)前Unix時(shí)間,以毫秒為單位;
      * 依次嘗試從5個(gè)實(shí)例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當(dāng)向Redis
      請(qǐng)求獲取鎖時(shí),客戶(hù)端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在5-50
      毫秒之間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下,客戶(hù)端還在死死地等待響應(yīng)結(jié)果。如果服務(wù)器端沒(méi)有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶(hù)端應(yīng)該盡快嘗試去另外一個(gè)Redis
      實(shí)例請(qǐng)求獲取鎖;
      * 客戶(hù)端使用當(dāng)前時(shí)間減去開(kāi)始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個(gè)節(jié)點(diǎn))的Redis
      節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功;
      * 如果取到了鎖,key的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。
      * 如果因?yàn)槟承┰颍@取鎖失?。](méi)有在至少N/2+1個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過(guò)了有效時(shí)間),客戶(hù)端應(yīng)該在所有的Redis
      實(shí)例上進(jìn)行解鎖(即便某些Redis實(shí)例根本就沒(méi)有加鎖成功,防止某些節(jié)點(diǎn)獲取到鎖但是客戶(hù)端沒(méi)有得到響應(yīng)而導(dǎo)致接下來(lái)的一段時(shí)間不能被重新獲取鎖)。
      2.1.2 解鎖

      向所有的Redis實(shí)例發(fā)送釋放鎖命令即可,不用關(guān)心之前有沒(méi)有從Redis實(shí)例成功獲取到鎖.

      2.2 案例(商品超賣(mài)為例)

      這部分以最常見(jiàn)的案例:搶購(gòu)時(shí)的商品超賣(mài)(庫(kù)存數(shù)減少為負(fù)數(shù))為例

      2.2.1 準(zhǔn)備

      * good表 CREATE TABLE `good` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT
      '主鍵id', `good_name` varchar(255) NOT NULL COMMENT '商品名稱(chēng)', `good_counts`
      int(255) NOT NULL COMMENT '商品庫(kù)存', `create_time` timestamp NOT NULL ON UPDATE
      CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間', PRIMARY KEY (`id`) ) ENGINE=InnoDB
      AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; -- 插入兩條測(cè)試數(shù)據(jù) INSERT INTO
      `good` VALUES (1, '哇哈哈', 5, '2019-09-20 17:39:04'); INSERT INTO `good` VALUES
      (2, '衛(wèi)龍', 5, '2019-09-20 17:39:06');
      * 配置文件跟上面一樣
      2.2.2 核心實(shí)現(xiàn)

      * Redisson 配置類(lèi) RedissonConfig.java
      我這里配置的是單機(jī),更多配置詳見(jiàn)https://github.com/redisson/redisson/wiki/配置
      <https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95>
      @Configuration public class RedissonConfig { @Value("${spring.redis.host}")
      private String host; @Value("${spring.redis.port}") private String port;
      @Value("${spring.redis.password}") private String password; /** *
      RedissonClient,單機(jī)模式 * @return * @throws IOException */ @Bean public
      RedissonClient redissonSentinel() { //支持單機(jī),主從,哨兵,集群等模式,此為單機(jī)模式 Config config =
      new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" +
      port) .setPassword(password); return Redisson.create(config); } }
      * 不加鎖時(shí) @Override public HttpResult saleGoods(){ // 以指定goodId = 1:哇哈哈為例 Long
      goodId = 1L; GoodDO goodDO = goodMapper.selectByPrimaryKey(goodId); int
      goodStock = goodDO.getGoodCounts(); if (goodStock >= 1) {
      goodMapper.saleOneGood(goodId); } return HttpResult.success(); }
      * 加鎖 @Override public HttpResult saleGoodsLock(){ // 以指定goodId = 2:衛(wèi)龍為例 Long
      goodId = 2L; GoodDO goodDO = goodMapper.selectByPrimaryKey(goodId); int
      goodStock = goodDO.getGoodCounts(); String key = goodDO.getGoodName();
      log.info("{}剩余總庫(kù)存,{}件", key,goodStock); // 將商品的實(shí)時(shí)庫(kù)存放在redis 中,便于讀取
      stringRedisTemplate.opsForValue().set(key, Integer.toString(goodStock)); //
      redisson 鎖 的key String lockKey = goodDO.getId() +"_" + key; RLock lock =
      redissonClient.getLock(lockKey); // 設(shè)置60秒自動(dòng)釋放鎖 (默認(rèn)是30秒自動(dòng)過(guò)期) lock.lock(60,
      TimeUnit.SECONDS); // 此步開(kāi)始,串行銷(xiāo)售 int stock =
      Integer.parseInt(stringRedisTemplate.opsForValue().get(key)); //
      如果緩存中庫(kù)存量大于1,可以繼續(xù)銷(xiāo)售 if (stock >= 1) { goodDO.setGoodCounts(stock - 1); int num =
      goodMapper.saleOneGood(goodId); if (num == 1) { // 減庫(kù)存成功,將緩存同步
      stringRedisTemplate.opsForValue().set(key,Integer.toString((stock-1))); }
      log.info("{},當(dāng)前庫(kù)存,{}件", key,stock); } lock.unlock(); return
      HttpResult.success(); }
      2.3 測(cè)試

      采用的是JMeter工具進(jìn)行測(cè)試,初始化的時(shí)候兩個(gè)商品的庫(kù)存設(shè)置都是:5;所以這里加鎖和不加鎖的情況都設(shè)置成:十次并發(fā)請(qǐng)求。

      2.3.1 不加鎖
      /** * 售賣(mài)商品(不加鎖) * @return */ @PostMapping("/saleGoods") public HttpResult
      saleGoods() { return redisLockService.saleGoods(); }
      * 請(qǐng)求方式:POST
      * 請(qǐng)求地址:http://localhost:8080/redisLock/saleGoods
      <http://localhost:8080/redisLock/saleGoods>
      * 返回結(jié)果:id =1的商品庫(kù)存減為-5


      2.3.2 加鎖
      /** * 售賣(mài)商品(加鎖) * @return */ @PostMapping("/saleGoodsLock") public HttpResult
      saleGoodsLock() { return redisLockService.saleGoodsLock(); }
      * 請(qǐng)求方式:POST
      * 請(qǐng)求地址:http://localhost:8080/redisLock/saleGoodsLock
      <http://localhost:8080/redisLock/saleGoodsLock>
      * 返回結(jié)果:id =1的商品庫(kù)存減為0


      2.3.3 小結(jié)

      通過(guò)2.3.1和2.3.2的結(jié)果對(duì)比很明顯:前者出現(xiàn)了超賣(mài)情況,庫(kù)存數(shù)賣(mài)到了-5,這是決不允許的;而加了鎖的情況后,庫(kù)存只會(huì)減少到0,便不再銷(xiāo)售。

      三、總結(jié)

      再次說(shuō)明:以上代碼不全,如需嘗試,請(qǐng)前往Van 的 Github 查看完整示例代碼

      第一種基于Redis的分布式鎖并不適合用于生產(chǎn)環(huán)境。Redisson 可用于生產(chǎn)環(huán)境。當(dāng)然,分布式的選擇還有Zookeeper
      的選項(xiàng),Van后續(xù)會(huì)整理出來(lái)供大家參考。

      3.1 示例源碼地址


      https://github.com/vanDusty/SpringBoot-Home/tree/master/springboot-demo-lock/redis-lock

      <https://github.com/vanDusty/SpringBoot-Home/tree/master/springboot-demo-lock/redis-lock>

      3.2 技術(shù)交流

      * 風(fēng)塵博客 <https://www.dustyblog.cn/>
      * 風(fēng)塵博客-掘金 <https://juejin.im/user/5d5ea68e6fb9a06afa328f56/posts>
      * 風(fēng)塵博客-CSDN <https://blog.csdn.net/weixin_42036952>

      友情鏈接
      ioDraw流程圖
      API參考文檔
      OK工具箱
      云服務(wù)器優(yōu)惠
      阿里云優(yōu)惠券
      騰訊云優(yōu)惠券
      京東云優(yōu)惠券
      站點(diǎn)信息
      問(wèn)題反饋
      郵箱:[email protected]
      QQ群:637538335
      關(guān)注微信

        <ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>
          啊灬啊灬啊灬快灬高潮了妇女主任 | 国产妇女乱码一区二区三区 | 亚洲色图8p| 国产精品免费人成网站酒店 | 日韩高清在线一区 | 午夜成人福利视频 | 小舞张开玉腿求我桶她的漫画 | 国产片婬乱一级毛片影片乱叫 | 操逼视频内射 | 天天三级|