怎样实现Redis分布式锁

58 篇文章 0 订阅
4 篇文章 0 订阅

对于某个JVM进程,要保证某个操作的唯一执行,可以使用synchronized关键字或ReentrantLock在执行前加锁,对于多个JVM进程,要保证这个操作在多个进程中的唯一执行,那就需要依赖第三方系统,例如DB,for update nowait等,除此之外,还可以借助redis、zookeeper实现分布式锁。

目录

测试代码

实现一

实现二

实现三

呼哈哈


Redis锁实现思路

业务操作会有编号m,线程1往redis中set一个key是m的数据,表示m操作已经加锁,别的线程判断如果redis中已经有了key为m的数据,不再执行操作,线程1执行完毕删除此数据,即m操作释放锁。

测试代码

 50个线程同时抢锁

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestRedisLock {

    @Test
    public void test_redislock() throws InterruptedException {
        Thread[] threads = new Thread[50];
        CountDownLatch cdl = new CountDownLatch(50);

        String busiID = "lxyceshi";
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    DistributedLock redisLock = new RedisLock1(busiID);
                    if (!redisLock.tryLock()) { // 尝试加锁
                        System.out.println("线程" + Thread.currentThread().getId() + "获取锁失败!");
                        cdl.countDown();
                        return;
                    }
                    try {
                        System.out.println("线程" + Thread.currentThread().getId() + "成功获取到锁,开始执行任务!");
                        try {
                            // 业务操作睡一会
                            Thread.sleep(200);
                        } catch (InterruptedException e) {}
                    } finally {
                        redisLock.unlock(); // 解锁
                    }
                    cdl.countDown();
                }
            });
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }

        cdl.await();
    }
}

实现一

很简单,判断不存在后set,解锁直接删除key

public class RedisLock1 implements DistributedLock{
	// redis锁的前缀
	private static String redisLockPrefix = "lock_";

	// 单例的redis操作工具对象,只是把redisTemplete套了一层
	private RedisUtil redisUtil = RedisUtil.getInstance();

	// 业务号,加锁的对象
	private String busiId;

	public RedisLock1(String busiId) {
		if (StringUtils.isEmpty(busiId)) {
			throw new IllegalArgumentException("RedisLock1.new : busiId should not be empty");
		}
		this.busiId = busiId;
	}

	@Override
	public void lock() throws AppException {
		if (!tryLock()) {
			throw new AppException("获取【" + busiId + "】锁失败,业务正在执行中,请稍后再试!");
		}
	}

	@Override
	public boolean tryLock() {
		String lockKey = redisLockPrefix + busiId;// 锁key

		// 是否已经被加锁
		boolean exists = redisUtil.exists(lockKey);
		if (!exists) {
			// 不存在key表示可以加锁
			redisUtil.set(lockKey, "");
		}
		return !exists;
	}

	@Override
	public void unlock(){
		String lockKey = redisLockPrefix + busiId;// 锁key
		// 删除锁
		redisUtil.del(lockKey);
	}
}

测试运行情况

结果如下,惨不忍睹跟没写一样:

缺陷

1、严重!非原子性操作,多个线程同时判断exists为不存在,同时走到了if判断中进行set,错误的以为自己获取到了锁

2、严重!任何线程都可以解开不属于自己的锁,只要是调用了unlock()

3、不可重入,同线程再次调用lock()会报错

4、死锁,程序执行了lock,宕机没有执行unlock,进入死锁状态需要人为干预

实现二

对实现一的缺陷1改进,就需要使用原子性进行exists判断和set加锁,redis提供了一个操作setNX,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果,即redisTemplete.setIfAbsent

对实现一的缺陷2改进,也就是锁需要有所属标识,在删除的时候判断是否是自己的锁,是再执行删除,方案:使用当前线程id作为锁的value,删除时判断,但是redis并没有提供类似setNX的对应删除操作,这里的判断为了保证原子性,使用lua脚本实现

对实现一的缺陷3改进,锁需要有计数,加锁操作计数加一,解锁操作计数减一,计数为0时删除锁

,方案:锁使用hash结构,key为业务操作编号,hashkey为当前线程id,hashvalue为计数

对实现一的缺陷4改进,锁需要定义超时时间,超时则自动删除锁,这里引入了超时时间,也就引入了一个新问题,加入业务操作耗时较长,大于超时时间,锁此时超时删除是错误的,也就是需要增加锁延时机制,定时判断当前锁业务是否结束,未结束则需要将锁续期。

这里实现二提供一个对于缺陷1,2的改进版,这版对要求不高的场景已经算可以了。3,4的改进需要改数据结构,对value计数加减等,一个setIfAbsent实现不了,有较大改动,放到实现三。

public class RedisLock2 implements DistributedLock{
	// redis锁的前缀
	private static String redisLockPrefix = "lock_";

	// 单例的redis操作对象
	private RedisUtil redisUtil = RedisUtil.getInstance();

	// 产生个uuid作为本应用jvm的标识
	private static String jvmID = UUID.randomUUID().toString();

	// 业务号,加锁的对象
	private String busiId;

	public RedisLock2(String busiId) {
		if (StringUtils.isEmpty(busiId)) {
			throw new IllegalArgumentException("RedisLock2.new : busiId should not be empty");
		}
		this.busiId = busiId;
	}

	@Override
	public void lock() throws AppException {
		if (!tryLock()) {
			throw new AppException("获取【" + busiId + "】锁失败,业务正在执行中,请稍后再试!");
		}
	}

	@Override
	public boolean tryLock() {
		String lockKey = redisLockPrefix + busiId;// 锁key
		String lockValue = jvmID + Thread.currentThread().getId();// 锁value = static uuid + 线程id

		// redisTemplete.setIfAbsent 就是  setNX
		return redisUtil.setIfAbsent(lockKey, lockValue);
	}

	@Override
	public void unlock(){
		String lockKey = redisLockPrefix + busiId;// 锁key
		String lockValue = jvmID + Thread.currentThread().getId();// 锁value = static uuid + 线程id

		// 删除锁, 脚本中先get判断是否为当前线程加的锁,如果是del,否则返回
		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
		redisScript.setScriptText(script);
		redisScript.setResultType(Long.class);
		redisUtil.execute(redisScript, Collections.singletonList(lockKey), new String[]{lockValue});
	}
}

运行测试如下,解决!还剩下死锁和重入!

实现三

解决死锁和可重入需增加超时时间和更换为hash结构

加锁流程:

  1. 判断是否已经加锁如果没有锁,设置key,hashkey为当前线程,hashvalue为1,设置超时时间,完
  2. 如果已经加锁,且是自己加的,hashvalue + 1,重置超时时间
  3. 如果已经加锁,别人加的,返回当前剩余的超时时间

解锁流程:

  1. 判断锁是否是自己加的,不是则return
  2. 是自己加的,hashvalue -1 ,判断如果为0,删除锁key

锁续期:

当线程加锁成功后,开启定时任务(间隔需要比超时时间小),定时判断该线程是否还拥有此锁,即hashvalue为此线程的锁是否还存在,若存在表示线程还未执行完,还未释放锁,那么重置超时时间,如果不存在,表示执行完不需要续期,不再执行定时任务

以上操作均使用lua脚本保证原子性,延时任务的执行使用ScheduledThreadPoolExecutor 执行器定时执行,参考redssion实现

public class RedisLock implements DistributedLock{

	// 默认30秒后自动释放锁
	private static long defaultExpireTime = 30 * 1000;// 30秒

	// redis锁的前缀
	private static String redisLockPrefix = "lock_";

	// 用于锁延时任务的执行
	private static ScheduledThreadPoolExecutor renewExpirationExecutor;
	
	// 加锁和解锁的lua脚本
	private static String lockScript;
	private static String unlockScript;
	// 锁延时脚本
	private static String renewScript;

	static {
		StringBuilder sb = new StringBuilder();
		sb.setLength(0);
		sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 如果不存在这个lockKey锁
		sb.append("     redis.call('hset', KEYS[1], ARGV[1], 1) ");// 设置锁 ,hash结构,hashkey为当前线程id,加锁数为1
		sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 设置锁超时时间
		sb.append("     return nil ");
		sb.append(" end ");
		sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then ");// 如果当前线程已经加锁
		sb.append("     redis.call('hincrby', KEYS[1], ARGV[1], 1) ");// 可重入,增加锁计数
		sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重设置锁超时时间
		sb.append("     return nil ");
		sb.append(" end ");
		sb.append(" return redis.call('pttl', KEYS[1]) ");// 如果别的线程已经加锁,返回剩余时间
		lockScript = sb.toString();
		
		sb.setLength(0);
		sb.append(" if (redis.call('exists', KEYS[1]) == 0) then ");// 不存在锁,返回1表示解锁成功
		sb.append("     return 1 ");
		sb.append(" end ");
		sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then ");// 存在锁,不是本线程加的,返回0失败
		sb.append("     return 0 ");
		sb.append(" end ");
		sb.append(" local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1) ");// 存在自己加的锁,锁计数减一
		sb.append(" if (counter > 0) then ");// 判断是否要删除锁,或重置超时时间
		sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");
		sb.append("     return 0 ");
		sb.append(" else ");
		sb.append("     redis.call('del', KEYS[1]) ");
		sb.append("     return 1 ");
		sb.append(" end ");
		sb.append(" return nil ");
		unlockScript = sb.toString();
		
		sb.setLength(0);
		sb.append(" if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then ");// 锁还存在
		sb.append("     redis.call('pexpire', KEYS[1], ARGV[2]) ");// 重置超时时间
		sb.append("     return 1 ");
		sb.append(" end ");
		sb.append(" return 0 ");
		renewScript = sb.toString();
		
		renewExpirationExecutor = new ScheduledThreadPoolExecutor(2);
	}

	// 单例的redis操作对象
	private RedisUtil redisUtil = RedisUtil.getInstance();
	
	// 业务编号,加锁的对象
	private String busiId;
	
	public RedisLock(String busiId) {
		if (StringUtils.isEmpty(busiId)) {
			throw new IllegalArgumentException("RedisLock实例化出错,业务编号为空");
		}
		this.busiId = busiId;
	}

	@Override
	public boolean tryLock() {
		String lockKey = redisLockPrefix + busiId;// 锁key
		long threadId = Thread.currentThread().getId();// 当前线程id
		String lockValue = redisUtil.getId().toString() + threadId;// 相当于jvmID+线程id
		
		DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
		redisScript.setScriptText(lockScript);
		redisScript.setResultType(Long.class);
		Long result = redisUtil.execute(redisScript, Collections.singletonList(lockKey), new Object[]{lockValue, defaultExpireTime});
		
		boolean isSuccess = result == null;
		if (isSuccess) {
			// 若成功,增加延时任务
			scheduleExpirationRenew(threadId);
		}
		
		return isSuccess;
	}

	@Override
	public void unlock(){
		String lockKey = redisLockPrefix + busiId;// 锁key
		long threadId = Thread.currentThread().getId();// 当前线程id
		String lockValue = redisUtil.getId().toString() + threadId;// 相当于jvmID+线程id
		
		DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
		redisScript.setScriptText(unlockScript);
		redisScript.setResultType(Long.class);
		redisUtil.execute(redisScript, Collections.singletonList(lockKey), new Object[]{lockValue, defaultExpireTime});
	}

	@Override
	public void lock() throws AppException {
		if (!tryLock()) {
			throw new AppException("获取【" + busiId + "】锁失败,正在执行中,请稍后再试!");
		}
	}

	/**
	 * 锁延时,定时任务队列,超时默认30s。每隔10s判断一次是否续期
	 */
	private void scheduleExpirationRenew(final long threadId) {
		Runnable renewTask = new Runnable(){
			
			@Override
			public void run() {
				String lockKey = redisLockPrefix + busiId;// 锁key
				String lockValue = redisUtil.getId().toString() + threadId;// 相当于jvmID+线程id
				
				DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
				redisScript.setScriptText(renewScript);
				redisScript.setResultType(Long.class);
				Long result = redisUtil.execute(redisScript, Collections.singletonList(lockKey), new Object[]{lockValue, defaultExpireTime});
			
				if (result == 1) {
					// 延时成功,再定时执行
					scheduleExpirationRenew(threadId);

					logger.info("redis锁【" + lockKey + "】延时成功!");
				}
			}
		};
		
		renewExpirationExecutor.schedule(renewTask, defaultExpireTime / 3, TimeUnit.MILLISECONDS);
	}
}

 把测试代码业务操作耗时睡眠改为睡30s,运行,很完美

呼哈哈

至此,假如生产是单机模式的redis,实现三已经可以算得上是满足需要。

但它真的是完美的吗,试想,假如线程1拿到锁后,延时任务因为执行器执行耗时任务迟迟得不到执行或者系统长时间GC导致STW,锁超时删除后,线程1继续执行,而线程2依旧拿到了锁,此时两个线程都认为拥有锁从而导致发生了错误。

若是集群模式,主从同步也可能会造成锁丢失

线程1set加锁命令在主库执行,成功获取锁,此时主库宕机,set命令还未同步到从库,从库升级为主库,导致锁丢失,此时另外线程可以再次成功获取锁。

官方给出了一种解决算法,红锁Redlock,Redission就支持红锁。因为目前项目中基本不会对正确性要求很高,单机已经够用,即使为了高可用部署主从,偶尔出现锁丢失也没什么大不了的。所以没有仔细了解实现和写demo,官方介绍

红锁的思想就是主从异步同步会导致数据丢失,那就不同步不部署从库,只部署独立的主库,官方建议部署至少5个主库,获取锁时,向5个主库同时申请锁,如果大多数过半(3)个库都返回加锁成功,且申请锁耗费的时间少于锁超时时间则加锁成功。解锁时将向所有节点发起解锁请求。

因此现在实现三在极端情况下,也不保证是一定安全的,如果应用中对正确性要求很高,那么除了redis分布式锁,在资源层也需要加点手段保证唯一性,例如数据库更新时,版本号之类的控制。这样redis将大部分情况拦截在上层,资源层也进行控制,这样能满足大部分场景。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring提供了对Redis分布式锁的支持,可以通过Spring的RedisTemplate或LettuceConnectionFactory来实现。下面是一个简单的实现示例: 1. 首先,在Spring配置文件中配置Redis连接池: ``` <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory"> <constructor-arg name="hostName" value="${redis.host}"/> <constructor-arg name="port" value="${redis.port}"/> <constructor-arg name="password" value="${redis.password}"/> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="redisConnectionFactory"/> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> </bean> ``` 2. 然后,定义一个Redis分布式锁的工具类: ``` @Component public class RedisLockUtil { private static final long LOCK_EXPIRE_TIME = 30000; // 锁过期时间,单位毫秒 private static final String LOCK_PREFIX = "lock:"; // 锁前缀 @Autowired private RedisTemplate<String, String> redisTemplate; public boolean lock(String key) { String lockKey = LOCK_PREFIX + key; long now = System.currentTimeMillis(); long expireTime = now + LOCK_EXPIRE_TIME; Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, String.valueOf(expireTime)); if (result != null && result) { redisTemplate.expire(lockKey, LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS); return true; } String oldExpireTime = redisTemplate.opsForValue().get(lockKey); if (oldExpireTime != null && Long.parseLong(oldExpireTime) < now) { String newExpireTime = redisTemplate.opsForValue().getAndSet(lockKey, String.valueOf(expireTime)); if (newExpireTime != null && newExpireTime.equals(oldExpireTime)) { redisTemplate.expire(lockKey, LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS); return true; } } return false; } public void unlock(String key) { String lockKey = LOCK_PREFIX + key; redisTemplate.delete(lockKey); } } ``` 该工具类定义了两个方法:lock和unlock。其中,lock方法实现Redis分布式锁的获取,unlock方法实现Redis分布式锁的释放。在lock方法中,首先将锁的过期时间设置为当前时间+30秒,并通过Redis的setIfAbsent方法尝试获取锁;如果获取成功,则返回true;否则,通过get方法获取锁的过期时间,如果锁已经过期,则通过getAndSet方法更新锁的过期时间并获取旧的过期时间,然后判断旧的过期时间是否等于获取到的过期时间,如果相等,则说明获取到了锁,返回true。在unlock方法中,直接通过delete方法删除锁。 3. 最后,在需要使用分布式锁的地方,注入RedisLockUtil即可使用分布式锁: ``` @Autowired private RedisLockUtil redisLockUtil; public void doSomethingWithLock(String key) { if (redisLockUtil.lock(key)) { try { // 获取锁成功后执行业务逻辑 // ... } finally { redisLockUtil.unlock(key); } } else { // 获取锁失败后的处理 // ... } } ``` 上面是一个简单的Spring实现Redis分布式锁的示例,如果您有更多的问题,请继续提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值