基于Redis实现分布式锁

前言

        锁可以理解为多线程情况下访问共享资源的一种线程同步机制。单机部署的应用中(单个JVM),通过Java提供synchronized关键字或者Lock机制可以实现。但是微服务架构中,通常一个服务都会部署多台(多个JVM),用Java的锁机制就无法实现了。此时就需要用到分布式锁。今天主要介绍一下基于Redis来实现分布式锁。

Redis实现分布式锁的基本思想

        我们将需要加锁的公共资源设置一个相同的key。当操作该资源时,先用Redis的setNX(如果不存在则新增)命令往Redis中存储数据。
如果新增数据成功,则表示获取到锁。然后执行业务逻辑,执行完毕后将key对应数据删除,把锁释放。
如果新增数据失败,则表示获取锁失败。表示Redis中已有该key对应的数据,则表示此时有其他线程先获取到了锁,正在执行业务逻辑。
        存在一种情况,某个线程在获取到锁后,由于服务器宕机。导致没有释放锁。那么可能会导致该锁永远无法释放。所以,我们需要给Redis的数据设置一个过期时间。超时则自动释放锁。
参考代码如下:

    public Boolean lock(String lockKey){
        String value = "AWSL";//(1)
        Boolean lock = false;//(2)
        try {
            lock = redisTemplate.opsForValue().setIfAbsent(lockKey, value, 2, TimeUnit.SECONDS);//(3)
            if(lock){//(4)
                log.info("获取到锁,执行业务逻辑");//(5)
                Thread.sleep(3000);
            } else {
                log.info("未获取到锁");//(6)
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(lock){//(7)
                String lockValue = redisTemplate.opsForValue().get(lockKey);//(8)
                if(value.equals(lockValue)){//(9)
                    return redisTemplate.delete(lockKey);//(10)
                }
            }
        }
        return lock;//(11)
    }

这段Redis实现锁的代码存在一些问题。
1.假设线程A获取到了锁。锁的过期时间为2S。但是A执行业务逻辑用了3S。因此2S时锁已经过期释放了。此时B线程可以获取到锁。然后A业务逻辑处理完毕,走到释放锁的代码(7)(8)(9)(10)。根据(7)(8)(9)(10)行代码,此时线程A根据lockKey删除了Redis中的数据,其实释放的是线程B的锁。
在这里插入图片描述
这个问题可以将key对应的value设置为一个随机值或者线程ID,释放锁时判断value是否等于该随机值即可。

但是改正后依然存在问题。
2.释放锁的操作不是原子操作。
假设线程A已经判断了value等于设置的随机值。但是执行完第⑼代码,由于网络问题,2S后才执行第⑽代码去deleteKey(释放锁)。但是这两秒内,可能锁已经过期释放了。此时如果线程B获取到了锁。当线程A执行到第⑽行代码时就会释放掉B的锁。依然存在误释放其他线程锁的问题。

这里可以通过Lua脚本来保证释放锁的操作是原子操作来解决该问题。

利用Lua脚本实现Redis锁

从Redis2.6.0版本开始引入对Lua脚本的支持,通过内置的Lua解释器,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。

释放锁的Lua脚本为:

根据参数key去获取value,如果value和传入的参数value值相等。则删除该key对应数据,成功返回1,否则返回0。
if redis.call(‘get’, KEYS[1]) == ARGV[1]
    then return redis.call(‘del’, KEYS[1])
else
    return 0
end

我们也可以通过Lua脚本来获取锁:

使用setNx命令设置值。如果成功则使用expire命令设置过期时间。成功返回1,否则返回0。
if redis.call(‘setNx’,KEYS[1],ARGV[1]) == 1
    then return redis.call(‘expire’,KEYS[1],ARGV[2])
else
    return 0
end

代码如下:

	@Autowired
    private RedisTemplate<String, String> redisTemplate;
    /**
     * 成功获取锁返回值
     */
    private static final Long LOCK_SUCCESS = 1L;
    /**
     * 成功释放锁返回值
     */
    private static final Long UNLOCK_SUCCESS = 1L;

    /**
     * 释放锁的LUA脚本:如果value的值与参数相等,则删除,否则返回0
     */
    public static final String UNLOCK_SCRIPT_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * @param [lockKey, value]
     * @return boolean
     * @author YiHaoXing
     * @description 使用LUA脚本释放锁, 原子操作
     * @date 0:47 2019/6/29
     **/
    public boolean releaseLockByLua(String lockKey, String value) {
        RedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT_LUA, Long.class);
        return UNLOCK_SUCCESS.equals(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value));
    }

    /**
     * 获取锁的LUA脚本:用setNx命令设置值,并设置过期时间
     */
    public static final String LOCK_SCRIPT_LUA = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";

    /**
     * @param [lockKey, value, expireTime]
     * @return boolean
     * @author YiHaoXing
     * @description 使用LUA脚本获取锁, 原子操作。过期时间单位为秒
     * @date 0:46 2019/6/29
     **/
    public boolean getLockByLua(String lockKey, String value, int expireTime) {
        RedisScript<Long> redisScript = new DefaultRedisScript<>(LOCK_SCRIPT_LUA, Long.class);
        return LOCK_SUCCESS.equals(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, expireTime));
    }

然后做一个简单的测试

	@Autowired
    private RedisLockUtils redisLockUtils;
	@GetMapping("/t1/{key}")
    public String test1(@PathVariable String key){

        String value = new StringBuilder().append(Thread.currentThread().getId()).append(Math.random()).toString();
        boolean lock = false;
        try {
            lock = redisLockUtils.getLockByLua(key, value, 30);
            if(lock){
                log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
                log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
                Thread.sleep(5000);
            } else {
                log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock){
                redisLockUtils.releaseLockByLua(key, value);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
        }
        return "t1 over";
    }
    @GetMapping("/t2/{key}")
    public String test2(@PathVariable String key){

        String value = new StringBuilder().append(Thread.currentThread().getId()).append(Math.random()).toString();
        boolean lock = false;
        try {
            lock = redisLockUtils.getLockByLua(key, value, 30);
            if(lock){
                log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
                log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
                Thread.sleep(5000);
            } else {
                log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock){
                redisLockUtils.releaseLockByLua(key, value);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
        }
        return "t2 over";
    }

假设处理业务需要的时间为5S,因此上边的代码中会让当前线程睡眠5S。
1.首先访问 http://localhost:8080/t1/T,然后立刻访问http://localhost:8080/t2/T (5S内即可)。结果如下图:

2019-09-09 17:05:26.388  INFO 31952 --- [nio-8080-exec-1] com.demo.controller.RedisLockController  : Thread:28获取锁成功
2019-09-09 17:05:26.388  INFO 31952 --- [nio-8080-exec-1] com.demo.controller.RedisLockController  : Thread:28执行业务逻辑中...
2019-09-09 17:05:27.846  INFO 31952 --- [nio-8080-exec-2] com.demo.controller.RedisLockController  : Thread:29获取锁失败
2019-09-09 17:05:31.392  INFO 31952 --- [nio-8080-exec-1] com.demo.controller.RedisLockController  : Thread:28释放锁

可以看到Thread:28获取锁后,Thread:29再去获取锁就失败了。
2.首先访问 http://localhost:8080/t1/T,等待5S后访问http://localhost:8080/t2/T 。结果如下图:

2019-09-09 17:13:57.259  INFO 31952 --- [nio-8080-exec-6] com.demo.controller.RedisLockController  : Thread:33获取锁成功
2019-09-09 17:13:57.259  INFO 31952 --- [nio-8080-exec-6] com.demo.controller.RedisLockController  : Thread:33执行业务逻辑中...
2019-09-09 17:14:02.261  INFO 31952 --- [nio-8080-exec-6] com.demo.controller.RedisLockController  : Thread:33释放锁
2019-09-09 17:14:03.633  INFO 31952 --- [nio-8080-exec-8] com.demo.controller.RedisLockController  : Thread:35获取锁成功
2019-09-09 17:14:03.633  INFO 31952 --- [nio-8080-exec-8] com.demo.controller.RedisLockController  : Thread:35执行业务逻辑中...
2019-09-09 17:14:08.635  INFO 31952 --- [nio-8080-exec-8] com.demo.controller.RedisLockController  : Thread:35释放锁

可以看到Thread:33先获取到锁,执行完业务逻辑后释放锁。此时t2的请求过来后(Thread:35)就可以获取到锁。

利用Redisson实现Redis锁

        有些业务场景中,我们希望如果当前线程如果没有获取到锁,可以等待几秒钟,继续获取锁。或者说,我们希望当前线程一直等待,直到获取到锁。比如:抢票时,用户点击“购买”后,如果该线程未抢到锁,直接返回失败,则用户需要不停的点击“购买”去抢票。如果该线程在未获取到锁后,可以在10S内继续尝试获取锁,那么就不需要用户不停点击“购买”去触发了。针对这种需求,用上边的方法不好实现。
        Redisson是一个Java写的基于netty的操作Redis的框架。它提供了一套Java中Lock的实现。我们可以直接调用其提供的方法来实现加锁和释放锁。Redisson可以使线程阻塞,这样我们就可以利Redisson来实现上述的需求。

目前Redisson已经提供了基于SpringBoot的starter依赖。下边介绍一下SpringBoot集成Redisson。
引入redisson-spring-boot-starter依赖。

<dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.11.1</version>
</dependency>

单机Redis配置redisson-single.yml

#单机Redis Redisson配置
singleServerConfig:
  address: "redis://192.168.154.129:6379"
  #redis连接密码
  password: foobared123
  clientName: null
  #选择使用哪个数据库0~15
  database: 0
  #连接空闲超时 如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  #连接超时
  connectTimeout: 10000
  #命令等待超时
  timeout: 3000
  #命令失败重试次数
  retryAttempts: 3
  #命令重试发送时间间隔
  retryInterval: 1500
  #重新连接时间间隔
  reconnectionTimeout: 3000
  failedAttempts: 3
  subscriptionsPerConnection: 5
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  dnsMonitoringInterval: 5000
  #dnsMonitoring: false
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"

集群Redis配置redisson-cluster.yml

#Redis集群 Redisson配置
clusterServersConfig:
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  password: 123456
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  slaveSubscriptionConnectionMinimumIdleSize: 1
  slaveSubscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 32
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 32
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  nodeAddresses:
    - "redis://192.168.154.129:7001"
    - "redis://192.168.154.129:7002"
    - "redis://192.168.154.129:7003"
    - "redis://192.168.154.129:7004"
    - "redis://192.168.154.129:7005"
    - "redis://192.168.154.129:7006"
  scanInterval: 1000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"

application.yml

spring:
  redis:
    redisson:
      #Redis集群配置
      #config: classpath:redisson-cluster.yml
      #Redis单机配置
      config: classpath:redisson-single.yml

然后注入RedissonClient即可

	@Autowired
    private RedissonClient redissonClient;

可重入锁

	public void getLock(String lockKey,int expireTime, TimeUnit timeUnit){
        RLock lock = redissonClient.getLock(lockKey);
        log.info("Thread:{}正在获取锁...",Thread.currentThread().getId());
        //拿不到锁线程会一直阻塞.直到拿到锁
        lock.lock(expireTime,timeUnit);
    }
    public boolean getReentrantLock(String lockKey, int waitTime, int expireTime, TimeUnit timeUnit) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockKey);
        log.info("Thread:{}正在获取锁...",Thread.currentThread().getId());
        //拿不到锁会等待waitTime,如果过了waitTime依然没有拿到锁,则获取锁失败.
        return lock.tryLock(waitTime, expireTime, timeUnit);
    }

Redisson实现可重入锁的方法:
1.org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit)
该方法如果获取不到锁,线程会一直阻塞,直到获取锁。
2.org.redisson.RedissonLock#tryLock(long, long, java.util.concurrent.TimeUnit)
该方法如果获取不到锁,会等待一段时间,继续尝试获取锁。超过等待时间仍未获取到锁,则获取锁失败。
简单测试一下

    @GetMapping("/t1/{key}")
    public String g1(@PathVariable String key) throws InterruptedException {
        redisLockUtils.getLock(key, 30, TimeUnit.SECONDS);
        log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
        log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
        Thread.sleep(10000);
        //释放锁
        redisLockUtils.unlock(key);
        log.info("Thread:{}释放锁",Thread.currentThread().getId());
        return "t1 over";
    }
    @GetMapping("/t2/{key}")
    public String g2(@PathVariable String key){
        redisLockUtils.getLock(key, 30, TimeUnit.SECONDS);
        log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
        log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
        //释放锁
        redisLockUtils.unlock(key);
        log.info("Thread:{}释放锁",Thread.currentThread().getId());
        return "t2 over";
    }	

	@GetMapping("/t3/{key}")
    public String t1(@PathVariable String key){
        boolean lock = false;
        try {
            //等待时间3S.缓存过期时间30S
            lock = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
            if(lock){
                //do something.
                log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
                log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
                Thread.sleep(10000);
            }else {
                log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            if(lock){
                redisLockUtils.unlock(key);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
        }
        return "t3 over";
    }
    @GetMapping("/t4/{key}")
    public String t2(@PathVariable String key){
        boolean lock = false;
        try {
            //等待时间3S.缓存过期时间30S
            lock = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
            if(lock){
                //do something.
                log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
                log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
                Thread.sleep(10000);
            }else {
                log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            if(lock){
                redisLockUtils.unlock(key);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
        }
        return "t4 over";
    }
    

假设处理业务需要的时间为10S,因此上边的代码中会让当前线程睡眠10S。

1.测试lock()方法。首先访问 http://localhost:8080/t1/T,然后立刻访问http://localhost:8080/t2/T 结果如下图:

2019-09-10 01:44:23.324  INFO 34696 --- [nio-8080-exec-2] com.demo.redis.RedisLockUtils            : Thread:46正在获取锁...
2019-09-10 01:44:23.326  INFO 34696 --- [nio-8080-exec-2] com.demo.controller.LockController       : Thread:46获取锁成功
2019-09-10 01:44:23.326  INFO 34696 --- [nio-8080-exec-2] com.demo.controller.LockController       : Thread:46执行业务逻辑中...
2019-09-10 01:44:24.633  INFO 34696 --- [nio-8080-exec-5] com.demo.redis.RedisLockUtils            : Thread:49正在获取锁...
2019-09-10 01:44:33.328  INFO 34696 --- [nio-8080-exec-2] com.demo.controller.LockController       : Thread:46释放锁
2019-09-10 01:44:33.333  INFO 34696 --- [nio-8080-exec-5] com.demo.controller.LockController       : Thread:49获取锁成功
2019-09-10 01:44:33.333  INFO 34696 --- [nio-8080-exec-5] com.demo.controller.LockController       : Thread:49执行业务逻辑中...
2019-09-10 01:44:33.335  INFO 34696 --- [nio-8080-exec-5] com.demo.controller.LockController       : Thread:49释放锁

Thread:46先获取到锁。24秒时Thread:49尝试获取锁。此时由于Thread:46未释放锁。所以Thread:49会等待直到Thread:46释放锁。最终33秒时Thread:49获取到锁。

2.测试tryLock()方法。首先访问 http://localhost:8080/t3/T,然后立刻访问http://localhost:8080/t4/T 结果如下图:

2019-09-10 01:26:03.936  INFO 9132 --- [nio-8080-exec-1] com.demo.redis.RedisLockUtils            : Thread:45正在获取锁...
2019-09-10 01:26:03.939  INFO 9132 --- [nio-8080-exec-1] com.demo.controller.LockController       : Thread:45获取锁成功
2019-09-10 01:26:03.939  INFO 9132 --- [nio-8080-exec-1] com.demo.controller.LockController       : Thread:45执行业务逻辑中...
2019-09-10 01:26:05.431  INFO 9132 --- [nio-8080-exec-2] com.demo.redis.RedisLockUtils            : Thread:46正在获取锁...
2019-09-10 01:26:08.437  INFO 9132 --- [nio-8080-exec-2] com.demo.controller.LockController       : Thread:46获取锁失败
2019-09-10 01:26:13.941  INFO 9132 --- [nio-8080-exec-1] com.demo.controller.LockController       : Thread:45释放锁

Thread:45先获取到锁,Thread:46在05秒去尝试获取锁,此时未能获取到锁,因此等待3秒,到08秒时最终获取锁失败。

3.测试tryLock()方法。首先访问 http://localhost:8080/t3/T,等待7S后访问http://localhost:8080/t4/T 。结果如下图:

2019-09-10 01:32:56.603  INFO 9132 --- [nio-8080-exec-6] com.demo.redis.RedisLockUtils            : Thread:50正在获取锁...
2019-09-10 01:32:56.605  INFO 9132 --- [nio-8080-exec-6] com.demo.controller.LockController       : Thread:50获取锁成功
2019-09-10 01:32:56.605  INFO 9132 --- [nio-8080-exec-6] com.demo.controller.LockController       : Thread:50执行业务逻辑中...
2019-09-10 01:33:04.770  INFO 9132 --- [nio-8080-exec-8] com.demo.redis.RedisLockUtils            : Thread:52正在获取锁...
2019-09-10 01:33:06.607  INFO 9132 --- [nio-8080-exec-6] com.demo.controller.LockController       : Thread:50释放锁
2019-09-10 01:33:06.612  INFO 9132 --- [nio-8080-exec-8] com.demo.controller.LockController       : Thread:52获取锁成功
2019-09-10 01:33:06.613  INFO 9132 --- [nio-8080-exec-8] com.demo.controller.LockController       : Thread:52执行业务逻辑中...
2019-09-10 01:33:16.615  INFO 9132 --- [nio-8080-exec-8] com.demo.controller.LockController       : Thread:52释放锁

Thread:50先获取到锁,04秒时Thread:52尝试获取锁。由于此时Thread:50还未释放锁,所以它需要等待。等待2S后,06秒时Thread:50释放了锁。此时Thread:52也成功获取到了锁。

需要留意一下Redisson存入Redis的数据是以hash来存储的,存储的数据是一个对象。我们先来看一下Redisson加锁时往Redis中存储的数据是什么样的。
在这里插入图片描述
可以看到,key为T,存储的value为对象。d249c8ab-0421-48fb-a204-af551bd8810d:50是对象的属性,1是该属性对应的值。那么属性d249c8ab-0421-48fb-a204-af551bd8810d:50以及该属性的值1分别是什么意思呢?
其实Redisson设置属性时,是根据"guid + 当前线程的ID" 生成的。因此d249c8ab-0421-48fb-a204-af551bd8810d是guid,而50是当前线程ID。
属性对应的值1表示的是当前线程获取锁的次数为1。这里Redisson实现的锁是可重入锁。因此在同一线程内,是可以多次获取这个锁的。这个值就表示当前线程内获取锁的次数。

我们可以写个方法来验证一下。

	@GetMapping("/t7/{key}")
    public String t7(@PathVariable String key){
        boolean lock = false;
        boolean lock2 = false;
        try {
            //等待时间3S.缓存过期时间30S
            lock = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
            if(lock){
                //do something.
                log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
                log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
                //再次获取锁
                lock2 = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
                if(lock2){
                    log.info("Thread:{}当前线程内再次获取锁",Thread.currentThread().getId());
                }
                Thread.sleep(10000);
            }else {
                log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock2){
                redisLockUtils.unlock(key);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
            //释放锁
            if(lock){
                redisLockUtils.unlock(key);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
        }
        return "t7 over";
    }

这个方法里我们获取了两次锁。访问:http://localhost:8080/t7/T
加锁后,Redis中存储的数据为:
在这里插入图片描述
可以看到属性d249c8ab-0421-48fb-a204-af551bd8810d:54对应的value值为2,因此这个value表示的就是Redisson可重入锁获取锁的次数。

还有一个问题:Redisson是如何保证释放锁的操作是原子性的?查看Redisson加锁和释放锁的源码可以发现,其实底层方法也是使用的Lua脚本来操作Redis的。
加锁方法:org.redisson.RedissonLock#tryLockInnerAsync

	<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));
    }

释放锁的方法:org.redisson.RedissonLock#unlockInnerAsync

	protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), 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.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

    }

利用Spring AOP加锁

        实际开发过程中,我们并不希望在业务逻辑代码中加入太多冗余的加锁,释放锁的代码。此时可以用SpringAOP结合注解来分离加锁/释放锁的代码。
引入AOP依赖:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <version>2.1.5.RELEASE</version>
</dependency>

创建自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    /**
     * 锁的过期时间.以秒为单位
     */
    int expireTime() default 30;

    /**
     * 未获取到锁后等待重试时间.以秒为单位
     */
    int waitTime() default 3;
    /**
     * redis的key
     * @return
     */
    String value() default "";
}

创建切面类:

@Aspect
@Component
@Slf4j
public class RedisLockAspect {
    @Autowired
    private RedisLockUtils redisLockUtils;

    @Pointcut("@annotation(com.demo.annotation.RedisLock)")
    public void redisLockPointCut() {
    }


    @Around("redisLockPointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        RedisLock annotation = method.getAnnotation(RedisLock.class);

        //锁的key
        String key = annotation.value();
        //过期时间
        int expireTime = annotation.expireTime();
        //等待时间
        int waitTime = annotation.waitTime();
        boolean lock = false;
        try {
            //获取锁
            lock = redisLockUtils.getReentrantLock(key, waitTime, expireTime, TimeUnit.SECONDS);
            if (lock) {
                log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
                return proceedingJoinPoint.proceed();
            } else {
                log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
            }
        } catch (Throwable throwable) {
            throw throwable;
        } finally {
            //释放锁
            if(lock){
                redisLockUtils.unlock(key);
                log.info("Thread:{}释放锁",Thread.currentThread().getId());
            }
        }
        return null;
    }
}

测试:

	public static final String LOCK_KEY = "T";
	
	@GetMapping("/t5")
    @RedisLock(LOCK_KEY)
    public String test3(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "t5 over";
    }
    
    @GetMapping("/t6")
    @RedisLock(LOCK_KEY)
    public String test4(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "t6 over";
    }

首先访问 http://localhost:8080/t5,然后立刻访问http://localhost:8080/t6,结果如下:

2019-09-10 03:03:36.070  INFO 24488 --- [nio-8080-exec-1] com.demo.redis.RedisLockUtils            : Thread:45正在获取锁...
2019-09-10 03:03:36.073  INFO 24488 --- [nio-8080-exec-1] com.demo.aspect.RedisLockAspect          : Thread:45获取锁成功
2019-09-10 03:03:37.186  INFO 24488 --- [nio-8080-exec-2] com.demo.redis.RedisLockUtils            : Thread:46正在获取锁...
2019-09-10 03:03:40.189  INFO 24488 --- [nio-8080-exec-2] com.demo.aspect.RedisLockAspect          : Thread:46获取锁失败
2019-09-10 03:03:41.078  INFO 24488 --- [nio-8080-exec-1] com.demo.aspect.RedisLockAspect          : Thread:45释放锁

小结

        如果项目中用Redis做缓存方案最好还是基于Lettuce(Jedis也行,不过SpringBoot2.x版本默认采用Lettuce),因为Redisson本身对字符串的操作支持很差;如果是作为分布式锁方案,可以采用Redisson。
        如果是SpringBoot框架同时采用starter启动依赖的方式集成Lettuce和Redisson的话,会导致LettuceConnectionFactory注入失败,因此如果需要在一个项目里同时集成Lettuce和Redisson操作Redis的话,建议不要用starter启动依赖的方式集成Redisson。可以单独引入Redisson的依赖,然后写一个配置类来集成Redisson。

Demo代码地址:
https://github.com/YiHaoxing/redis-lock-lettuce
https://github.com/YiHaoxing/redis-lock-redisson
https://github.com/YiHaoxing/redis-lock

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值