分布式锁-java

本文介绍了常见的分布式锁解决方案,包括基于DB、Zookeeper和Redis的实现。重点讨论了Redis分布式锁,并提供了源码,包括初版和优化版,优化版中通过自动顺延锁定时间来防止过早释放锁的问题。同时,提到了在选择Redis作为分布式锁基础服务时,需要考虑数据一致性的问题,如使用哨兵模式确保主从数据同步。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 简述

分布式系统,常需要协调对共享资源的处理,避免冲突导致的各种问题。

常见的分布式锁方案,有基于DB、Redis、zookeeper等,在"方案" 部分会简单描述。

既然是锁,就存在锁定时长问题,不同业务场景,有的毫秒级,有的几分钟,几小时都有可能,例如有些计划任务,如需排他处理,锁的时间就会长一些。

本文只对常见的redis分布式锁整理,提供了源码,对"锁定时间"过期,可能导致的"失锁"问题,采用"自动顺延"方法,做了优化处理。

2 方案

2.1 DB

在数据库创建一个锁表lock_table,通常会包含以下几个字段:

lock_business_id(业务ID)
lock_stat(锁状态:0 未锁 1 已锁)
lock_time(锁定时间)
version(版本)

其中一条记录表示一个排它锁,用字段lock_business_id对应相关资源, lock_stat表示锁状态,再通过version字段,采用乐观锁更新方式,来保证处理的原子性。

特点:
适用于低并发场景,只保证了基本的排他性处理。

2.2 zookeeper

对zookeeper来说,开源客户端curator,已提供多种实用的锁工具类,如:

InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁

特点: 
不仅提供对资源的排他性处理,还对抢占资源的队列进行了排队,先到先的,很是公平。

2.3 redis

通过redis提供的SETNX命令,该命令只有key不存在时才能成功赋值,通过该特性,正好符合锁的原子性操作特点,具体实现,参考"源码"部分内容。

提示:  redis在2.6.12版后,对set命令做了参数扩充,允许同时使用EX、NX参数,例如:key value EX 100 NX,对锁逻辑支持的更友好。

3 源码

3.1 redis锁-初版

redis客户端操作,一般直接采用spring封装的模板类就好,就不必关心具体客户端是jedis,还是lettuce实现,这部分就信任、交给spring处理吧。

通常为避免"死锁",在锁定时,都会添加"过期时间",以下是源码,包含两个类:RedisLock,RedisLockImpl,其中expire方法,在初版没意义,在"优化版"部分,进行自动顺延过期时间时才会用到, 细节如下:

package mtr.demo.distributed.lock;

public interface RedisLock {
	boolean lock(String key, long seconds);
	void unLock(String key);
	boolean expire(String key, long seconds);
}
package mtr.demo.distributed.lock;

import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

import mtr.demo.distributed.lock.RedisLock;

public class RedisLockImpl implements RedisLock {
	private static final Logger logger = LoggerFactory.getLogger(RedisLockImpl.class);
	
	private static final String LOCK_VALUE = "mtr_lock";
	private StringRedisTemplate stringRedisTemplate;
	
	public RedisLockImpl(StringRedisTemplate stringRedisTemplate) {
		this.stringRedisTemplate = stringRedisTemplate;
	}

	@Override
	public boolean lock(String key, long seconds) {
		return this.stringRedisTemplate.opsForValue().setIfAbsent(key, LOCK_VALUE, seconds, TimeUnit.SECONDS);
	}

	@Override
	public void unLock(String key) {
		boolean result = this.stringRedisTemplate.delete(key);
		if (!result) {
			logger.warn("RedisLock.unLock not find key:{}", key);
		}
	}

	@Override
	public boolean expire(String key, long seconds) {
		return this.stringRedisTemplate.expire(key, seconds, TimeUnit.SECONDS);
	}

}

3.2 redis锁-优化版

初版已满足大部分业务场景需求,但有时候"锁定时间"不太容易估算,而又不想把"过期时间"设置的太长,以免进程强制退出等场景,没有解锁,导致长时间无法获取锁。

这时候可以首先设置一个相对合理的"过期时间",如果业务偶发性执行时间超时(尤其是通过网络访问一些资源性服务时,还是很常见的), 这时可以采用  "自动顺延"锁定时间的策略,来避免锁定超时。

其中jdk中ScheduledThreadPoolExecutor很合适满足这部分逻辑要求,但实现中还是需要一些注意事项,比如主任务的"解锁逻辑",与计划任务中的锁"延时"处理,需要保证二者的有序性、原子性;其次,采用滞后方式删除解锁任务。

具体实现,保留初版的源码不变,只增加一个AutoDelayRedisLock类, 其中LockConfig类是一个配置类,具体如下:

package mtr.demo.distributed.lock;

import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/*
 * 自动延时redis锁
 * 
 */
public class AutoDelayRedisLock implements java.io.Closeable {	
	private static final Logger logger = LoggerFactory.getLogger(AutoDelayRedisLock.class);
	
	private final static long DEFAULT_MAX_DELAY_SECONDS = 3600;
	private final static long DEFAULT_DELAY_TIMES = 5;
	private final static int DEFAULT_CORE_POOL_SIZE = 2;
		
	private RedisLock redisLock = null;	
	public ScheduledThreadPoolExecutor scheduledExecutor = null;
	
	public AutoDelayRedisLock(RedisLock redisLock) {
		this(redisLock, DEFAULT_CORE_POOL_SIZE);
	}
	
	public AutoDelayRedisLock(RedisLock redisLock, int corePoolSize) {
		this.redisLock = redisLock;
		this.scheduledExecutor = (ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(corePoolSize);			
	}
	
	public DelayLock lock(String key, long seconds) {
		long maxDelaySeconds = 	seconds * DEFAULT_DELAY_TIMES;
		maxDelaySeconds = maxDelaySeconds > DEFAULT_MAX_DELAY_SECONDS ? maxDelaySeconds : DEFAULT_MAX_DELAY_SECONDS;
		return this.lock(key, seconds, maxDelaySeconds);
	}
	
	public DelayLock lock(String key, long seconds, long maxDelaySeconds) {
		
		// check paramers
		if (StringUtils.isBlank(key) || seconds <= 0 || maxDelaySeconds <= 0 || seconds > maxDelaySeconds) {
			throw new java.lang.IllegalArgumentException(String.format("AutoDelayRedisLock check paramers fail:%s, %s, %s", key, seconds, maxDelaySeconds));
		}
		
		// redis lock
		if (!this.redisLock.lock(key, seconds)) return null;
		
		// delay lock
		DelayLock delayLock = new DelayLock(key, seconds, maxDelaySeconds, this.redisLock);
		
		// delay task
		this.addDelayTask(delayLock);
		
		return delayLock;
	}

	private void addDelayTask(DelayLock delayLock) {
		DelayTask delayTask = new DelayTask(delayLock);
		this.scheduledExecutor.schedule(delayTask, delayLock.calculateScheduleSeconds(), TimeUnit.SECONDS);
	}
	
//	private void removeDelayTask(ScheduledFuture<?> scheduledFuture) {
//		boolean result = this.scheduledExecutor.getQueue().remove(scheduledFuture);
//		if (!result) {
//			System.out.println("removeDelayTask is null");
//		}
//	}

	private class DelayTask implements Runnable {
		
		private DelayLock delayLock;
		
		DelayTask(DelayLock delayLock) {
			this.delayLock = delayLock;
		}

		@Override
		public void run() {
			try {
				this.doProcess();
			} catch (Throwable t) {
				logger.error("DelayTask run exception", t);
			}
		}
		
		private void doProcess() {
			
			if (!this.delayLock.isDelay()) {
				logger.debug("DelayTask stop, key:{}", delayLock.key);
				return;
			}
			
			if (this.delayLock.doDelay()) {			
				DelayTask delayTask = new DelayTask(delayLock);
				AutoDelayRedisLock.this.scheduledExecutor.schedule(delayTask, delayLock.calculateScheduleSeconds(), TimeUnit.SECONDS);
			} else {
				logger.warn("DelayTask.doDelay stop, key:{}", delayLock.key);
			}
		}			
	}

	public static class DelayLock {

		private final String key;
		private final long delaySeconds;
		private final long maxDelaySeconds;
		
		private final long createTime;
		private final RedisLock redisLock;
		
		private boolean isUnLock = false;
		
		public long debugCount = 0;
		
		public DelayLock(String key, long delaySeconds, long maxDelaySeconds, RedisLock redisLock) {
			this.key = key;
			this.delaySeconds = delaySeconds;
			this.maxDelaySeconds = maxDelaySeconds;
			
			this.redisLock = redisLock;
			this.createTime = System.currentTimeMillis();
		}
				
		public synchronized boolean doDelay() {
			if (isUnLock) return false;
				
			boolean result = this.redisLock.expire(key, delaySeconds);
			if (!result) {
				logger.warn("DelayLock.doDelay, no find key:{}", key);
			}
			return result;
		}
		
		public synchronized void unLock() {
			this.isUnLock = true;
			this.redisLock.unLock(key);
		}
		
		public synchronized boolean isDelay() {
			boolean overtimed = System.currentTimeMillis() > (createTime + maxDelaySeconds * 1000L);
			return !(overtimed || this.isUnLock);
		}
		
		public long calculateScheduleSeconds() {
			return delaySeconds / 2;
		}

		@Override
		public String toString() {
			return "DelayLock [key=" + key + ", delaySeconds=" + delaySeconds + ", maxDelaySeconds=" + maxDelaySeconds
					+ ", createTime=" + createTime + ", redisLock=" + redisLock + ", isUnLock=" + isUnLock + "]";
		}
	}

	@Override
	public void close() throws IOException {
		this.redisLock = null;
		this.scheduledExecutor.shutdownNow();		
	}
}
package com.demo.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

import mtr.demo.distributed.lock.AutoDelayRedisLock;
import mtr.demo.distributed.lock.RedisLock;
import mtr.demo.distributed.lock.RedisLockImpl;

@Configuration
public class LockConfig {
	
	@Bean
	public RedisLock redisLock(StringRedisTemplate stringRedisTemplate) {		
		return new RedisLockImpl(stringRedisTemplate);
	}
	
	@Bean
	public AutoDelayRedisLock autoDelayRedisLock(RedisLock redisLock) {		
		return new AutoDelayRedisLock(redisLock, 2);
	}
        
}

4 注意

大家选redis做分布式锁的基础服务时,是不是会有一个“担忧“,如果redis是集群,是读写分离,多个redis实例,那么数据不一致就不可避免,还有什么“锁”可言,的确如此。

如果基于redis做分布式锁,那就必须保证同一份数据,同一时间,只有一个redis提供数据服务,例如:redis的哨兵模式,尽管一主多从,但从库在主库正常时,不对外提供读、写服务,在主从切换时,可能会有小概率问题,但这是小概率事件,因此满足条件。

### Redission 可重入分布式锁的实现机制 #### 背景介绍 在分布式环境中,为了防止多个节点同时操作共享资源而导致数据不一致的问题,通常需要引入一种同步控制机制——分布式锁Redission 是 Redis 官方推荐的一种高效、易用的分布式锁解决方案[^1]。 #### 可重入锁的概念 可重入锁是指同一个线程可以多次获取同一把锁而不发生死锁的情况。这种特性允许线程在其已经持有的锁内部再次申请该锁,而不会因为重复加锁导致阻塞。Redission 的 `RLock` 接口实现了这一功能,并通过 Redis 的原子性和 Lua 脚本保障其一致性[^4]。 --- #### Redission 可重入锁的核心实现原理 ##### 1. 锁的状态管理 Redission 使用 Redis 中的 Hash 数据结构来存储锁的信息。Hash 结构中的字段如下: - **Key**: 表示锁的存在与否。 - **Field (小 Key)**: 存储持有锁的客户端唯一标识符(通常是线程 ID 或会话 ID)。 - **Value**: 记录锁的持有次数(即重入计数器)。每次线程重新进入锁时,此值都会增加;当线程退出锁时,此值减少直到变为零,则释放锁[^3]。 ##### 2. 加锁流程 以下是 Redission 获取可重入锁的主要逻辑: ```java // 尝试获取锁 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); ``` - 当某个线程调用 `tryLock()` 方法时,Redission 首先会在 Redis 上创建一个带有过期时间的键值对作为锁。 - 如果当前线程已经是锁的拥有者(即 Field 对应的 Value 已存在),则直接更新重入计数器并延长锁的有效期。 - 否则,尝试设置新的锁记录。如果成功,则初始化重入计数器为 1。 Lua 脚本用于确保整个过程的原子性,避免因网络延迟或其他异常情况引发的竞争条件。 ##### 3. 解锁流程 解锁的过程同样依赖于 Lua 脚本来完成: ```java lock.unlock(); ``` - 线程在调用 `unlock()` 方法时,首先检查自己是否是锁的实际持有者。 - 若确实是持有者,则将对应的重入计数器减一。 - 如果计数器降为零,则删除锁的相关记录,正式释放锁。 需要注意的是,只有真正的锁持有者才能执行解鎖动作,其他线程即使知道锁的名字也无法非法解除他人拥有的锁。 --- #### 技术细节补充 - **锁超时保护** Redission 设定了一个合理的默认超时期限,在超过该期限后未续期的情况下自动释放锁,从而避免死锁的发生。 - **高可用支持** 利用了 Redis Sentinel 或 Cluster 模式下的选举算法,使得即便部分 Redis 实例宕机也不会影响整体服务正常运行[^2]。 - **公平性与性能权衡** Redission 并未严格遵循 FIFO 的排队策略,而是优先考虑效率和吞吐量。因此,在极端负载下可能会出现轻微的不公平现象。 --- ### 总结 综上所述,Redission 的可重入分布式锁基于 Redis 的高性能特性和 Lua 脚本的一致性保障,提供了一种简单可靠的方式解决跨进程间的资源共享冲突问题。它的设计既兼顾了灵活性又不失安全性,非常适合现代微服务架构的需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值