面试总是会被问到有没有用过分布式锁、redis 锁,大部分平时很少接触到,所以只能很无奈的回答 “没有”。本文通过 Spring Boot 整合 redisson 来实现分布式锁,并结合 demo 测试结果。
首先看下大佬总结的图:
下面介绍实现方式:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>
<!--自动生成get set-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
说明:
实现基础功能只需要使用到前两个redis, redisson 的依赖
加lombok依赖是为了方便实用@Slf4j注解快速实现log日志的打印
加入swagger2是为了方便接口测试使用,实际也可以使用postman工具进行测试,不一定非要使用swagger2
spring:
redis:
host: localhost
port: 6379
password: 12345
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 100
# 连接池中的最小空闲连接
max-idle: 10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接超时时间(毫秒)
timeout: 5000
# 默认是索引为0的数据库
database: 0
由于我们使用的是springboot,所以使用javaconfig文件配置属性,尽量不使用xml配置文件
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
Config config =new Config();
//单节点
config.useSingleServer().setAddress("redis://"+host+":"+port);
if (StringUtils.isEmpty(password)){
config.useSingleServer().setPassword(null);
}else {
config.useSingleServer().setPassword(password);
}
// //添加主从配置
// config.useMasterSlaveServers()
// .setMasterAddress("")
// .setPassword("")
// .addSlaveAddress("","");
// //集群模式配置 setScanInterval() 扫描间隔时间,单位是毫秒
// //可以使用"redis://" 来启用SSL连接
// config.useClusterServers()
// .setScanInterval(2000)
// .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
// .addNodeAddress("redis://127.0.0.1:7002");
return Redisson.create(config);
}
}
注意: redisson 他实际上支持主从配置模式和集群模式的部署方式,为了方便测试,我只使用了单节点的方式进行配置,其他两种配置 注释掉了.
接口
/**
* @Author: shangjp
* @Email: [email protected]
* @Date: 2020/5/20 11:08
* @Description: 底层封装
*/
public interface DistributedLocker {
RLock lock(String lockKey);
RLock lock(String lockKey, int timeout);
RLock lock(String lockKey, TimeUnit unit, int timeout);
boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime);
void unlock(String lockKey);
void unlock(RLock lock);
/**
* 获取计数器
*
* @param name
* @return
*/
RCountDownLatch getCountDownLatch(String name);
/**
* 获取信号量
*
* @param name
* @return
*/
RSemaphore getSemaphore(String name);
}
实现类
/**
* @Author: shangjp
* @Email: [email protected]
* @Date: 2020/5/20 11:15
* @Description:
*/
@Service
public class RedisDistributedLocker implements DistributedLocker {
@Autowired
private RedissonClient redissonClient;
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
/**
* 加锁
* @param lockKey key
* @param leaseTime 持有锁的最长时间(单位默认是秒)
*/
@Override
public RLock lock(String lockKey, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime,TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime,unit);
return lock;
}
/**
* 尝试加锁--出错会抛异常
* @param lockKey key
* @param unit 单位
* @param waitTime 请求获取锁的最大超时时间
* @param leaseTime 上锁后对锁的最长的持有时间
* @return
*/
@Override
public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime,leaseTime,unit);
} catch (InterruptedException e) {
return false;
}
}
/**
* 解锁
* @param lockKey key
*/
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
/**
* 获取计数器
*
* @param name
* @return
*/
@Override
public RCountDownLatch getCountDownLatch(String name) {
return redissonClient.getCountDownLatch(name);
}
/**
* 获取信号量
*
* @param name
* @return
*/
@Override
public RSemaphore getSemaphore(String name) {
return redissonClient.getSemaphore(name);
}
}
/**
* @Author: shangjp
* @Email: [email protected]
* @Date: 2020/5/20 10:57
* @Description: redis分布式锁工具类类
*/
public class RedisLockUtil {
private static RedisDistributedLocker distributedLocker = SpringContextHolder.getBean(RedisDistributedLocker.class);
/**
* 加锁
*
* @param lockKey
* @return
*/
public static RLock lock(String lockKey) {
return distributedLocker.lock(lockKey);
}
/**
* 带超时的锁
*
* @param lockKey
* @param timeout 超时时间 单位:秒
*/
public static RLock lock(String lockKey, int timeout) {
return distributedLocker.lock(lockKey, timeout);
}
/**
* 带超时的锁
*
* @param lockKey
* @param unit 时间单位
* @param timeout 超时时间
*/
public static RLock lock(String lockKey, int timeout, TimeUnit unit) {
return distributedLocker.lock(lockKey, unit, timeout);
}
/**
* 尝试获取锁
*
* @param lockKey
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
return distributedLocker.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
}
/**
* 尝试获取锁
*
* @param lockKey
* @param unit 时间单位
* @param waitTime 最多等待时间
* @param leaseTime 上锁后自动释放锁时间
* @return
*/
public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
return distributedLocker.tryLock(lockKey, unit, waitTime, leaseTime);
}
/**
* 释放锁
*
* @param lockKey
*/
public static void unlock(String lockKey) {
distributedLocker.unlock(lockKey);
}
/**
* 释放锁
*
* @param lock
*/
public static void unlock(RLock lock) {
distributedLocker.unlock(lock);
}
/**
* 获取计数器
*
* @param name
* @return
*/
public static RCountDownLatch getCountDownLatch(String name) {
return distributedLocker.getCountDownLatch(name);
}
/**
* 获取信号量
*
* @param name
* @return
*/
public static RSemaphore getSemaphore(String name) {
return distributedLocker.getSemaphore(name);
}
}
需要注意的一点是:
private static RedisDistributedLocker distributedLocker = SpringContextHolder.getBean(RedisDistributedLocker.class);
这个地方其实是有个坑的,因为我们工具类中的方法都是static静态的但是静态方法却需要调用service中的函数,@Autowired无法注入静态bean,导致方法无法被声明为static。这种情况下找到利用Spring的使用SpringContextHolder工具类的getBean方法来使得service方法能够被声明为静态方法。
/**
* @Author: shangjp
* @Email: [email protected]
* @Date: 2020/5/20 12:05
* @Description: 自定义ContextHolder
*/
@Component
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
/**
* 以静态变量保存ApplicationContext,可在任意代码中取出ApplicaitonContext.
*/
private static ApplicationContext context=null;
/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextHolder.context = applicationContext;
}
public static ApplicationContext getApplicationContext() {
assertContextInjected();
return context;
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
assertContextInjected();
return (T) context.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
assertContextInjected();
return context.getBean(requiredType);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(String name, Class<T> requiredType) {
assertContextInjected();
return context.getBean(name, requiredType);
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
public static void clearHolder() {
context = null;
}
/**
* 实现DisposableBean接口, 在Context关闭时清理静态变量.
*/
@Override
public void destroy() throws Exception {
SpringContextHolder.clearHolder();
}
/**
* 检查ApplicationContext不为空.
*/
private static void assertContextInjected() {
Assert.notNull(context, "applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder");
}
}
到此为止,我们完成基本的配置,下面写一个controller来测试一下
/**
* @Author: shangjp
* @Email: [email protected]
* @Date: 2020/5/20 15:29
* @Description: redis分布式锁控制器
*/
@RestController
@Api(tags = "redisson", description = "redis分布式锁控制器")
@RequestMapping("/redisson")
@Slf4j
public class RedissonLockController {
/**
* 锁测试共享变量
*/
private Integer lockCount = 10;
/**
* 无锁测试共享变量
*/
private Integer count = 10;
/**
* 模拟线程数
*/
private static int threadNum =10;
/**
* 模拟并发测试加锁和不加锁
* 根据打印结果可以明显看到,未加锁的 count-- 后值是乱序的,而加锁后的结果和我们预期的一样。
* 由于条件问题没办法测试分布式的并发。只能模拟单服务的这种并发,但是原理是一样,
* @return
*/
@GetMapping("/test")
@ApiOperation("模拟并发测试加锁和不加锁")
private void lock() {
//计数器
final CountDownLatch countDownLatch =new CountDownLatch(1);
for (int i = 0; i < threadNum; i++) {
MyRunnable myRunnable = new MyRunnable(countDownLatch);
Thread thread = new Thread(myRunnable);
thread.start();
}
//释放所有的线程
countDownLatch.countDown();
}
/**
* 加锁测试
*/
private void testLockCount() {
String lockKey = "lock-test";
try {
RedisLockUtil.lock(lockKey,2, TimeUnit.SECONDS);
lockCount--;
log.info("lockCount值:"+lockCount);
} catch (Exception e) {
log.error(e.getMessage(),e);
}finally {
//必须在finally代码块中释放锁,避免产生死锁
RedisLockUtil.unlock(lockKey);
}
}
/**
* 无锁测试
*/
private void testCount() {
count--;
log.info("count值:"+count);
}
public class MyRunnable implements Runnable {
/**
* 计数器
*/
final CountDownLatch countDownLatch;
public MyRunnable(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//阻塞当前线程,直到计数器的值为0
try {
countDownLatch.await();
} catch (InterruptedException e) {
log.error(e.getMessage(),e);
}
//无锁操作
testCount();
//加锁操作
testLockCount();
}
}
}
启动项目,测试一下:
结果分析:
根据打印结果可以明显看到,未加锁的 count-- 后值是乱序的,而加锁后的结果和我们预期的一样
getLock()其实就是去获取一个锁的实例
@Override
public RLock getLock(String name) {
return new RedissonLock(connectionManager.getCommandExecutor(), name);
}
RedissonLock 对获取到的锁进行初始化
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
//命令执行器
this.commandExecutor = commandExecutor;
//uuid 字符串
this.id = commandExecutor.getConnectionManager().getId();
//内部锁的过期时间
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
加锁
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lock(leaseTime, unit, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
lock(-1, null, true);
}
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
lock(leaseTime, unit, true);
}
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//获取当前线程的id
long threadId = Thread.currentThread().getId();
//尝试获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
//如果获取ttl为空,则说明获取锁成功
if (ttl == null) {
return;
}
//如果获取锁失败,则订阅到对应这个锁的channel
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
//一直尝试获取锁
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
//ttl为空,说明成功获取锁,返回
if (ttl == null) {
break;
}
// ttl>0 则等待ttl时间之后继续尝试重新获取锁
if (ttl >= 0) {
try {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
getEntry(threadId).getLatch().acquire();
} else {
getEntry(threadId).getLatch().acquireUninterruptibly();
}
}
}
} finally {
//取消对channel的订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
获取锁
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
//如果带有过期时间,则按照普通的方式获取锁
if (leaseTime != -1) {
//采用底层加锁方式
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//先按照30秒的过期时间来执行获取锁的方法
RFuture<
Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
//如果还持有这个锁,则开启定时任务不断地去刷新该锁的过期时间
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
下面我们看一下底层加锁的逻辑:实际上还是使用了lua脚本实现的原子性操作,他使用的是hash数据结构
主要是判断锁是否存在,如果存在的话就设置过期时间,然后对比一下线程id,如果是同一个线程,证明可以
重入,如果所存在,但不是当前线程持有锁,证明别人还没有释放锁,就把剩余的时间返回,加锁失败
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
//过期时间
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//如果锁不存在,则通过hset设置它的值,并设置过期时间
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果锁已经存在了,并且是当前线程持有锁,则通过hincrby给数值加1
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果锁存在,但不是当前线程持有锁,则返回所得过期时间pttl
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
解锁
解锁时一样要去判断,是否是当前线程持有锁,是同一个线程则锁计数器减1,如果锁计数器的值大于0,说明是可重入锁
,那就要刷新过期时间,直到锁计数器值为0,删除key释,放锁
其实原理和AQS很像,AQS就是通过一个volatile修饰的status值,去查看锁的状态,判断是否是可重入的锁.
@Override
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
//调用内部解锁方法
RFuture<Boolean> future = unlockInnerAsync(threadId);
//获取返回的结果
future.onComplete((opStatus, e) -> {
if (e != null) {
//如果不抛异常,取消刷新过期时间的定时任务
cancelExpirationRenewal(threadId);
result.tryFailure(e);
return;
}
//如果返回结果为空,抛出异常
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
//解锁成功,取消刷新过期时间的定时任务
cancelExpirationRenewal(threadId);
result.trySuccess(null);
});
return result;
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//如果锁存在,但不是不是当前线程持有锁,返回null
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
//是同一个线程,则通过hincrby递减1,释放一次锁
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
//如果锁计数器的次数任然大于0,则刷新过期时间
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
//否则证明锁已经释放,删除key并发布释放锁的消息
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}