作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库。目前大家使用的最多的第三方库是jedis。
和SpringCloud gateway一样,Redisson也是基于Netty实现的,是更高性能的第三方库。 所以,这里推荐大家使用Redission替代 jedis。
Redisson简介
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
基本用法
@Slf4j
@SpringBootTest
public class RedissionTest {
@Resource
RedissonClient client;
@Test
public void testRBucketExamples() {
// RList 继承了 java.util.List 接口
RBucket<String> rstring = client.getBucket("redission:test:bucket:string");
rstring.set("this is a string");
RBucket<UserDTO> ruser = client.getBucket("redission:test:bucket:user");
UserDTO dto = new UserDTO();
dto.setToken(UUID.randomUUID().toString());
ruser.set(dto);
System.out.println("string is: " + rstring.get());
System.out.println("dto is: " + ruser.get());
client.shutdown();
}
@Test
public void testListExamples() {
// 默认连接上 127.0.0.1:6379
// RList 继承了 java.util.List 接口
RList<String> nameList = client.getList("redission:test:nameList");
nameList.clear();
nameList.add("张三");
nameList.add("李四");
nameList.add("王五");
nameList.remove(-1);
System.out.println("List size: " + nameList.size());
boolean contains = nameList.contains("李四");
System.out.println("Is list contains name '李四': " + contains);
nameList.forEach(System.out::println);
client.shutdown();
}
@Test
public void testMapExamples() {
// 默认连接上 127.0.0.1:6379
// RMap 继承了 java.util.concurrent.ConcurrentMap 接口
RMap<String, Object> map = client.getMap("redission:test:personalMap");
map.put("name", "张三");
map.put("address", "北京");
map.put("age", new Integer(50));
System.out.println("Map size: " + map.size());
boolean contains = map.containsKey("age");
System.out.println("Is map contains key 'age': " + contains);
String value = String.valueOf(map.get("name"));
System.out.println("Value mapped by key 'name': " + value);
client.shutdown();
}
@Test
public void testLuaExamples() {
// 默认连接上 127.0.0.1:6379
client.getBucket("redission:test:foo").set("bar");
String r = client.getScript().eval(RScript.Mode.READ_ONLY,
"return redis.call('get', 'redission:test:foo')", RScript.ReturnType.VALUE);
System.out.println("foo: " + r);
// 通过预存的脚本进行同样的操作
RScript s = client.getScript();
// 首先将脚本加载到Redis
String sha1 = s.scriptLoad("return redis.call('get', 'redission:test:foo')");
// 返回值 res == 282297a0228f48cd3fc6a55de6316f31422f5d17
System.out.println("sha1: " + sha1);
// 再通过SHA值调用脚本
Future<Object> r1 = client.getScript().evalShaAsync(RScript.Mode.READ_ONLY,
sha1,
RScript.ReturnType.VALUE,
Collections.emptyList());
try {
System.out.println("res: " + r1.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
client.shutdown();
}
@Test
public void testRAtomicLongExamples() {
// 默认连接上 127.0.0.1:6379
RAtomicLong atomicLong = client.getAtomicLong("redission:test:myLong");
// 线程数
final int threads = 10;
// 每条线程的执行轮数
final int turns = 1000;
ExecutorService pool = Executors.newFixedThreadPool(threads);
long start = System.currentTimeMillis();
for (int i = 0; i < threads; i++) {
pool.submit(() ->
{
try {
for (int j = 0; j < turns; j++) {
atomicLong.incrementAndGet();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
ThreadUtil.sleepSeconds(5);
long sum = atomicLong.get();
System.out.println("atomicLong: " + sum);
//输出统计结果
float time = System.currentTimeMillis() - start;
System.out.println("运行的时长为:" + time);
System.out.println("每一次执行的时长为:" + time / sum);
client.shutdown();
}
@Test
public void testRLongAdderExamples() {
// 默认连接上 127.0.0.1:6379
RLongAdder longAdder = client.getLongAdder("redission:test:myLongAdder");
// 线程数
final int threads = 10;
// 每条线程的执行轮数
final int turns = 1000;
ExecutorService pool = Executors.newFixedThreadPool(threads);
long start = System.currentTimeMillis();
for (int i = 0; i < threads; i++) {
pool.submit(() ->
{
try {
for (int j = 0; j < turns; j++) {
longAdder.increment();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
ThreadUtil.sleepSeconds(5);
System.out.println("longAdder: " + longAdder.sum());
long sum = longAdder.sum();
//输出统计结果
float time = System.currentTimeMillis() - start;
System.out.println("运行的时长为:" + time);
System.out.println("每一次执行的时长为:" + time / sum);
client.shutdown();
}
}
Redisson分布式锁
redisson实现分布式锁
@Test
public void testLockDemo() {
// 默认连接上 127.0.0.1:6379
// RLock 继承了 java.util.concurrent.locks.Lock 接口
RLock disLock = client.getLock("DISLOCK");
boolean isLock = false;
try {
//disLock.lock();
isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
Thread.sleep(15000);
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
disLock.unlock();
}
}
通过代码可知,经过Redisson的封装,实现Redis分布式锁非常方便,和显式锁的使用方法是一样的。RLock接口继承了 Lock接口。
分布式锁在redis中的数据结构是Hash结构:
● key:所得的名字
● 字段:UUID+threadId
● 值:表示重入的次数
加锁原理
tryLockInnerAsync是Redission加锁的关键方法
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', 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.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
代码得知:KEYS[1]就是getRawName(),ARGV[2]是getLockName(threadId)
● KEYS[1]=DISLOCK: 你加锁的那个key
● ARGV[1]=30秒(默认): 锁key的默认生存时间
● ARGV[2]=01a6d806-d282-4715-9bec-f51b9aa98110:1 : 加锁的客户端的ID
脚本逻辑:
● 加锁
○ 判断有没有一个叫“DISLOCK”的key
○ 如果没有,则在其下设置一个字段为“01a6d806-d282-4715-9bec-f51b9aa98110:1”,值为“1”
○ 设置它的过期时间
● 锁互斥
○ 客户端2来尝试加锁
○ 判断有没有一个叫“DISLOCK”的key,返回key已存在
○ 接着第二个判断: DISLOCK锁key的hash数据结构中,是否包含客户端2的ID ,返回没有,因为存在客户端1的ID
○ 客户端2会获取到pttl DISLOCK返回的一个数字,这个数字代表了DISLOCK 这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间
○ 此时客户端2会进入一个while循环,不停的尝试加锁
● 锁重入
○ 第一个if判断肯定不成立,“exists DISLOCK”会显示锁key已经存在了
○ 第二个if判断会成立,因为DISLOCK的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
○ 此时就会执行可重入加锁的逻辑,他会用:incrby DISLOCK 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1,通过这个命令对于客户端1的锁,累加1
○ 此时DISLOCKD数据接口如下:
DISLOCK:
{
8743c9c0-0795-4907-87fd-6c719a6b4586:1 2
}
锁重入代码
RLock lock = redisson.getLock("DISLOCK")
lock.lock();
//业务代码
lock.lock();
//业务代码
lock.unlock();
lock.unlock();
释放锁原理
如果执行lock.unlock(),就可以释放分布式锁,就是每次都对DISLOCK数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del DISLOCK”命令,从redis里删除这个key。然后呢,另外的客户端2就可以尝试完成加锁了。
@Override
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
核心代码
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
调试可知:
● KEYS[1]:getRawName(),即KEYS[1]=DISLOCK
● KEYS[2]:getChannelName(),即KEYS[2]=redisson_lock__channel:{DISLOCK}
● ARGV[1]:LockPubSub.unlockMessage,即ARGV[1]=0
● ARGV[2]:生存时间
● ARGV[3]:getLockName(threadId),即ARGV[3]=8743c9c0-0795-4907-87fd-6c719a6b4586:1
上述脚本逻辑
- 判断是否存在一个叫“DISLOCK”的key
- 如果不存在,返回nil
- 如果存在,使用Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值 -1 ,代表减去1
- 若减完以后,counter > 0 值仍大于0,则返回0
- 减完后,若字段值小于或等于0,则用 publish 命令广播一条消息,广播内容是0,并返回1;
可以猜测,广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了
解锁订阅源码
watch dog自动延期
假如我的业务操作比有效时间长,我的业务代码还没执行完,就自动给我解锁了,这个问题的解决方案
- 预估执行时间
预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。
但是这并不是万全之策,比如网络抖动这种情况是无法预测的,也有可能导致业务代码执行的时间变长,所以并不安全。
- watch dog 机制
当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁
Redisson框架实现的分布式锁,就使用了watchDog机制实现锁的续期,这种方法比较靠谱,无业务代码入侵
注意:
● watchDog 只有在未显示指定加锁时间时才会生效。(这点很重要)
● lockWatchdogTimeout:可以设置超时时间