基于Redis实现的分布式锁

1、前言

  在《基于MySQL数据库排它锁(for update)实现的分布式锁》中,我们学习了基于Mysql实现的分布式锁,这里我们开始学习基于Redis实现的分布式锁。在这里,我们将尝试两种方法,一种是基于Redis的RedisTemplate客户端实现,一种是基于Redisson实现。

2、基于Redis实现的分布式锁

2.1、实现原理

  主要是利用Redis的setnx命令实现分布式锁。一般使用如下命令:

set resource_name my_random_value NX PX 30000 

其中,

  • resource_name 资源名称,可以根据不同的业务区分不同的锁
  • my_random_value 随机值,用来作为释放锁时,做校验,避免释放掉其他线程的锁,所以该随机数需要不同线程产生不同的随机数,一般使用UUID。
  • NX key不存在时设置成功,key存在时则设置不成功
  • PX 自动失效时间,出现异常情况,锁可以过期失效

  使用该命令实现分布式锁,主要利用了NX的原子性,多线程并发时,只有一个线程可以设置成功,获取到锁后,可以执行后续的业务逻辑(如果出现异常,等待锁过期失效),执行完业务逻辑后,通过Redis的delete命令释放锁,释放锁时需要校验随机数(避免出现释放掉其他线程的锁),为了保证释放锁的原子性,一般采用Lua脚本实现。

2.2、搭建环境

  这里主要基于Spring Boot、redis等组件搭建项目。该项目作为一个模块,继承自qriver-distributed-lock,其中pom文件定义如下:

<modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.qriver</groupId>
        <artifactId>qriver-distributed-lock</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.qriver.distributedlock</groupId>
    <artifactId>distributedlock-redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>


    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

  application.properties文件配置如下:

server.port=8080

#redis配置
spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=123456

#日志配置
logging.level.com.qriver.distributedlock=debug

  最后,SpringBoot默认的启动类,如下:

@SpringBootApplication
public class DistributedlockRedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(DistributedlockRedisApplication.class, args);
    }

}

2.3、实现基于Redis的分布式锁
2.3.1、RedisConfig配置类

  实现RedisConfig配置类,主要是注入RedisTemplate对象。

/**
 * Redis 基础配置
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(factory);
        //key序列化
        RedisSerializer keySerializer = new StringRedisSerializer();
        RedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setKeySerializer(keySerializer);
        //value序列化
        redisTemplate.setValueSerializer(valueSerializer);
        //hash key 序列化
        redisTemplate.setHashKeySerializer(keySerializer);
        //hash value 序列化
        redisTemplate.setHashValueSerializer(valueSerializer);
        //redis初始化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplateGroup(RedisConnectionFactory factory) {
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(factory);
        RedisSerializer keySerializer = new StringRedisSerializer();
        RedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(getMapper());
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        redisTemplate.setHashValueSerializer(valueSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 获取JSON工具
     * @return
     */
    private final ObjectMapper getMapper() {
        ObjectMapper mapper = new ObjectMapper();
        //将类名称序列化到json串中,去掉会导致得出来的的是LinkedHashMap对象,直接转换实体对象会失败
        //设置输入时忽略JSON字符串中存在而Java对象实际没有的属性
        //其中该配置,需要升级fasterxml版本
        //mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return mapper;
    }

}
2.3.2、RedisLock 分布式锁

  首先,RedisLock 分布式锁实现了AutoCloseable 接口,通过实现close方法,使得发生异常或逻辑执行完成后,自动释放分布式锁。

AutoCloseable接口位于java.lang包下,从JDK1.7开始引入。引入该接口就是为了更好的进行管理资源,准确说是资源的释放。当一个类实现了该接口close方法,在使用try-catch-resources语法创建的资源抛出异常后,JVM会自动调用close 方法进行资源释放,当没有抛出异常正常退出try-block时候也会调用close方法。像数据库链接类Connection,io类InputStream或OutputStream都直接或者间接实现了该接口。

  然后,提供了一个构造函数,在需要使用分布式锁的地方,需要通过new方法创建锁对象。

  又提供了加锁方法getLock(),其中通过redisTemplate.execute()方法执行Redis命令,实际上就是通过setnx命令为一个指定的key设置value值,如果设置成功就表示获取到锁了(在没有释放锁或锁失效前,其他线程将无法获取到锁),否则就是获取锁失败。

  最后,提供了一个释放锁的方法unLock(),该方法是通过执行了一一段LUA脚本实现了锁释放(比较操作和删除操作)的原子性。

public class RedisLock implements AutoCloseable {

    private Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private String key;
    private String value;
    //单位:秒
    private int expireTime;

    private RedisTemplate<String, Object> redisTemplate;

    public RedisLock(RedisTemplate<String, Object> redisTemplate, String key,int expireTime){
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime = expireTime;
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取分布式锁
     * @return
     */
    public boolean getLock(){
        logger.debug("进入获取锁的方法。");
        RedisCallback<Boolean> redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            //序列化key
            RedisSerializer keySerializer = new StringRedisSerializer();
            byte[] redisKey = keySerializer.serialize(key);
            //序列化value
            RedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
            byte[] redisValue = valueSerializer.serialize(value);
            //执行setnx操作
            Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
            return result;
        };
        //获取分布式锁
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        logger.debug("获取分布式锁的结果:" + lock);
        return lock;
    }

    public boolean unLock() {
        logger.debug("进入释放锁的方法。");
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Arrays.asList(key);

        Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
        logger.debug("释放锁的结果:"+result);
        return result;
    }


    @Override
    public void close() throws Exception {
        unLock();
    }

}
2.3.3、RedisLock 分布式锁的应用

  这里实现一个DemoController类,来应用基于Redis实现的分布式锁,实现如下:

@RestController
public class DemoController {

    private Logger logger = LoggerFactory.getLogger(DemoController.class);

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @RequestMapping("redisLock")
    @Transactional(rollbackFor = Exception.class)
    public String testLock() throws Exception {
        logger.debug("进入testLock()方法;");
        try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
            if (redisLock.getLock()) {
                logger.debug("获取到分布式锁;");
                //注意,如果大于超时时间,则会释放锁失败
                Thread.sleep(20 * 1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        logger.debug("方法执行完成;");
        return "方法执行完成";
    }

}

2.3.4、测试

  分别启动端口为8080、8081的两个实例,启动方法可以参考《IntelliJ Idea如何为一个项目启动多个项目实例》。然后,在浏览器分别访问http://localhost:8081/redisLock、 http://localhost:8080/redisLock两个地址,在控制台打印的日志如下:

在这里插入图片描述
在这里插入图片描述
  通过打印日志的时间我们可以知道,和基于数据库的一个很重要的区别就是:在基于数据库的实现中,线程会等待获取锁,然后执行业务逻辑,而在基于Redis的实现中,则直接返回获取锁失败,并执行获取锁失败的后续逻辑,即不支持阻塞。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姠惢荇者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值