Redisson(分布式锁、限流)

注意Redisson是基于Redis的,所以必须先引入Redis配置(参考SpringBoot集成Redis文章

1. 集成Redisson

  1. 引入依赖

    <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.16.2</version> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.4</version> </dependency>
  2. 自定义配置

    package com.tjx.config;

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

    /**

    • @author tianjiaxin

    • @createTime 2024/6/4 15:31

    • @Description:
      */
      @Configuration
      public class RedissonConfig {

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

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

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

      @Bean
      public RedissonClient getRedisson() {
      System.out.println("初始化redisson : " + redisHost);
      Config config = new Config();
      // 单机配置,集群配置用config.useClusterServers(),自行百度网上太多了!
      config.useSingleServer().setAddress("redis://" + redisHost + ":" + port).setPassword(password);
      config.setCodec(new JsonJacksonCodec());
      return Redisson.create();
      }
      }

2. 分布式锁

参考文章:

  1. 分布式锁使用:https://blog.csdn.net/liuerpeng1904/article/details/135459765
  2. 加解锁分析:https://writer.blog.csdn.net/article/details/130613740

2.1. 如何使用?

redissonClient.getLock("key"); 获取锁
rLock.tryLock(1, TimeUnit.MINUTES); 在1分钟内尝试加锁,并返回是否获取成功结果
rLock.lock(); 加锁,如果不成功则一直阻塞
rLock.unlock(); 释放锁
rLock.isHeldByCurrentThread() 判断当前线程是否持有锁(防止超时或网络原因造成自动释放锁,从而防止在手动释放锁的时候找不到锁而产生报错)
package com.tjx.service.impl;

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

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *
 * </p>
 *
 * @author tianjiaxin
 * @createTime 2024/6/4 14:46
 * @Description:
 */
@Service
public class RedissonServiceImpl {
    @Autowired
    private RedissonClient redissonClient;

    public void test() {
//        new Thread(this::business_01).start();
//        new Thread(this::business_02).start();
//        new Thread(this::business_03).start();
        new Thread(this::business_04).start();
        new Thread(this::business_05).start();
    }

    public void business_01() {
        // 1.获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + ": business_01 锁获取成功");
        try {
            // tryLock: 直接返回获取结果,1分钟内不断尝试获取锁,如果获取成功执行业务逻辑,否则释放锁
            boolean isLocked = rLock.tryLock(1, TimeUnit.MINUTES);
            System.out.println(getDate() + " : business_01 加锁成功");
            if (isLocked) {
                System.out.println(getDate() + ": business_01 开始执行业务逻辑");
                Thread.sleep(60000);
                System.out.println(getDate() + ": business_01 结束执行业务逻辑");
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + ": business_01 释放锁");
            // 判断当前线程是否持有锁(防止超时或网络原因造成自动释放锁,从而防止在手动释放锁的时候找不到锁而产生报错)
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }

    public void business_02() {
        // 1.获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + ": business_02 锁获取成功");
        try {
            boolean isLocked = rLock.tryLock(1, TimeUnit.MINUTES);
            System.out.println(getDate() + " : business_02 加锁成功");
            if (isLocked) {
                System.out.println(getDate() + ": business_02 开始执行业务逻辑");
                Thread.sleep(60000);
                System.out.println(getDate() + ": business_02 结束执行业务逻辑");
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + ": business_02 释放锁");
            // 判断当前线程是否持有锁(防止超时或网络原因造成自动释放锁,从而防止在手动释放锁的时候找不到锁而产生报错)
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }

    public void business_03() {
        // 1.获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + ": business_03 锁获取成功");
        try {
            boolean isLocked = rLock.tryLock(1, TimeUnit.MINUTES);
            System.out.println(getDate() + " : business_03 加锁成功");
            if (isLocked) {
                System.out.println(getDate() + ": business_03 开始执行业务逻辑");
                Thread.sleep(60000);
                System.out.println(getDate() + ": business_03 结束执行业务逻辑");
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + ": business_03 释放锁");
            // 判断当前线程是否持有锁(防止超时或网络原因造成自动释放锁,从而防止在手动释放锁的时候找不到锁而产生报错)
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }

    public void business_04() {
        // 1.获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + ": business_04 锁获取成功");
        try {
            // 如果获取不到锁,则一直阻塞,直到获取到锁
            rLock.lock();
            System.out.println(getDate() + " : business_04 加锁成功");
            System.out.println(getDate() + ": business_04 开始执行业务逻辑");
            Thread.sleep(10000);
            System.out.println(getDate() + ": business_04 结束执行业务逻辑");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + ": business_04 释放锁");
            // 判断当前线程是否持有锁(防止超时或网络原因造成自动释放锁,从而防止在手动释放锁的时候找不到锁而产生报错)
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }

    public void business_05() {
        // 1.获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + ": business_05 锁获取成功");
        try {
            rLock.lock();
            System.out.println(getDate() + " : business_05 加锁成功");
            System.out.println(getDate() + ": business_05 开始执行业务逻辑");
            Thread.sleep(10000);
            System.out.println(getDate() + ": business_05 结束执行业务逻辑");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + ": business_05 释放锁");
            // 判断当前线程是否持有锁(防止超时或网络原因造成自动释放锁,从而防止在手动释放锁的时候找不到锁而产生报错)
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }

    public static String getDate() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String format = dateFormat.format(new Date());
        return format;
    }
}

OMS系统使用:

RLock rLock = redissonClient.getMultiLock(actualDeliveryDTO.getItems().stream()
    .map(OutboundOrderActualDeliveryByTruckloadItemDTO::getOrderNo)
    .distinct()
    .map(orderNo -> redissonClient.getLock(getLockKeyForOrderNo(orderNo)))
    .toArray(RLock[]::new));
try {
    rLock.lock();
    return doUpdateActualDeliveryByTruckload(actualDeliveryDTO);
} finally {
    rLock.unlock();
}

2.2. 加解锁分析

加锁:

  1. 尝试获取锁,通过lua加锁脚本获取
  2. 如果没有获取到锁,则订阅解锁消息,并阻塞
  3. 持有的线程释放锁之后,进行广播并唤醒阻塞的线程

两个问题

  1. 如果持有锁的线程崩了怎么办?
    设置锁的时候有过期时间,超时自动释放
  2. 如果超过过期时间但是还没有执行完怎么办?
    看门狗机制,监听持有锁的线程并定义增加过期时间

加锁lua脚本:

解锁:

很简单,直接执行解锁lua脚本

3. 限流

3.1. 基本使用

API详细解释文章:https://blog.csdn.net/qq_43686863/article/details/135634098

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * <p>
 *
 * </p>
 *
 * @author tianjiaxin
 * @createTime 2024/6/5 11:24
 * @Description:
 */
@Slf4j
@Service
public class RedissonRateLimiterServiceImpl {
    @Autowired
    private RedissonClient redissonClient;

    public void testRateLimiter(String phone) {
        // Step1.创建限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter("PHONE:" + phone);
        // Step2.设置限流参数 每10s生成一个令牌
        // RateType.OVERALL 全局限流, RateType.PER_CLIENT 单机限流
        rateLimiter.trySetRate(RateType.OVERALL, 1, 10, RateIntervalUnit.SECONDS);
        // Step3.尝试获取1个令牌
        // 注意:acquire会阻塞等待, tryAcquire直接返回结果
        /*if (rateLimiter.tryAcquire()) {
            log.info("向手机:{}发送短信", phone);
        } else {
            log.info("被限流了!!!!!");
        }*/
        // 尝试获取1个令牌,获取到返回true,没有获取到就返回false
//        if (rateLimiter.tryAcquire(1)) {
//            log.info("向手机:{}发送短信", phone);
//        } else {
//            log.info("被限流了!!!!!");
//        }
        // 获取一个令牌,获取到返回true,没有获取到最多等待3秒
//        if (rateLimiter.tryAcquire(3, TimeUnit.SECONDS)) {
//            log.info("向手机:{}发送短信", phone);
//        } else {
//            log.info("被限流了!!!!!");
//        }

        // 获取1个令牌,获取到返回true,没有获取到最多等待3秒
        // 注意:permits参数的值必须小于等于trySetRate的rate
        if (rateLimiter.tryAcquire(1, 3, TimeUnit.SECONDS)) {
            log.info("向手机:{}发送短信", phone);
        } else {
            log.info("被限流了!!!!!");
        }

    }
}

3.2. 原理

参考文章:https://github.com/oneone1995/blog/issues/13

主要是通过令牌桶的方式来实现的,令牌桶会设置每间隔多少时间就生产多少令牌,比如10S生产100个令牌,只有拿到令牌才能执行业务逻辑;

具体实现的话有两个重要参数,一个是存储当前剩余的令牌数量,一个是存储每次请求的令牌的时间以及数量;

第一次请求的过来的时候,设置好剩余令牌数,并记录这次请求

第二次请求过来的时候,如果与上一次请求的时间进行比较,如果时间间隔小于令牌桶的间隔,则根据剩余令牌数判断是否可以获取令牌。如果大于令牌桶的间隔,则重置令牌数量;

3.3. 限流常见方案

TODO:理解的比较浅显,后续可以深入了解

参考文章:https://juejin.cn/post/7000152990501847048#heading-4

1. 固定窗口计数器

  • 定义:固定时间窗口计数,比如1秒10次,超过就拒绝否则计数器+1
  • 缺陷:临界问题,比如在0.8s-1s的时候来了10次请求,1s-1.2s来了10次,那其实就是0.8-1.2s有20次请求了!

2. 滑动窗口计数器

  • 定义:在固定时间窗口的基础上,把窗口分隔成n个小窗口,滑动前行,解决了临界问题
  • 缺陷:因为对超出的流量直接放弃,所以削峰填谷;
  • 实现方案:阿里的sentinel

3. 漏桶算法

  • 定义:固定桶大小,固定速度流出,请求数量超出桶大小就拒绝;
  • 缺陷:无法处理突发流量;

4. 令牌桶算法

  • 定义:固定速率生成令牌,每个请求都需要先拿到令牌才能够处理逻辑;可以处理突发流量,比如已经生产了100个令牌,突然来了100个请求,可以直接拿令牌就处理!

  • 实现方案:redisson

相关推荐
Code apprenticeship2 小时前
怎么利用Redis实现延时队列?
数据库·redis·缓存
百度智能云技术站2 小时前
广告投放系统成本降低 70%+,基于 Redis 容量型数据库 PegaDB 的方案设计和业务实践
数据库·redis·oracle
装不满的克莱因瓶2 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
黄名富6 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
G_whang7 小时前
centos7下docker 容器实现redis主从同步
redis·docker·容器
.生产的驴7 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
我叫啥都行10 小时前
计算机基础复习12.22
java·jvm·redis·后端·mysql
阿乾之铭11 小时前
Redis四种模式在Spring Boot框架下的配置
redis
on the way 12313 小时前
Redisson锁简单使用
redis
科马13 小时前
【Redis】缓存
数据库·redis·spring·缓存