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的哨兵模式,尽管一主多从,但从库在主库正常时,不对外提供读、写服务,在主从切换时,可能会有小概率问题,但这是小概率事件,因此满足条件。