MENU

Redis分布式锁(二)

1、初始化版本简单添加
2、nginx分布式微服务架构
3、redis分布式锁 v3.0
4、宕机与过期+防止死锁 v4.0
5、防止误删除key的问题 v5.0
6、Lua保证原子性 v6.0

InventoryController

package com.zzrg.redislock.controller;

import com.zzrg.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @Author ZzRG
* @Date 2023/6/14 1:14
* @Version 1.0
*/

@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController {

   @Autowired
   private InventoryService inventoryService;

   @ApiOperation("扣减库存sale,一次卖一个")
   @GetMapping(value = "/inventory/sale")
   public String sale()
   {
       return inventoryService.sale();
   }
}

InventoryService

package com.zzrg.redislock.service;

/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
public interface InventoryService {
   String sale();


}

InventoryServiceImpl:

package com.zzrg.redislock.service.Impl;

import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {

  @Autowired
  private StringRedisTemplate stringRedisTemplate;
  @Value("${service.port}")
  private String port;

  /**
   * v6.0 Lua脚本的redis分布式锁调用,必须保证原子性
   *      但是基本 v6.0 版本已经够用
   * @return
   */
  @Override
  public String sale() {
      String retMessage = "";
      String key = "zzrgRedisLock";
      String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

      //不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if 用while来代替
      //添加Key同时添加过期时间 保证原子性
      while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){
          //等待20毫秒
          try {
              TimeUnit.MILLISECONDS.sleep(20);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          //如果这样设置过期时间  就会无法保证原子性  还会出现死锁,添加了key 没走到这一步 就不会有过期时间
          //因此需要 与上面的添加Key同时添加过期时间 保证原子性
          // stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
      }
      //枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage);
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          //改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
          String luaScript =
                  "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                       "return redis.call('del',KEYS[1])" +
                  "else " +
                       "return 0 " +
                  "end";
          stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
      }
      return retMessage + "\t" + "服务端口号:" + port;
  }


  /**
   * v4.0 加了过期时间 stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)
   *                  问题:误删锁,如果线程A运行时间超出了过期时间
   *                           在线程A运行时,xfcyRedisLock这个key过期,另一个线程B进来加了key
   *                           线程A结束后,把线程B的锁删了
   *                 stringRedisTemplate.delete(key);  只能自己删除自己的,需要添加判断是否是自己的锁
   * 设置的key + 过期时间不能分开 必须合并在一行具备原子性!
   * v5.0 防止误删Key 但是还存在问题是最后的 finally 中的判断+del key 不是一行原子性操作,需要用Lua脚本修改。
   * @return
  @Override
  public String sale() {
      String retMessage = "";
      String key = "zzrgRedisLock";
      String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

      //不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if 用while来代替
      //添加Key同时添加过期时间 保证原子性
      while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){
          //等待20毫秒
          try {
              TimeUnit.MILLISECONDS.sleep(20);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          //如果这样设置过期时间  就会无法保证原子性  还会出现死锁,添加了key 没走到这一步 就不会有过期时间
          //因此需要 与上面的添加Key同时添加过期时间 保证原子性
          // stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
      }
      //枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage);
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          //改进点,只能删除数据自己的key,不能删除被人的
          if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
              stringRedisTemplate.delete(key);
          }
      }
      return retMessage + "\t" + "服务端口号:" + port;
  }
   */

  /**
   * v3.2  出现的问题死锁:分布式微服务中突然一个挂了,代码层面跟们没有走到finally,这个服务就一直拿着这个锁,没办法保证解锁(redis 中的key一直存在)
   * 这个key没有被删除,需要加入一个过期时间的key。
   * ---------------------------------------------
   * setnx  用while判断
   *               问题: setnx过后,正在进行业务逻辑操作时,没有走到finally之前,整个微服务down机了,导致锁一直存在
   *                     不是程序出了问题,如果程序问题,最后还是会执行finally
   *                     没办法保证解锁(没有过期时间,该key一直存在),需要加入过期时间限定key
   * @return

  @Override
  public String sale() {
      String retMessage = "";
      String key = "zzrgRedisLock";
      String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

      //不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if 用while来代替
      while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
          //等待20毫秒
          try {
              TimeUnit.MILLISECONDS.sleep(20);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      //枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage);
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          stringRedisTemplate.delete(key);
      }
      return retMessage + "\t" + "服务端口号:" + port;
  }
   */

  /**
   * v3.1 版本的出现了递归重试,容易导致stackoverflowerror,所以不太推荐; 另外,高并发缓刑后推荐while判断而不是if
  @Override
  public String sale() {
      String retMessage = "";
      String key = "zzrgRedisLock";
      String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

      Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
      //flag == false    抢不到的线程要继续重试。。。。
      if (!flag){
          //等待20毫秒,进行递归重试。。。
          try {
              TimeUnit.MILLISECONDS.sleep(20);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          sale();
      }else {
          //枪锁成功的请求线程。进行正常业务逻辑操作,扣减库存
          try {
              // 1 查询库存信息
              String result = stringRedisTemplate.opsForValue().get("inventory001");
              // 2 判断库存是否足够
              Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
              // 3 扣减库存 每次减少一个
              if (inventoryNumber > 0) {
                  stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                  retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
                  System.out.println(retMessage);
              } else {
                  retMessage = "商品卖完了,o(╥﹏╥)o";
              }
          } finally {
              stringRedisTemplate.delete(key);
          }
      }
      return retMessage + "\t" + "服务端口号:" + port;
  }
   */


  /**
   * v2.0, 单机版枷锁配合nigx和jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖

  private Lock lock = new ReentrantLock();
  @Override
  public String sale() {
      String retMessage = "";
      lock.lock();
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage);
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          lock.unlock();
      }
      return retMessage + "\t" + "服务端口号:" + port;

  }
   */
  @Override
  public String saleByRedisson() {

  }

}
//改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
          String luaScript =
                  "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                       "return redis.call('del',KEYS[1])" +
                  "else " +
                       "return 0 " +
                  "end";
          stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);

image-20230616012650788

7、可重入锁+设计模式

可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么]ava虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

image-20230617160109583

//加锁的Lua脚本,对标我们的lock方法 v1.0
if redis.call('exists','key') ==0 then
    redis.call('hest','key','uuid:threadid',1)
    redis.call('expire','key',50)
    return 1
elseif redis.call('hexists','key','uuid:threadid') ==1 then
    redis.call('hincrby','key','uuid:threadid',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end

//v2.0 合并相同的代码,用hincrby替代hset,精简代码
if redis.call('exists','key') ==0 or redis.call('hexists','key','uuid:threadid') == 1 then
    redis.call('hincrby','key','uuid:threadid',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end

//v3.0 脚本换成动态参数来代替
if redis.call('exists',KEYS[1]) ==0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
    redis.call('hincrby',KEYS[1],ARGV[1],1)
    redis.call('expire',KEYS[1],ARGV[2])
    return 1
else
    return 0
end 

image-20230617161726195

//解锁的Lua脚本,对标我们的unlock方法//V1
if redis.call( 'hexists ',key , uuid:threadid) == 0 then
  return nil
elseif redis.call( 'hincrby' , key, uuid:threadid,-1)== 0 then
    return redis.call( 'del ' ,key)
else
    return 0
end

//V2
if redis.call( ' HEXISTS',KEYS[1],ARGV[1])== 0 then
  return nil
elseif redis.cal1( 'HINCRBY' ,KEYS[1],ARGV[1],-1) == 0 then
  return redis.call( 'del',KEYS[1])
else
    return 0
end

程序编写

image-20230617164537560

创建RedisDistributedLock

package com.zzrg.redislock.myLock;

import cn.hutool.core.util.IdUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
* @Author ZzRG
* @Date 2023/6/17 16:42
* @Version 1.0
* 我们自研的redis分布式锁,实现了Lock接口
*/

public class RedisDistributedLock implements Lock {
  private StringRedisTemplate stringRedisTemplate;
  private String lockName; //KEYS[1]
  private String uuidValue; //ARGV[1]
  private long expireTime; //ARGV[2]

  public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
      this.stringRedisTemplate = stringRedisTemplate;
      this.lockName = lockName;
      this.uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
      this.expireTime = 50L;
  }

  @Override
  public void lock() {
      tryLock();
  }

  @Override
  public boolean tryLock() {
      try {
          tryLock(-1L,TimeUnit.SECONDS);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      return false;
  }

  @Override
  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
      if (time == -1L){
          String script = "if redis.call('exists',KEYS[1]) ==0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then" +
                              "redis.call('hincrby',KEYS[1],ARGV[1],1)" +
                              "redis.call('expire',KEYS[1],ARGV[2])" +
                              "return 1" +
                          "else" +
                              "return 0" +
                          "end ";
          while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
              //等待20毫秒
              try {
                  TimeUnit.MILLISECONDS.sleep(20);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          return true;

      }

      return false;
  }

  @Override
  public void unlock() {
      String script = "if redis.call( ' HEXISTS',KEYS[1],ARGV[1])== 0 then" +
                          "return nil" +
                       "elseif redis.cal1( 'HINCRBY' ,KEYS[1],ARGV[1],-1) == 0 then" +
                          "return redis.call( 'del',KEYS[1])" +
                       "else" +
                          "return 0" +
                       "end";
      // nil =false 1=true 0=false
      Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
      if (null ==flag){
          throw new RuntimeException("this lock doesn't exists");
      }

  }

  // ===========下面两个暂时用不到 不在重写!=====
  @Override
  public void lockInterruptibly() throws InterruptedException {

  }


  @Override
  public Condition newCondition() {
      return null;
  }
}

业务逻辑

package com.zzrg.redislock.service.Impl;

import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {

  @Autowired
  private StringRedisTemplate stringRedisTemplate;
  @Value("${service.port}")
  private String port;

  //v7.0 如何将我们的Lock/unLock+lua脚本自研版的redis分布式锁搞定?|
  Lock redisDistributedLock=new RedisDistributedLock(stringRedisTemplate,"RedisLock");
  @Override
  public String sale() {
      String retMessage = "";
      redisDistributedLock.lock();
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage);
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          redisDistributedLock.unlock();
      }
      return retMessage + "\t" + "服务端口号:" + port;

  }
}

这样设计出现了分布式锁写死的问题

image-20230617171721117

改造引入工厂模式

package com.zzrg.redislock.config;

import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;

/**
* @Author ZzRG
* @Date 2023/6/17 17:23
* @Version 1.0
*/
@Component
public class DistributedLockFactory {

  @Autowired
  private StringRedisTemplate stringRedisTemplate;
  private String lockName;
  private String uuidValue;

  public Lock getDistributedLock(String lockType) {
      if (lockType == null) {
          return null;
      }
      if (lockType.equalsIgnoreCase("REDIS")) {
          this.lockName = "RedisLock";
          return new RedisDistributedLock(stringRedisTemplate,lockName);
      } else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
          this.lockName = "ZookeeperLock";
          // TODO zoookeeper 版本的分布式锁
          return null;
      } else if (lockType.equalsIgnoreCase("MYSQL")) {
          this.lockName = "MysqlLock";
          // TODO MYSQL 版本的分布式锁
          return null;
      }
      return null;
  }
}

引入可重入锁业务逻辑

package com.zzrg.redislock.service.Impl;

import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.config.DistributedLockFactory;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {

  @Autowired
  private StringRedisTemplate stringRedisTemplate;
  @Value("${service.port}")
  private String port;
  @Autowired
  private DistributedLockFactory distributedLockFactory;

  //v7.0 如何将我们的Lock/unLock+lua脚本自研版的redis分布式锁搞定?
  @Override
  public String sale() {
      String retMessage = "";

      Lock redisLock=distributedLockFactory.getDistributedLock("REDIS");

      redisLock.lock();
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage + "\t" + "服务端口号:" + port);
              testReEntry();
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          redisLock.unlock();
      }
      return retMessage + "\t" + "服务端口号:" + port;

  }

  //可重性测试
  private void testReEntry() {
      Lock redisLock=distributedLockFactory.getDistributedLock("REDIS");
      redisLock.lock();
      try {
          System.out.println("==========测试可重入锁==========");
      } finally {
          redisLock.unlock();
      }
  }

中间加入可重入性测试出现问题

image-20230617174724464

改造DistributedLockFactory

package com.zzrg.redislock.config;

import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;

/**
* @Author ZzRG
* @Date 2023/6/17 17:23
* @Version 1.0
*/
@Component
public class DistributedLockFactory {

  @Autowired
  private StringRedisTemplate stringRedisTemplate;
  private String lockName;
  private String uuidValue;

  public DistributedLockFactory() {
      this.uuidValue = IdUtil.simpleUUID();
  }
  public Lock getDistributedLock(String lockType) {
      if (lockType == null) {
          return null;
      }
      if (lockType.equalsIgnoreCase("REDIS")) {
          this.lockName = "RedisLock";
          return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
      } else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
          this.lockName = "ZookeeperLock";
          // TODO zoookeeper 版本的分布式锁
          return null;
      } else if (lockType.equalsIgnoreCase("MYSQL")) {
          this.lockName = "MysqlLock";
          // TODO MYSQL 版本的分布式锁
          return null;
      }
      return null;
  }
}

改造RedisDistributedLock

 // public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
  //     this.stringRedisTemplate = stringRedisTemplate;
  //     this.lockName = lockName;
  //     this.uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
  //     this.expireTime = 50L;
  // }
  public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
      this.stringRedisTemplate = stringRedisTemplate;
      this.lockName = lockName;
      this.uuidValue = uuidValue+":" + Thread.currentThread().getId();
      this.expireTime = 50L;
  }
8、自动续期

image-20230617210124043

//自动续期加个钟的Lua脚本
if redis.call('HEISTS',KEYS[1],ARGV[1]) ==1 then
  return redis.call('expire',KEYS[1],ARGV[2])
else
  return 0
end
package com.zzrg.redislock.myLock;

import cn.hutool.core.util.IdUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
* @Author ZzRG
* @Date 2023/6/17 16:42
* @Version 1.0
* 我们自研的redis分布式锁,实现了Lock接口
*/

//@component引入DistributedLockFactory 工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock {
  private StringRedisTemplate stringRedisTemplate;
  private String lockName; //KEYS[1]
  private String uuidValue; //ARGV[1]
  private long expireTime; //ARGV[2]

  // public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
  //     this.stringRedisTemplate = stringRedisTemplate;
  //     this.lockName = lockName;
  //     this.uuidValue = IdUtil.simpleUUID()+":" + Thread.currentThread().getId();
  //     this.expireTime = 50L;
  // }
  public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
      this.stringRedisTemplate = stringRedisTemplate;
      this.lockName = lockName;
      this.uuidValue = uuidValue+":" + Thread.currentThread().getId();
      this.expireTime = 50L;
  }

  @Override
  public void lock() {
      tryLock();
  }

  @Override
  public boolean tryLock() {
      try {
          tryLock(-1L,TimeUnit.SECONDS);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      return false;
  }

  @Override
  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
      if (time == -1L){
          String script = "if redis.call('exists',KEYS[1]) ==0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then" +
                              "redis.call('hincrby',KEYS[1],ARGV[1],1)" +
                              "redis.call('expire',KEYS[1],ARGV[2])" +
                              "return 1" +
                          "else" +
                              "return 0" +
                          "end ";
          System.out.println("lockName:" + lockName + "\t" + "uuidValue" + uuidValue);
          while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
              //等待20毫秒
              try {
                  TimeUnit.MILLISECONDS.sleep(20);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          //新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2  1/3未来实现续期
          this.renewExpire();
          return true;

      }

      return false;
  }
  @Override
  public void unlock() {
      String script = "if redis.call( ' HEXISTS',KEYS[1],ARGV[1])== 0 then" +
                          "return nil" +
                       "elseif redis.cal1( 'HINCRBY' ,KEYS[1],ARGV[1],-1) == 0 then" +
                          "return redis.call( 'del',KEYS[1])" +
                       "else" +
                          "return 0" +
                       "end";
      // nil =false 1=true 0=false
      Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
      if (null ==flag){
          throw new RuntimeException("this lock doesn't exists");
      }

  }

  private void renewExpire() {
      String script = "if redis.call('HEISTS',KEYS[1],ARGV[1]) ==1 then" +
                          "return redis.call('expire',KEYS[1],ARGV[2])" +
                      "else" +
                          "return 0" +
                      "end";
      new Timer().schedule(new TimerTask() {
          @Override
          public void run() {
              if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
                  renewExpire();
              }

          }
      },(this.expireTime * 1000/3));
  }

  // ===========下面两个暂时用不到 不在重写!=====
  @Override
  public void lockInterruptibly() throws InterruptedException {

  }


  @Override
  public Condition newCondition() {
      return null;
  }
}
package com.zzrg.redislock.service.Impl;

import cn.hutool.core.util.IdUtil;
import com.zzrg.redislock.config.DistributedLockFactory;
import com.zzrg.redislock.myLock.RedisDistributedLock;
import com.zzrg.redislock.service.InventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.sql.Time;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
* @Author ZzRG
* @Date 2023/6/14 1:09
* @Version 1.0
*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {

  @Autowired
  private StringRedisTemplate stringRedisTemplate;
  @Value("${service.port}")
  private String port;
  @Autowired
  private DistributedLockFactory distributedLockFactory;

  //V8.0,实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本nublic string ca1e( )
  @Override
  public String sale() {
      String retMessage = "";

      Lock redisLock=distributedLockFactory.getDistributedLock("REDIS");

      redisLock.lock();
      try {
          // 1 查询库存信息
          String result = stringRedisTemplate.opsForValue().get("inventory001");
          // 2 判断库存是否足够
          Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
          // 3 扣减库存 每次减少一个
          if (inventoryNumber > 0) {
              stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
              retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
              System.out.println(retMessage + "\t" + "服务端口号:" + port);
              //暂停120秒钟线程  故意的 主要演示自动续期的功能
              try {
                  TimeUnit.MILLISECONDS.sleep(120);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } else {
              retMessage = "商品卖完了,o(╥﹏╥)o";
          }
      } finally {
          redisLock.unlock();
      }
      return retMessage + "\t" + "服务端口号:" + port;

  }
}