分布式锁3-Redis分布式锁实战(手动实现)

Redis分布式锁

基于jedis实现分布式锁,源码地址

配置文件

		<!--jedis-->
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>3.2.0</version>
		</dependency>

		<!-- spring2.X集成redis所需common-pool2-->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.6.0</version>
		</dependency>
@Configuration
public class JedisConfig {

    /**
     * 设置redis服务器的host或者ip地址
     */
    @Value("${universe.redis.hostName}")
    private String hostName;

    /**
     * 设置redis的服务的端口号
     */
    @Value("${universe.redis.port}")
    private Integer port;
    /**
     * 超时时间 毫秒
     */
    @Value("30000")
    private int timeOut;

    /**
     * 资源池允许的最大空闲连接数
     */
    @Value("${universe.redis.pool.maxIdle}")
    private Integer maxIdle;

    /**
     * 当资源池用尽后,调用者是否要等待。只有当值为true时,
     * 下面的maxWaitMillis才会生效。
     */
    @Value("${universe.redis.pool.blockWhenExhausted}")
    private Boolean blockWhenExhausted;

    /**
     * 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)。
     */
    @Value("${universe.redis.pool.maxWaitMillis}")
    private Long maxWaitMillis;

    /**
     * 是否开启JMX监控
     */
    @Value("${universe.redis.pool.jmxEnabled}")
    private Boolean jmxEnabled;


    @Bean
    public JedisPool jedisPoolFactory(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
        jedisPoolConfig.setJmxEnabled(jmxEnabled);
        return new JedisPool(jedisPoolConfig,hostName,port,timeOut);
    }
}

分布式锁实现

redis中key的续期要用到延迟队列,所以定义一个实体类实现延迟队列操作,让延迟队列在锁过期前200毫秒时拿到队列元素。

/**
 * @Author 
 * @Date 2024/5/18 22:30
 * 延迟队列存储的对象
 */
public class ItemVo<T> implements Delayed {
    /** 激活时间,表示activeTime毫秒之后可以取到数据*/
    private long activeTime;

    /** 业务数据,这里是redis中的key和value*/
    private T data;

    /** 延迟队列采用毫秒单位计时*/
    public ItemVo(Long time, T data) {
        /** 表示在redis中key的过期之前的100毫秒才能拿到数据,进行延期操作*/
        this.activeTime = System.currentTimeMillis() + time -200;
        this.data = data;
    }

    public Long getTime() {
        return activeTime;
    }

    public T getData() {
        return data;
    }

    /** 返回到激活元素的剩余时长*/
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(activeTime-System.currentTimeMillis(),unit);
    }

    /** 队列元素按剩余时长排序*/
    @Override
    public int compareTo(Delayed o) {
        long between = getDelay(TimeUnit.MILLISECONDS)-o.getDelay(TimeUnit.MILLISECONDS);
        return between == 0 ? 0 : between > 0 ? 1 : -1;
    }
}

定义一个实体类存储key-value形式(使用map也可以)

/**
 * @Author 
 * @Date 2024/5/18 22:31
 * 存储redis中的key-value
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class LockItem {
    private String key;
    private String value;
}

定义一个lock类实现分布式锁,定义锁的过期时间为1S

/**
 * @Author 邵博跃
 * @Date 2024/5/15 21:22
 *
 * redis分布式锁
 */
@Component
public class RedisLock implements Lock {
    /** 失效时间 */
    private static final long LOCK_TIME = 1000;

    /** key前缀 */
    private static final String KEY_PREFIX = "test:";

    /** 释放锁,确保原子性,确保释放锁的线程是枷锁的线程 RUA脚本 */
    private static final String LOCK_RUA =
            "if(redis.call('get', KEYS[1]) ==  ARGV[1]) then\n" +
                    "                return redis.call('del', KEYS[1])\n" +
                    "                end\n" +
                    "                return 0";

    /** 定义一个线程变量存储加锁线程的ID */
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    /** 存储当前线程, 解决锁的可重入问题*/
    private Thread currentThread;

    /** 锁名称 */
    private String lockName = "lock";

    @Autowired
    private JedisPool jedisPool;

    /** -------守护线程参数------- */

    /** 延迟队列*/
    public final DelayQueue<ItemVo<LockItem>> delayQueue = new DelayQueue<>();

    /** 锁失效时间转为为字符串,用于执行锁续期RUA脚本*/
    public static final String LOCK_TIME_STR = String.valueOf(LOCK_TIME);

    /**
     * 续期RUN脚本
     */
    private static final String DELAY_LOCK_RUA =
            "if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
                    " return redis.call('pexpire',KEYS[1],ARGV[2])\n" +
                    " else end return 0 ";


    public String getLockName() {
        return lockName;
    }

    public void setLockName(String lockName) {
        this.lockName = lockName;
    }

    public void setCurrentThread(Thread currentThread) {
        this.currentThread = currentThread;
    }


    /**
     * 加锁
     */
    @Override
    public void lock() {
        /** 加锁,如果加锁失败,阻塞100毫秒后重试 */
        while (!tryLock()){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    /**
     * 加锁
     *
     * @return
     */
    @Override
    public boolean tryLock() {
        //获取当前线程
        Thread t = Thread.currentThread();
        /**
         *   先判断当前线程是不是持有锁的线程,如果是,实现可重入,返回true
         *   如果不是,判断持有锁的线程是不是空,如果不是空,证明有其他线程持有锁,返回false
         *
         */
        if(t == currentThread){
            return true;
        }else if (currentThread != null){
            return false;
        }
        Jedis jedis = jedisPool.getResource();
        String id = null;
        try {
            id = UUID.randomUUID().toString();
            SetParams setParams = new SetParams();
            setParams.nx();
            setParams.px(LOCK_TIME);
            /** 本地线程枪锁 */
            synchronized (this){
               if(currentThread == null && "OK".equals(jedis.set(KEY_PREFIX + lockName,id, setParams))){
                   //持有锁的线程设置为当前线程
                   setCurrentThread(t);
                   //将set的Value放到线程变量
                   threadLocal.set(id);
                   System.out.println("加锁成功,uuid ->" + id);
                   //延迟队列塞数据
                   ItemVo<LockItem> lockItemItemVo = new ItemVo<>(LOCK_TIME, new LockItem(KEY_PREFIX + lockName, id));
                   long delay = lockItemItemVo.getDelay(TimeUnit.MILLISECONDS);
                   System.out.println("队列过期时间 -》"+delay);
                   delayQueue.add(lockItemItemVo);
                   //开启守护线程
                   /**
                    * 守护线程(看门狗)
                    */
                   Thread daoThread = new Thread(() ->{
                       System.out.println("看门狗线程已启动");
                       //守护线程执行逻辑:只要用户线程未结束,守护线程就一直要运行,通过延迟队列来控制是否执行逻辑
                       while (!Thread.currentThread().isInterrupted()){
                           try {
                               //获取key-value
                               LockItem data = delayQueue.take().getData();

                               //续期操作
                               Long result = (Long) jedis.eval(DELAY_LOCK_RUA, Arrays.asList(data.getKey()), Arrays.asList(data.getValue(), LOCK_TIME_STR));
                               if(result.longValue() == 0L){
                                   System.out.println("已释放锁,无需要延期!");
                               }else {
                                   System.out.println("锁已续期->" + LOCK_TIME_STR);
                                   //如果续期成功了,延迟队列再塞入一个元素用于下次续期使用
                                   delayQueue.add(new ItemVo<>(LOCK_TIME,new LockItem(data.getKey(),data.getValue())));
                               }
                           } catch (Exception e) {
                               e.printStackTrace();
                               throw new RuntimeException("锁续期失败"+e);
                           }finally {
                               if(jedis != null){jedis.close();}
                           }
                       }
                   });
                   daoThread.setDaemon(true);
                   daoThread.start();
                   return true;
               }else {
                   return false;
               }
            }
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("加锁失败,uuid -> " + id + e.getMessage());
        }finally {
            jedis.close();//关闭jedis
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        if(t != currentThread){
            System.out.println("非持有锁线程,不能释放锁");
        }
        Jedis jedis = null;
      try {
          jedis = jedisPool.getResource();
          String id = threadLocal.get();
          Long eval = (Long)jedis.eval(LOCK_RUA, Arrays.asList(KEY_PREFIX + lockName), Arrays.asList(id));
          if(eval != 0L){
              System.out.println("解锁成功!uuid ->" + id);
          }else {
              System.out.println("解锁失败! uuid ->" + id);
          }
      }catch (Exception e){
          e.printStackTrace();
      }finally {
          if(jedis != null){
              jedis.close();
          }
          threadLocal.remove();
          setCurrentThread(null);
      }
    }

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

测试

让执行业务逻辑代阻塞5S。

 /** 使用Redis分布式锁 */
    @Transactional
    public void updateByRedisLock(Long id,int count){
        try {
            redisLock.lock();
            Product product = mapper.selectById(id);
            int newCont = product.getProductCount() - count;
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(newCont < 0){
                throw new RuntimeException("商品不足,扣除失败!");
            }
            product.setProductCount(newCont);
            if(mapper.updateProduct(product) >0 ){
                System.out.println("扣减成功!");
            }else {
                throw new RuntimeException("扣减失败");
            }
        }finally {
            redisLock.unlock();
        }
    }

结果:续期6次

加锁成功,uuid ->4d31de91-e929-4596-b6d1-05b95c7255f8
队列过期时间 -》800
看门狗线程已启动
锁已续期->1000
锁已续期->1000
锁已续期->1000
锁已续期->1000
锁已续期->1000
锁已续期->1000
扣减成功!
解锁成功!uuid ->4d31de91-e929-4596-b6d1-05b95c7255f8
已释放锁,无需要延期!
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值