基于Redis手工实现分布式锁

1.分布式锁概述

1.1什么是分布式锁

        随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。而在分布式系统中访问共享资源就需要一种互斥机制,来解决分布式系统中控制共享资源访问的问题,以保证数据一致性,在这种情况下,我们就需要用到分布式锁。

总结:

        1)应用场景:分布式系统。

        2)作用:提供一种共享资源访问的互斥机制,保证数据一致性。

1.2分布式锁的特性

  • 互斥“”在分布式系统环境下,一个方法在同一时间只能被一个线程执行(即获取锁)
  • 高可用:高可用的获取锁与释放锁
  • 高性能:高性能的获取锁与释放锁
  • 可重入:具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  • 防止死锁:具备锁失效机制,超时即自动解锁,
  • 非阻塞:即如果线程没有获取到锁将,直接返回获取锁失败,而不会一直阻塞

2.分布式锁的实现

        目前分布式锁常见的三种实现方式:

        1、基于数据库实现分布式锁;

        2、基于缓存(Redis等)实现分布式锁;

        3、基于Zookeeper实现分布式锁。

        本文就基于Redis手工实现分布式锁,当前现在主流的是基于Redisson工具包实现(不在本文范围内),但手工实现原理基本一致。

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。

2.1 锁接口定义

        接口比较简单,主要定义了加锁和释放锁两个方法

package com.example.demo.redis.lock;

import java.util.concurrent.TimeUnit;

/**
 * 
* @ClassName: RedisDistributeLock
* @Description: Redis分布式锁
* @Author: liulianglin
* @DateTime 2022年8月30日 下午5:47:47
 */
public interface RedisDistributeLock {
	/**
	 * 
	* @Description: 加锁 
	* @Author: liulianglin
	* @Datetime: 2022年8月30日 下午5:49:43
	* @param key 主键
	* @param timeout 超时时间
	* @param unit	 超时时间单位
	* @return boolean  true:加锁成功;false:加锁失败
	* @throws
	 */
	boolean tryLock(String key, long timeout, TimeUnit unit);
	
	/**
	 * 
	* @Description: 释放锁。加锁和释放锁的线程必须保证是同一个。 
	* @Author: liulianglin
	* @Datetime: 2022年8月30日 下午5:51:20
	* @param key 	主键
	* @throws
	 */
	void releaseLock(String key);
}

 

2.2 接口实现

         实现RedisDistributeLock接口,其中针对分布式锁的一些特性进行实现,如加锁解锁线程一致性保证、可重入、非阻塞等(参考注释)

package com.example.demo.redis.lock;

import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;


public class RedisDistributeLockImpl implements RedisDistributeLock{

	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	
	private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
	
	// 计数器
	private ThreadLocal<Integer> counterThreadLocal = new ThreadLocal<Integer>();
	
	@Override
	public boolean tryLock(String key, long timeout, TimeUnit unit) {
		Boolean isLocked = false;
		if (Objects.isNull(threadLocal.get())) {
			String uuid = UUID.randomUUID().toString();
			threadLocal.set(uuid);
			isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
			// 如果获取锁失败,通过自旋尝试获取锁,
			if (!isLocked) {
				for (;;) {
					isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
					if (isLocked) {
						// 获取成功立即退出
						break;
					}
				}
			}
			
			/*
			 *  启动一个线程扮演“看门狗”的角色,不断更新锁索过期时间
			 *  
			 *  注意:这里将stringRedisTemplate对象传给看门狗
			 */
			new Thread(new WatchDogThread(uuid, stringRedisTemplate, key)).start();
		} else {
			isLocked = true;
		}
		
		// 加锁成功后
		if (isLocked) {
			Integer curCount = counterThreadLocal.get() == null ? 0 : counterThreadLocal.get();
			counterThreadLocal.set(curCount++);
		}
		return isLocked;
	}

	@Override
	public void releaseLock(String key) {
		// 保证解锁和加锁线程是同一个,防止避免了一个线程对程序进行了加锁操作后,其他线程对这个锁进行了解锁操作的问题
		String uuid = stringRedisTemplate.opsForValue().get(key);
		if (!Objects.isNull(threadLocal.get()) && 
				threadLocal.get().equals(uuid)) {
			Integer curCount = counterThreadLocal.get();
			if (Objects.isNull(curCount) || (--curCount)<=0) {
				stringRedisTemplate.delete(key);
				// 获取对应的看门狗线程的ID
				String watchDogThreadIdStr = stringRedisTemplate.opsForValue().get(uuid);
				// 获取看门狗
				Thread watchDogThread = ThreadUtils.getThreadByThreadId(Long.valueOf(watchDogThreadIdStr));
				
				if (!Objects.isNull(watchDogThread)) {
					// 终端看门狗
					watchDogThread.interrupt();
					stringRedisTemplate.delete(uuid);
				}
			} 
		}
	}
}

 

2.3 看门狗线程

        看门狗线程,更新锁的超时时间,保证锁的释放一定是在加锁线程业务代码执行完毕之后,
防止在业务处理过程中锁超时失效,其他线程依旧能够获取到锁。

package com.example.demo.redis.lock;

import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * 
* @ClassName: WatchDogThread
* @Description: 看门狗线程,更新锁的超时时间,保证锁的释放一定是在加锁线程业务代码执行完毕之后,
* 				防止在业务处理过程中锁超时失效,其他线程依旧能够获取到锁。
* @Author: liulianglin
* @DateTime 2022年8月31日 上午9:33:07
 */
public class WatchDogThread implements Runnable{
	private String uuid;
	private String key;
	private StringRedisTemplate stringRedisTemplate;
	
	
	public WatchDogThread(String uuid, StringRedisTemplate stringRedisTemplate, String key) {
		this.uuid = uuid;
		this.stringRedisTemplate = stringRedisTemplate;
		this.key = key;
	}
	
	@Override
	public void run() {
		/*
		 *  以uuid为key,将当前线程的ID作为value保存到Redis中
			在RedisDistributeLockImpl删除锁时需要通过uuid获取到当前线程ID,然后停止当前看门狗线程。
		 */
		stringRedisTemplate.opsForValue().set(uuid, String.valueOf(Thread.currentThread().getId()));
		
		// 循环更新所的过期时间
		while(true) {
			stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);
			try {
				// 每秒执行1次
				TimeUnit.SECONDS.sleep(1);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
	
}

2.4 线程操作工具类

        主要是通过线程ID获取线程,目前只有一个方法,比较简单

package com.example.demo.redis.lock;

public class ThreadUtils {
	
	/**
	 * 
	* @Description: 通过ThreadID获取线程对象 
	* @Author: liulianglin
	* @Datetime: 2022年8月31日 上午10:00:54
	* @param threadId
	* @return Thread
	* @throws
	 */
	public static Thread getThreadByThreadId(long threadId) {
		ThreadGroup group = Thread.currentThread().getThreadGroup();
        while(group != null) {
            Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
            int count = group.enumerate(threads, true);
            for(int i = 0; i < count; i++) {
                if(threadId == threads[i].getId()) {
                    return threads[i];
                }
            }
            group = group.getParent();
        }
        return null;
	}
}

 

 完毕。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值