Redisson实现分布式锁

Redisson 是一个在Redis的基础上实现的Java驻内存数据网格 (In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

官方文档地址:https://github.com/Redisson/Redisson/wiki

1 概述

  • Redisson是一个在Redis的基础上实现的Java驻内存数据网格,封装很多功能,比如分布式锁,布隆过滤器,可以使用很简单方式实现这些功能

2 基本使用

1. 引入依赖

首先,我们需要在项目的pom.xml中添加Redisson和Nacos的依赖。以下是使用Maven的示例:

XML 复制代码
<!-- redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.3</version> <!-- 根据最新版本选择 -->
</dependency>
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-spring-context</artifactId>
    <version>2.0.3</version> <!-- 根据最新版本选择 -->
</dependency>

2. 创建Nacos配置

在nacos中进行配置管理

XML 复制代码
spring:  
  data:
    redis:
      host: 192.168.200.130
      port: 6379
      database: 0
      timeout: 1800000
      password:
      jedis:
        pool:
          max-active: 20 #最大连接数
          max-wait: -1    #最大阻塞等待时间(负数表示没限制)
          max-idle: 5    #最大空闲
          min-idle: 0     #最小空闲

3.创建配置类,初始redisson对象

java 复制代码
package com.atguigu.tingshu.common.config.redssion;

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

/**
 * redisson配置信息
 */
@Data
@Configuration
@ConfigurationProperties("spring.data.redis")
public class RedissonConfig {

    private String host;

    private String password;

    private String port;

    private int timeout = 3000;
    private static String ADDRESS_PREFIX = "redis://";

    /**
     * 自动装配
     *
     */
    @Bean
    RedissonClient redissonSingle() {
        Config config = new Config();

        if(StringUtils.isEmpty(host)){
            throw new RuntimeException("host is  empty");
        }
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
                .setTimeout(this.timeout);
        if(!StringUtils.isEmpty(this.password)) {
            serverConfig.setPassword(this.password);
        }
        return Redisson.create(config);
    }
}

整个流程的序列图如下:

4. 基本示例

java 复制代码
// 阻塞式获取锁
RLock lock = redisson.getLock("myLock");
lock.lock(); //加锁

//执行需要锁保护的代码
//执行需要锁保护的代码
//执行需要锁保护的代码
//执行需要锁保护的代码
//执行需要锁保护的代码

lock.unlock(); //解锁

// 非阻塞式获取锁
RLock lock = redisson.getLock("myLock");
if (lock.tryLock(10, TimeUnit.SECONDS)) {
  // 执行需要锁保护的代码
  lock.unlock();
} else {
  // 获取锁失败,执行其他操作
}

5. 测试代码

java 复制代码
package com.atguigu.tingshu.album.service.impl;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.atguigu.tingshu.album.service.TestService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;


@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public void testLock() {
        //0.1 创建锁对象
        RLock lock = redissonClient.getLock("lock");

        //0.2 获取锁
        //0.2.1 一直等待到获取锁,如果获取锁成功,锁的有效时间为:30s,底层启动"看门狗"线程(如果业务有超时风险)可以延迟锁有效时间
        lock.lock();

        //0.2.2 一直等待到获取锁,如果获取锁成功,自定义锁有效时间
        //lock.lock(10, TimeUnit.SECONDS);

        //0.2.3 尝试获取锁 参数1:等待获取锁时间,超时则放弃获取  参数2:如果获取锁成功,锁的有效时间 参数3:时间单位
        //boolean b = lock.tryLock(3, 10, TimeUnit.SECONDS);
        try {
            //1.从Redis缓存中获取key="num"的值  保证redis中存在"num"(手动提前在redis中创建key)
            String value = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(value)) {
                return;
            }
            //2.对获取到值进行+1操作
            int num = Integer.parseInt(value);
            stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {
            //3. 释放锁
            lock.unlock();
        }
    }
}

整个业务,在查询一个专辑详情时,添加缓存和分布式锁代码

java 复制代码
@Override
public AlbumInfo getAlbumInfoById(Long id) {
   //生成数据key,包含专辑id
   String albumKey = RedisConstant.ALBUM_INFO_PREFIX+ id;
   //1 查询redis缓存
   AlbumInfo albumInfo = (AlbumInfo)redisTemplate.opsForValue().get(albumKey);

   //2 如果redis查询不到数据,查询mysql,把mysql数据放到redis里面
   if(albumInfo == null) {
      // 查询mysql添加分布式锁,使用Redisson实现
      try {//获取锁对象
         String albumLockKey = RedisConstant.ALBUM_INFO_PREFIX+id+ ":lock";
         RLock rLock = redissonClient.getLock(albumLockKey);
         //加锁
         // rLock.lock(); 加锁,默认值
         //rLock.lock(30,TimeUnit.SECONDS); 自己设置过期时间
         // trylock三个参数:第一个等待时间,第二个参数过期时间
         boolean tryLock = rLock.tryLock(10, 30, TimeUnit.SECONDS);
         if(tryLock) { //加锁成功
            try {
               //查询mysql,
               AlbumInfo albumInfoData = this.getAlbumInfoData(id);
               if(albumInfoData == null) {
                  albumInfoData = new AlbumInfo();
                  //把mysql查询数据放到redis里面
                  redisTemplate.opsForValue().set(albumKey,albumInfoData,
                        RedisConstant.ALBUM_TIMEOUT,TimeUnit.SECONDS);
               }
               //把mysql查询数据放到redis里面
               redisTemplate.opsForValue().set(albumKey,albumInfoData,
                     RedisConstant.ALBUM_TIMEOUT,TimeUnit.SECONDS);

               return albumInfoData;

            }finally {
               //解锁
               rLock.unlock();
            }
         } else {
            // 没有获取到锁的线程,自旋
            return getAlbumInfoById(id);
         }
      } catch (InterruptedException e) {
         throw new RuntimeException(e);
      }
   } else {
      //3 如果redis查询数据,直接返回
      return albumInfo;
   }
}

3 两个机制

第一个:重试机制

  • 重试机制 是由于各种原因导致的锁竞争失败,Redisson会自动进行重试,直到获取到锁或者超时。这个机制可以有效地避免因为网络问题等不可控因素导致的锁竞争失败。

第二个:看门狗机制(WatchDog)

  • **看门狗机制(WatchDog)**是 指在获取锁之后,Redisson会启动一个守护线程来监控锁的情况,如果锁的过期时间即将到达,守护线程会自动续期。保证操作在有锁状态下执行

  • 看门狗机制是Redission提供的一种自动延期机制,这个机制使得 Redission提供的分布式锁是可以自动续期的。

  • 在Redission中想要启动看门狗机制,不要自己设置过期时间。如果自己定义了过期时间,无论是通过 lock 还是 tryLock 方法,都无法启用看门狗机制 。

4 使用优化

在项目中很多数据接口需要查询缓存,那么分布式锁的业务逻辑代码就会出现大量的重复。因此,我们可以借助Spring框架事务注解来实现简化代码。只要类上添加了一个注解,那么这个注解就会自带分布式锁的功能。

实现如下:

在模块中添加一个自定义注解

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GuiguCache {
    /**
     * 缓存key的前缀
     * @return
     */
    String prefix() default "cache";
}

创建AOP切面类

java 复制代码
@Aspect
@Component
public class GuiguCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    //当方法上有GuiguCache时候,这个方法执行
    @SneakyThrows
    @Around("@annotation(com.atguigu.tingshu.common.cache.GuiguCache)")
    public Object cacheAspect(ProceedingJoinPoint joinPoint) {
        //1 获取业务方法上注解里面前缀值 album:Info @GuiguCache(prefix="album:Info")
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        GuiguCache guiguCache = signature.getMethod().getAnnotation(GuiguCache.class);
        String prefix = guiguCache.prefix();

        //2 获取业务方法里面参数值 id值  public AlbumInfo getAlbumInfoById(Long id)
        Object[] args = joinPoint.getArgs();

        //3 根据前缀值 + id值生成redis的key
        String key = prefix+ Arrays.asList(args).toString();

        //4 查询redis
        Object o = redisTemplate.opsForValue().get(key);
        if(o == null) {
            try {
                //5 如果redis查询不到数据,查询mysql,把mysql数据放到redis里面,返回数据
                //5.1 添加分布式锁
                //获取锁对象
                RLock rLock = redissonClient.getLock(key + ":lock");
                //加锁
                boolean tryLock = rLock.tryLock(10, 30, TimeUnit.SECONDS);
                if(tryLock) {//加锁成功
                    try {
                        //查询mysql
                        Object obj = joinPoint.proceed(args);
                        if (null == obj){
                            // 并把结果放入缓存
                            obj = new Object();
                            this.redisTemplate.opsForValue().set(key, obj, 1,TimeUnit.HOURS);
                            return obj;
                        }
                        redisTemplate.opsForValue().set(key,obj,1,TimeUnit.HOURS);
                        return obj;
                    } finally {
                        rLock.unlock();
                    }
                } else {//加锁失败
                    return cacheAspect(joinPoint);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        } else {
            //6 如果redis查询到数据,直接返回
            return o;
        }
    }
}

这样只要在实现类中添加该注解,设置前缀,redis里面key名字前缀 ,即可使用分布式锁

例如:

java 复制代码
@GuiguCache(prefix = RedisConstant.ALBUM_INFO_PREFIX)
@Override
public AlbumInfo getAlbumInfoById(Long id) {
  return this.getAlbumInfoDB(id);
}

@Override
@GuiGuCache(prefix = "albumStat:")
public AlbumStatVo getAlbumStatVoByAlbumId(Long albumId) {
  //	调用mapper 层方法
  return albumInfoMapper.selectAlbumStat(albumId);
}

@GuiguCache(prefix = "category:")
@Override
public BaseCategoryView getCategoryViewByCategory3Id(Long category3Id) {
  return baseCategoryViewMapper.selectById(category3Id);
}
相关推荐
龙哥·三年风水4 分钟前
workman服务端开发模式-应用开发-总架构逻辑说明
分布式·gateway·php
无涯学徒199831 分钟前
J8学习打卡笔记
笔记·学习
lzz的编码时刻40 分钟前
Spring Boot 声明式事务
java·spring boot·后端
KpLn_HJL1 小时前
leetcode - 1530. Number of Good Leaf Nodes Pairs
android·java·leetcode
Akamai中国1 小时前
基于容器的云原生,让业务更自由地翱翔云端
分布式·云原生·云计算·云服务·云平台·akamai·云主机
Qzer_4071 小时前
在JVM(Java虚拟机)中,PC寄存器(Program Counter Register)扮演着至关重要的角色,它是JVM执行引擎的核心组成部分之一。
java·开发语言·jvm
星沁城1 小时前
JVM的垃圾回收机制
java·开发语言·jvm
FF在路上2 小时前
MybatisPlus使用LambdaQueryWrapper更新时 int默认值问题
java·开发语言·mybatis
gb42152872 小时前
java中sha256和md5某个字符串实例代码
java·开发语言·哈希算法
三十六煩惱風2 小时前
Java中常见的锁及其应用场景
java