Redisson 为什么有时候加锁比不加锁要快?

942 阅读3分钟

我们先看两个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线程)

clipboard.png

无锁的dome执行结果(100线程)

clipboard1.png

居然有锁比无锁快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));
}

clipboard2.png 哈哈哈哈哈哈,自己看看 RLocalCachedMap 的源码就知道了~~~~