Redisson实现分布式锁

一、jedis/luttuce/redisson关系
 
在redis官网推荐的三大框架就是:jedis、lettuce,redission。
 
1、jedis
  • jedis使用阻塞的I/O,是同步的,即当jedis与redis数据库建立连接后,只有当连接释放后才允许下一次的连接
  • jedis客户端实例API非线程安全,需要通过连接池来使用jedis
  • jedis是redis的java实现的客户端,,其API提供了比较全面的redis命令的支持,jedis的每个方法底层都是对redis的单个命令的封装
 
2、 lettuce
  • lettuce底层基于netty,使用异步非阻塞I/O
  • lettuce客户端实例API是线程安全的,可通过操作单个lettuce连接来完成各种操作
  • spring-boot-starter-data-redis默认使用的lettuce作为redis客户端
 
3、redisson
  • redisson使用异步非阻塞I/O,且基于netty框架的事件驱动通信层
  • redisson的API是线程安全的,可以操作单个redisson连接来完成各种操作
  • redisson对的方法基本进行了较高的抽象,每个方法可能封装了一个或多个redis命令方法
 
二、使用redisson实现分布式锁原因
 
常见的分布式锁,基本是通过Redis或Zookeeper实现,其中通过redis实现最常见。
 
1、redis实现分布式锁的简单思路
redis分布式锁的实现思路可以概括为:在redis中设置一个值表示加了锁,然后获取到该锁后即可进行一系列业务逻辑操作,最后删除这个key(或设置过期时间)即表示释放了该锁。
 
2、redis原生实现方法
 
2.1、加锁
通过命令  SET key value [EX seconds] [PX milliseconds] [NX|XX] 的组合, Redis 中实现锁的简单方法一般如下
//如果不用以下命令,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(key永久存在)
SET key value NX EX max-lock-time

其中参数意义如下:

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。
 
客户端执行以上的命令:
  • 如果服务器返回 OK ,那么这个客户端获得锁。
  •  如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
 
同时通过以下修改,可以让锁的实现更健壮:
  • 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
  • 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
这两个改动可以 防止持有过期锁的客户端误删现有锁的情况出现,如:
假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了
 
2.2、释放锁
释放锁涉及到两条指令即get和del,但这两条指令不是原子性的;所以,为保证原子性操作,需要用到redis的lua脚本执行,lua脚本是原子性的。
释放锁时,可通过以下脚本
if redis.call("get",KEYS[1] == ARGV[1])
then
    return redis.call("del",KEYS[1])
else
    return 0
end

 

 
2.3、组合加锁解锁
组合在一起即
// 获取锁
// NX是指如果key不存在就成功,key存在返回false,EX可以指定过期时间
SET lock_key unique_value NX EX 30

// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

上述脚本可以通过 EVAL  script  1 key  value 命令来调用。

 
通过redis原生编码实现分布式锁,除了需要考虑客户端怎么实现外,还需要考虑redis的部署问题等等,需要注意的细节很多,一般没有大牛的中小公司,很难顾全。
 
3、redisson分布式锁的优势
Redisson作为一款优秀的企业级开源redis client,也提供了分布式锁的实现,并且屏蔽了很多细节的处理,减少了一般公司程序员的工作,提供了较稳定的技术方案。
  • redisson所有指令都通过lua脚本执行,保证了操作的原子性
  • redisson设置了watchdog看门狗,“看门狗”的逻辑保证了没有死锁发生
举个例子,如:
redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
因为有看门狗的处理,它会在获取锁之后,每隔10秒把key的超时时间设为30s;
这样的话,就算一直持有锁也不会因为出现key过期,导致其他线程获取到锁的问题;
同时,如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁。
 
 
三、Redisson分布式锁的实现
 
1、新建一个 spring-boot项目,pom文件引入以下依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用lettuce连接池必须-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.0</version>
</dependency>

2、yaml中添加redis相关配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 100
        # 连接池中的最小空闲连接
        max-idle: 10
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
    # 连接超时时间(毫秒)
    timeout: 5000
    #默认是索引为0的数据库
    database: 2
    password: 123456

 

 
3、配置redisson
 
package com.he.comm.redis;


import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
* <b>@Desc</b>:   redisson 配置
* <b>@Author</b>: hesh
* <b>@Date</b>:   2020/6/27
* <b>@Modify</b>:
*/
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password:}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        //单节点
        config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
//        //主从模式配置
//        config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
//        //集群模式
//        config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001");

        return Redisson.create(config);
    }
}

 

 
4、编写分布式锁
 
4.1、通用接口
package com.he.comm.redis;


import org.redisson.api.RCountDownLatch;
import org.redisson.api.RSemaphore;

import java.util.concurrent.TimeUnit;

/**
* <b>@Desc</b>:   分布式锁,T-锁类型
* <b>@Author</b>: hesh
* <b>@Date</b>:   2020/6/27
* <b>@Modify</b>:
*/
public interface DistributedLocker<T> {


    /**
     * 加锁
     * @param lockKey
     * @return T
     */
    T lock(String lockKey);


    /**
     * 带超时的锁
     * @param lockKey
     * @param expireTime 超时释放时间,单位:秒
     * @return T
     */
    T lock(String lockKey, int expireTime);


    /**
     * 带超时的锁
     * @param lockKey
     * @param unit        时间单位
     * @param expireTime 超时释放时间
     * @return T
     */
    T lock(String lockKey, TimeUnit unit, int expireTime);


    /**
     * 尝试获取锁
     * @param lockKey
     * @param unit        时间单位
     * @param waitTime    最多等待时间
     * @param expireTime  超时释放时间
     * @return boolean
     */
    boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int expireTime);


    /**
     * 释放锁
     * @param lockKey
     */
    void unlock(String lockKey);


    /**
     * 释放锁
     * @param lock
     */
    void unlock(T lock);


    /**
     * 获取计数器
     * @param name
     * @return
     */
    RCountDownLatch getRCountDownLatch(String name);


    /**
     * 获取信号量
     * @param name
     * @return
     */
    RSemaphore getRSemaphore(String name);

}

 

 
4.2、redisson锁实现类
package com.he.comm.redis;


import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


import java.util.concurrent.TimeUnit;


/**
* <b>@Desc</b>:   redisson实现的分布式锁
* <b>@Author</b>: hesh
* <b>@Date</b>:   2020/6/27
* <b>@Modify</b>:
*/
@Component
public class RedisDistributedLocker implements DistributedLocker<RLock> {


    @Autowired
    private RedissonClient redissonClient;


    @Override
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }


    @Override
    public RLock lock(String lockKey, int expireTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(expireTime, TimeUnit.SECONDS);
        return lock;
    }


    @Override
    public RLock lock(String lockKey, TimeUnit unit, int expireTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(expireTime, unit);
        return lock;
    }


    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int expireTime) {
        RLock lock = redissonClient.getLock(lockKey);


        try {
            return lock.tryLock(waitTime, expireTime, unit);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        //防止线程不持有锁的情况下,在finally调用unlock释放锁,报IllegalMonitorStateException错,增加判断
        // java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: eb987c01-9f60-4612-9a87-2ec8186128f1 thread-id: 124
        // 是否还是锁定状态
        if (lock.isLocked()) {
            // 是否为当前执行线程持有的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    @Override
    public void unlock(RLock lock) {
        //防止线程不持有锁的情况下,在finally调用unlock释放锁,报IllegalMonitorStateException错,增加判断
        // java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: eb987c01-9f60-4612-9a87-2ec8186128f1 thread-id: 124
        if (lock.isLocked()) {
            // 是否为当前执行线程持有的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }


//    @Override
//    public RCountDownLatch getRCountDownLatch(String name) {
//        return redissonClient.getCountDownLatch(name);
//    }

    
//    @Override
//    public RSemaphore getRSemaphore(String name) {
//        return redissonClient.getSemaphore(name);
//    }

}

 

4.3、工具类
package com.he.comm.redis;


import org.redisson.api.RCountDownLatch;
import org.redisson.api.RLock;
import org.redisson.api.RSemaphore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


import java.util.concurrent.TimeUnit;


/**
* <b>@Desc</b>:   redis分布式锁工具类
* <b>@Author</b>: hesh
* <b>@Date</b>:   2020/6/27
* <b>@Modify</b>:
*/
@Component
public class RedisLockUtil{


    @Autowired
    private DistributedLocker<RLock> distributedLocker;


    /**
     * 加锁
     * @param lockKey
     * @return RLock
     */
    public RLock lock(String lockKey) {
        return distributedLocker.lock(lockKey);
    }


    /**
     * 带超时的锁
     * @param lockKey
     * @param expireTime 超时释放时间,单位:秒
     * @return RLock
     */
    public RLock lock(String lockKey, int expireTime) {
        return distributedLocker.lock(lockKey,expireTime);
    }


    /**
     * 带超时的锁
     * @param lockKey
     * @param unit        时间单位
     * @param expireTime 超时释放时间
     * @return RLock
     */
    public RLock lock(String lockKey, TimeUnit unit, int expireTime) {
        return distributedLocker.lock(lockKey, unit, expireTime);
    }


    /**
     * 尝试获取锁
     * @param lockKey
     * @param unit        时间单位
     * @param waitTime    最多等待时间
     * @param expireTime  超时释放时间
     * @return boolean
     */
    public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int expireTime) {
        return distributedLocker.tryLock(lockKey, unit, waitTime, expireTime);
    }


    /**
     * 释放锁
     * @param lockKey
     */
    public void unlock(String lockKey) {
        distributedLocker.unlock(lockKey);
    }


    /**
     * 释放锁
     * @param lock
     */
    public void unlock(RLock lock) {
        distributedLocker.unlock(lock);
    }


    /**
     * 获取计数器
     * @param name
     * @return
     */
//    public RCountDownLatch getRCountDownLatch(String name) {
//        return distributedLocker.getRCountDownLatch(name);
//    }


    /**
     * 获取信号量
     * @param name
     * @return
     */
//    public RSemaphore getRSemaphore(String name) {
//        return distributedLocker.getRSemaphore(name);
//    }
}

 

 
4.4、编写测试controller
 
通过多线程模拟分布式多实例部署,高并发业务场景
package com.he.comm.controller;


import com.he.comm.redis.RedisLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


/**
* <b>@Desc</b>:   测试redis分布式锁
* <b>@Author</b>: hesh
* <b>@Date</b>:   2020/6/28
* <b>@Modify</b>:
*/
@Slf4j
@RequestMapping("/redis_lock")
@RestController
public class TestRedisLockController {


    @Autowired
    private RedisLockUtil redisLockUtil;

    private int lockCount = 10;//有锁共享变量

    private int unlockCount = 10;//无锁共享变量


    @GetMapping("/test")
    public void test(@RequestParam Integer threadnum) {
        //模拟并发测试加锁和不加锁,观察日志打印
        if (threadnum == null || threadnum == 0) threadnum = 10;
        for (int i = 0; i < threadnum; i++) {
            new Thread(() -> {
                //无锁
                testUnlockCount();
                //有锁
                testLockCount();
            }).start();
        }
    }


    private void testUnlockCount() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        unlockCount--;
        log.info("--unlockCount 值:{}", unlockCount);
    }


    private void testLockCount() {
        String lockKey = "lock-test";
        try {
            // 加锁,设置超时时间2s
            redisLockUtil.lock(lockKey, 2);
            Thread.sleep(100);


            lockCount--;
            log.info("--lockCount值: {}", lockCount);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            redisLockUtil.unlock(lockKey);
        }
    }

}

 

 
4.5、测试结果
 
调用API
http://localhost:8060/redis_lock/test?threadnum=10

后,日志打印如下,可见加了分布式锁的,变量有序减少;未加锁的,则出现不规律错乱。

 
 
 
到此,简单的redisson实现分布式锁编码完毕。
 
参考文章:
 
1、分布式锁用 Redis 还是 Zookeeper?
 
2、Redisson是如何实现分布式锁的?
 
3、SET — Redis 命令参考
 
 
 
 
 
 
 
 
 
 
 
 
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值