1、什么是分布式锁?
分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调各个系统之间的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
2、redis实现的分布式锁
使用的是redisson框架操作redis命令。redisson实现了redlock算法,提供了异步,公平性等高级功能。添加maven依赖。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.1</version>
</dependency>
java实现的基于单机模式redis的分布式锁,集群模式于该模式类似,可靠性更高
public class RedisConfig {
private static int count = 0; // 代表分布式环境下要争夺得资源
private String address = "127.0.0.1:6379";
private String password = "961113";
// private String poolSize= "20";
// private String database= "10";
public static void main(String[] args) throws InterruptedException {
String address = "redis://127.0.0.1:6379";
Config config = new Config();
config.useSingleServer().setAddress(address).setPassword("961113");
RedissonClient redisson = Redisson.create(config);
System.out.println(redisson);
int nThreads = 500; // 测试并发数
ExecutorService exec = Executors.newFixedThreadPool(nThreads);
List tasks = new ArrayList(nThreads);
for (int i = 0; i < nThreads; i++) {
tasks.add((Callable) () -> {
Thread.sleep(10); //代替处理业务的时间
RLock lock = redisson.getLock("redis");
try {
boolean b = lock.tryLock(60, TimeUnit.SECONDS);
if (b) {
++RedisConfig.count;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 使用完之后一定要释放锁
lock.unlock();
}
return RedisConfig.count;
});
}
exec.invokeAll(tasks);
exec.shutdown();
while (true) {
if (exec.isTerminated()) {
System.out.println("所有的子线程都结束了!");
break;
}
}
System.out.println("count = " + count);
redisson.shutdown();
}
}
output
org.redisson.Redisson@1672fe87
所有的子线程都结束了!
count = 500
3、内部实现解析
3.1、redis中的数据变化
以org.redisson.RedissonLock的这一个实现来进行解读。上面的测试代码执行过程,redis中会出现一个hash类型的值,其中,“redis”是锁名称,“redis”中有一个键值对,键为:“7b319fef-0674-4a62-9a5b-13d786f4a99e:137”,值为“1”。
其中,键的格式RedissonLock类中如下所示,为UUID + threadId,value为重入值。redisson实现了可重入的分布式锁,此种情况value就对应实际的可重入值了。
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
redis中的数据
127.0.0.1:6379> TYPE redis
hash
127.0.0.1:6379> HGETALL redis
1) "7b319fef-0674-4a62-9a5b-13d786f4a99e:137"
2) "1"
127.0.0.1:6379> HGETALL redis
1) "7b319fef-0674-4a62-9a5b-13d786f4a99e:95"
2) "1"
127.0.0.1:6379> HGETALL redis
(empty list or set)
127.0.0.1:6379> HGETALL redis
1) "7b319fef-0674-4a62-9a5b-13d786f4a99e:93"
2) "1"
3.2、redisson的实现方式
主要实现是通过lua脚本实现,lua脚本本身支持原子性。分析org.redisson.RedissonLock中尝试加锁的过程。
加锁过程
// KEYS[1]是分布式锁的key,示例中的“redis”字符串
// ARGV[1]是所得租约时间,默认30s。
// ARGV[2]获取锁时set的唯一值(uuid + threadId),因为要确认一个锁是不是同一个线程操作。
// 如果分布式锁key不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)
// 如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间
// 获取分布式锁的KEY的失效时间毫秒数
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"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; " +
"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; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
释放锁过程
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果锁存在,但是value不匹配,表示锁被占用,直接返回
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 执行到这一步,意味着锁存在,value也匹配,表示当前线程占有锁,重入数减一
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 重入次数减一后是大于0的,则只设置失效时间,不能删除
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 重入次数减一后是等于0的,则删除key,并发布解锁消息。
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
// 这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
/**
* 欢迎评论、留言、发表看法。谢谢!
*/