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

相关推荐
Dlwyz1 小时前
问题: redis-高并发场景下如何保证缓存数据与数据库的最终一致性
数据库·redis·缓存
飞升不如收破烂~2 小时前
redis的List底层数据结构 分别什么时候使用双向链表(Doubly Linked List)和压缩列表(ZipList)
redis
吴半杯4 小时前
Redis-monitor安装与配置
数据库·redis·缓存
J不A秃V头A4 小时前
Redisson 中开启看门狗(watchdog)机制
java·分布式锁·看门狗
会code的厨子5 小时前
Redis缓存高可用集群
redis·缓存
尽兴-6 小时前
Redis模拟延时队列 实现日程提醒
java·redis·java-rocketmq·mq
Karoku06611 小时前
【企业级分布式系统】ELK-企业级日志分析系统
运维·数据库·redis·mysql·elk·缓存
是店小二呀11 小时前
【C++】右值引用与移动语义详解:如何利用万能引用实现完美转发
c++·redis
一直要努力哦1 天前
Redis的高可用性
数据库·redis·缓存
吃着火锅x唱着歌1 天前
Redis设计与实现 学习笔记 第十八章 发布与订阅
redis·笔记·学习