我们先看两个dome,依JVM本地开发来看,这就是两个很正常的维持线程安全的策略
- 1.加同步锁,保证整个流程是串行的。
- 2.用ack的复合操作先占位,保证这次操作是属于自己的。
/**
* 使用锁 + 缓存map
*/
@Test
public void redisLockTest() {
RLocalCachedMap<Integer, Integer> map = client.getLocalCachedMap("test", update_options);
RLock lock = client.getLock("testLock");
boolean isLock = lock.tryLock();
if (isLock) {
try {
for (int i = 0; i < 1000; i++) {
Integer value = map.addAndGet(1, 0);
if (value > 10) {
// dosome...
}
}
// 中间操作异步执行了~
} finally {
lock.unlock();
}
}
}
/**
* 使用并发修改
*/
@Test
public void redisAddAndGetTest() {
RMap<Integer, Integer> map = client.getMap("test");
for (int i = 0; i < 1000; i++) {
// 先加后判断,保持原子性操作
Integer value = map.addAndGet(1, 1);
if (value > 10) {
// dosome...
}
// 退回操作
map.addAndGetAsync(1, -1);
// 中间操作异步执行了~
}
// 在真正获取这个物品的时候才加回去
}
有锁的dome执行结果(100线程)
无锁的dome执行结果(100线程)
居然有锁比无锁快27倍的速度?这是为什么?
要理解这个问题,我们先得理清楚 Redisson 是这个什么框架?Redisson 锁是怎么实现的?
Redisson 是这个什么框架?
官方概述:Redisson 是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。
所以说 Redisson 只是我们的程序和 Redis 的一个桥梁,我们只是通过 Redisson 发生指令到Redis 上面,而且等待返回。也就是我们如果是使用同步请求的话,我们是需要阻塞线程(while(true))去等待返回的。
所以我们每一个同步请求不是我们普通map一样,是有性能消耗的,且不少。
Redisson 同步请求相关方法 :
@Override
public V get(Object key) {
// getAsync 就是发送命令给Redis服务端
return get(getAsync((K) key));
}
public RFuture<V> getOperationAsync(K key) {
String name = getName(key);
// 发生命令为 "HGET"
return commandExecutor.readAsync(name, codec, RedisCommands.HGET, name, encodeMapKey(key));
}
// 阻塞
protected final <V> V get(RFuture<V> future) {
return commandExecutor.get(future);
}
Redisson 锁是怎么实现的?
网上分析的也很多,在这里不是重点。如果有兴趣下次再写一篇。
在这里简析一下: RedissonLock 相关方法
@Override
public boolean tryLock() {
return get(tryLockAsync());
}
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 不断地去发生命令
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 具体逻辑就是
// 用一个map来保存锁
// 判断锁如果没有占用即是0,然后把自己线程id存下去加1
// 如果有人占用则获取锁失败
return evalWriteAsync(getName(), 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(getName()), internalLockLeaseTime, getLockName(threadId));
}
那就是这个锁本质上就是在不断地请求数据,直到成功为止。
理解上面的两个东西,我们就可以开始总结了。
翻译一下上面两段代码
public void redisLockTest() {
// 请求锁,发送多个同步请求,直到返回成功
try{
// 获取缓存,发生一个同步请求
// 处理要锁的数据
// 把操作异步执行 1m
}finaylly{
// 释放锁,发送一个同步请求
}
}
// RLocalCachedMap<Integer, Integer> map = client.getLocalCachedMap("test", update_options);
RMap<Integer, Integer> map = client.getMap("test");
for (int i = 0; i < 1000; i++) {
// 发送同步请求 addAndGet
// 发送异步请求 addAndGet(忽略)
map.addAndGetAsync(1, -1);
// 把操作异步执行 1m
}
}
可以看到了,那就是一个同步请求发多少的问题了
如果获取锁(请求)的次数超过1000遍,那redisAddAndGetTest这个方法的性能超过redisLockTest了。
那在什么情况下才有可以用这个锁方案替换呢?
- 1.本身在并发不多
- 2.这个程序去同步操作redis的情况很多
- 3.中间操作同步数据比较少的
- 4.后续逻辑可以用异步操作执行,如:存数据库
满足以上三种情况:就可以(加锁+读缓存) 来替代 ack的复合操作是可行的。
当然加锁的情况也比较少,正常业务还是还是ack的复合操作比较合适 那么就算用ack的复合操作的复合操作,也要尽量避免同步的去查询数据
最后一个问题:能不能 ack的复合操作 + 缓存啊?
RLocalCachedMap<Integer, Integer> map = client.getLocalCachedMap("test", update_options);
public void redisAddAndGetTest() {
// RMap<Integer, Integer> map = client.getMap("test");
for (int i = 0; i < 1000; i++) {
// 先加后判断,保持原子性操作
Integer value = map.addAndGet(1, 1);
if (value > 10) {
// dosome...
}
// 退回操作
map.addAndGetAsync(1, -1);
// 中间操作异步执行了~
}
// 在真正获取这个物品的时候才加回去
}
测试并发的代码:
public void testThread() throws InterruptedException {
long sTime = System.currentTimeMillis();
Vector<Thread> vector = new Vector<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> redisAddAndGetTest());
vector.add(thread);
thread.start();
}
for (Thread thread : vector) {
thread.join();
}
long eTime = System.currentTimeMillis();
System.out.println("总耗时:" + (eTime - sTime));
}
哈哈哈哈哈哈,自己看看 RLocalCachedMap 的源码就知道了~~~~