Redis = Remote Dictionary Server,即远程字典服务。

Redis是由C语言编写的开源、基于内存、支持多种数据结构、高性能的Key-Value数据库。

1.安装(docker):

1.1.拉取redis镜像:

//拉取最新版本
docker pull redis
//拉取指定版本
docker pull redis:6.0.8

1.2.查看拉取是否成功:

docker images
查看所有的镜像

image-20231018092741654

1.3.编辑配置文件:

新建一个文件,命名为redis.conf 修改为

# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#bind 127.0.0.1
​
protected-mode no
​
port 6379
​
tcp-backlog 511
​
requirepass root
​
timeout 0
​
tcp-keepalive 300
​
daemonize no
​
supervised no
​
pidfile /var/run/redis_6379.pid
​
loglevel notice
​
logfile ""
​
databases 30
​
always-show-logo yes
​
save 900 1
save 300 10
save 60 10000
​
stop-writes-on-bgsave-error yes
​
rdbcompression yes
​
rdbchecksum yes
​
dbfilename dump.rdb
​
dir ./
​
replica-serve-stale-data yes
​
replica-read-only yes
​
repl-diskless-sync no
​
repl-disable-tcp-nodelay no
​
replica-priority 100
​
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
​
appendonly yes
​
appendfilename "appendonly.aof"
​
no-appendfsync-on-rewrite no
​
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
​
aof-load-truncated yes
​
aof-use-rdb-preamble yes
​
lua-time-limit 5000
​
slowlog-max-len 128
​
notify-keyspace-events ""
​
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
​
list-max-ziplist-size -2
​
list-compress-depth 0
​
set-max-intset-entries 512
​
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
​
hll-sparse-max-bytes 3000
​
stream-node-max-bytes 4096
stream-node-max-entries 100
​
activerehashing yes
​
hz 10
​
dynamic-hz yes
​
aof-rewrite-incremental-fsync yes
​
rdb-save-incremental-fsync yes

并将此文件上传到安装redis服务器的某个位置,这里为/home/redis

1.4 启动容器:

docker run  -p 6379:6379 --name myredis -v /home/redis/redis.conf:/etc/redis/redis.conf -v /home/redis/data:/data -d redis redis-server /etc/redis/redis.conf    --requirepass "root"

image-20231018093125001

1.5 查看容器是否创建成功:

docker ps
查看所有运行的容器

image-20231018093233068

1.6 内部连接:

 docker exec -it myredis redis-cli
// auth 密码
 auth root

image-20231018093321575

1.7 基本使用:

image-20231018093357573

2.特性:

2.1 数据存储:

内存指的就是主板上的存储部件,CPU直接与之沟通,并用其存储数据的部件,存放当前正在使用的(即执行中的)数据和程序,它的物理实质就是一组或多组具备数据输入输出和数据存储功能的集成电路,内存只用于暂时存放程序和数据,一旦关闭电源或发生断电,其中的程序和数据就会丢失。外存包括软盘、硬盘和光盘,存放在其中的数据靠磁来维持,因此可永久保存数据。

Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。

2.2 Nosql:

Nosql = not only sql(不仅仅是SQL).

关系型数据库:列+行,同一个表下数据的结构是一样的。

非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。

Redis是NoSQL数据库系统,它是一种内存数据库,主要用于存储键值对(key-value)数据。

2.3 单线程:

Redis的处理网络请求部分采用的是单线程,如果想充分利用CPU资源的话,可以多开几个Redis实例来达到目的,为什么单线程还是速度快的原因呢?我们知道Redis的读写都是基于内存的,读写速度都是非常快的,不会出现需要等待很长时间,所以瓶颈并不会出现在请求读写上,所以没必要使用多线程来利用CPU,如果使用多线程的话(线程数>CPU数情况下),多线程的创建、销毁、线程切换、线程竞争等开销所需要的时间会比执行读写所损耗的时间还多,那就南辕北辙了,当然这是在数据量小的时候才会这样,如果数据量到达一定量级了,那肯定是多线程比单线程快(线程数<=CPU数情况下)。

QQ截图20230215111849

2.4 持久化:

redis数据保存在内存中,而内存断电易失的特性.所以redis提供了持久化的机制. Redis 提供了两种持久化的机制,用于在服务器重启或崩溃时保护数据,以确保数据的持久性。这两种持久化方法分别是快照(Snapshotting)和追加式文件(Append-Only File):

  1. 快照(RDB - Redis DataBase)持久化

    RDB 是一种基于时间点的持久化方法,它可以将 Redis 数据的快照保存到磁盘上的二进制文件中。通常,RDB 文件的扩展名为.rdb。这个文件包含了 Redis 在某个时间点上的数据快照,以及一些元数据。

    • 优点

      • RDB 文件非常紧凑,适用于备份数据和迁移数据。

      • 在 Redis 重启时加载 RDB 文件,恢复速度快。

      • 可以定期自动创建快照。

    • 缺点

      • 如果 Redis 崩溃,数据可能会在最后一次快照之后丢失。

      • 创建快照时,Redis 可能会在某个时间点停止响应客户端请求。

    RDB 持久化可以通过配置文件中的选项来启用和配置,以及通过执行 SAVE、BGSAVE 命令手动触发。

  2. 追加式文件(AOF - Append-Only File)持久化

    AOF 持久化记录了 Redis 服务器接收的写命令,以文本追加方式将这些命令写入磁盘上的日志文件。这个文件包含了可以重新构建数据集的所有写操作。

    • 优点

      • 数据完全持久化,不会丢失任何写操作。

      • 适用于数据的持续追加和重放。

    • 缺点

      • AOF 文件通常比 RDB 文件大,因为它包含了所有写操作的文本记录。

      • 数据恢复速度较慢,因为需要重放所有写操作。

    AOF 持久化可以通过配置文件中的选项来启用和配置,以及通过执行 BGREWRITEAOF 命令手动触发。

通常情况下,Redis 用户可以选择启用 RDB 持久化、AOF 持久化,或者同时启用两种方式,以兼顾数据恢复速度和数据完整性。RDB 适用于定期备份数据和快速恢复,而 AOF 适用于确保数据的完整性,尤其在关注数据不丢失的场景下。此外,Redis 提供了多种持久化配置选项,允许用户根据实际需求进行定制。

2.5 数据结构:

Redis支持五种基本的数据结构,分别是String(字符串),Hash(哈希),List(列表),Set(集合),Zset(即Sorted Set有序集合),这五种结构针对的都是value的组成,这些数据结构类型和我们使用的开发语言的数据结构类型其实是相对应的。

168d03ecb4ed8d57~tplv-t2oaga2as0_0_0_q75

image-20231018161251678

2.5.1 string:

String 是一种最简单的数据类型,它是二进制安全的,可以包含任意数据,最大长度为 512MB。Redis 的 String 类型不同于常规编程语言中的字符串,它可以包含任何数据,包括二进制数据。

C语言的字符串是char[]实现的,而Redis使用SDS(simple dynamic string) 封装:

ad018b1bb30479015c921e944ea9431d

2.5.2 Hash:

哈希.

  • 简介:在Redis中,哈希类型是指v(值)本身又是一个键值对(k-v)结构.

68510cbfa131a0375f0ab0611d2468c2

例如在这个例子内,user:1这个键有两个hash值,分别是name jay,age 18.

注意,Hash 是一种键值对数据结构,其中一个键对应多个字段和值的组合。这意味着一个 Hash 键可以有多个字段和它们各自的值,而不是一个键对应多个值。

看成一个java中的map似乎更好理解一点.

Hash 数据结构非常适合用于表示类似用户数据这样的结构化信息,以及其他需要存储多个字段和值之间关联的场景。

2.5.3 list:

List(列表)

  • 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。

  • 属于队列,有先进先出的特性.

  • 数据可以重复.

下面是一个简单的例子,用list来实现消息队列:

# 将消息推入队列的右侧(尾部)
RPUSH messages "Hello, Redis!"
RPUSH messages "This is a test message."
RPUSH messages "Redis is great!"
​
# 获取队列的长度
LLEN messages
# 输出:3
​
# 从队列的左侧(头部)弹出消息
LPOP messages
# 输出: "Hello, Redis!"
​
# 再次获取队列的长度
LLEN messages
# 输出:2
​
# 获取队列中的所有消息
LRANGE messages 0 -1
# 输出: ["This is a test message.", "Redis is great!"]
​
# 从队列的左侧(头部)继续弹出消息
LPOP messages
# 输出: "This is a test message."
​
# 再次获取队列的长度
LLEN messages
# 输出:1
  • "messages" 是 Redis List 的键(key),它代表了消息队列的名字。

  • "Hello, Redis!"、"This is a test message."、"Redis is great!" 是 List 中的元素(value),它们分别代表了消息队列中的三条消息。

List 数据结构的有序性和允许重复元素的特性使其非常适合实现消息队列,任务队列,以及其他需要有序存储和访问数据的场景。

2.5.4 set:

集合.

  • 简介:集合(set)类型也是用来保存多个的字符串元素

  • 不允许重复元素.

  • 无序的.

下面是一个使用set的例子:

# 添加用户1的兴趣爱好
SADD user:1:hobbies "Reading" "Cooking" "Hiking" "Gardening"
​
# 添加用户2的兴趣爱好
SADD user:2:hobbies "Traveling" "Photography" "Hiking"
​
# 获取用户1的兴趣爱好
SMEMBERS user:1:hobbies
# 输出: ["Reading", "Cooking", "Hiking", "Gardening"]
​
# 获取用户2的兴趣爱好
SMEMBERS user:2:hobbies
# 输出: ["Traveling", "Photography", "Hiking"]
​
# 查看用户2是否喜欢 "Reading"
SISMEMBER user:2:hobbies "Reading"
# 输出: 0(表示不喜欢)
​
# 查看用户2是否喜欢 "Hiking"
SISMEMBER user:2:hobbies "Hiking"
# 输出: 1(表示喜欢)
​
# 获取用户1和用户2共同的兴趣爱好
SINTER user:1:hobbies user:2:hobbies
# 输出: ["Hiking"]
​
# 获取用户1和用户2的所有兴趣爱好
SUNION user:1:hobbies user:2:hobbies
# 输出: ["Reading", "Cooking", "Hiking", "Gardening", "Traveling", "Photography"]
​

Set 数据结构非常适合处理无序且不允许重复元素的数据集合。

2.5.5 sorted set:(zset)

有序集合.

有序集合(Sorted Set)是一种数据结构,它是集合数据结构的扩展,每个成员都关联一个分数(score),这个分数用于对成员进行排序.

# 使用 Redis 命令行客户端进行操作
​
# 添加用户分数到排行榜
ZADD leaderboard 1000 "User1"
ZADD leaderboard 1500 "User2"
ZADD leaderboard 800 "User3"
ZADD leaderboard 1200 "User4"
​
# 获取排行榜前3名用户及其分数
ZRANGE leaderboard 0 2 WITHSCORES
# 输出:
# ["User3", "800", "User1", "1000", "User4", "1200"]
​
# 获取用户 "User2" 的分数
ZSCORE leaderboard "User2"
# 输出: "1500"
​
# 获取排行榜上 "User3" 的排名(按分数从低到高)
ZRANK leaderboard "User3"
# 输出: 0
​
# 删除用户 "User4" 从排行榜
ZREM leaderboard "User4"
# 输出: 1
​
# 获取当前排行榜的成员数量
ZCARD leaderboard
# 输出: 3

而他们实现是通过跳跃表:(多级随机索引, 为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。)

5dce8dd07bcdad365d4e3dd71bb12f8f

有序集合非常适合实现需要按分数排序的场景,如排行榜、评分系统等。

更多redis在分布式中的特性,参看【精选】Redis?它主要用来什么的_码农汉子的博客-CSDN博客

3.使用:

使用之前确保redis能够在服务器本地进行连接,如果是云服务器,还需要在控制台打开端口.

3.1 springboot整合redis:

3.1.1 引入依赖:

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

3.1.2 编写配置类:

  redis:
    # Redis服务器地址
    host: 101.42.37.250
    # Redis服务器端口号
    port: 6379
    # 使用的数据库索引,默认是0
    database: 0
    # 连接超时时间
    timeout: 1800000
    # 设置密码
    password: root
    lettuce:
      pool:
      # 最大阻塞等待时间,负数表示没有限制

3.1.3 配置bean:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
​
@Configuration
public class RedisConfig {
​
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        return template;
    }
}

3.1.4 使用bean:

 @Resource
 RedisTemplate<String, String> redisTemplate;
​
 @PostMapping("/test1")
 public String searchScore1(@RequestParam("key") String key,@RequestParam("value") String value) {
    redisTemplate.opsForValue().set(key, value);
    return redisTemplate.opsForValue().get(key);
}

3.2 Another Redis DeskTop Manager:

Another Redis Desktop Manager 是一款备受推崇的Redis可视化管理工具。

  • 兼容Windows、Mac、Linux

  • 更快、更好、更稳定的Redis桌面(GUI)管理客户端

  • 支持加载海量的key

3.2.1 下载:

Release v1.6.1 · qishibo/AnotherRedisDesktopManager (github.com)

windows直接找.exe,下载下来安装完成就可以使用了.

3.2.2 连接:

image-20231018165701777

不需要输入用户名,输入地址端口和密码就可以.

连接成功:

image-20231018165820905

4.应用场景:

4.1 为什么需要redis:

QQ截图20230215111415

QQ截图20230215111447

一定程度上实现持久化。

4.2. 应用场景举例:

4.2.1 连续签到:

把redis当做数据库使用,key设置为用户id,value设置为用户天数,expireAt(自动过期)设置为后天的零点.

4.2.2 消息队列:

用数据类型中的list实现.

QQ截图20230215112738

4.2.3 缓存:

用作某些关键数据的缓存,当请求发生时,先去redis中寻找,当redis中寻找不到这些数据时,再去查找数据库并返回,同时把这些查找到的数据存入redis.

需要注意的是,这种基本的写回逻辑并不是对所有情况都适用,对于那些永久的key,如果增加写回逻辑可能会增加接口反应的时间,而对于那些可能会被业务代码删除的key,可以采用这种逻辑.

4.2.3.1 缓存token黑名单:

当用户登录时,把用户token中的某些特定的字段作为key,不需要设置value.

当用户登录时,把该用户token的key从黑名单中移除,用户登出时,重新根据token中的某些特定字段作为key重新存入redis.

当用户访问其它接口时,需要验证token是否在黑名单中,如果在,说明用户没有正确的进行登录操作,提高了安全性.

删除key:

// API接口,用于获取OAuth2令牌
    @ApiOperation(value = "OAuth2认证", notes = "登录入口")
    @PostMapping("/token")
    public Object postAccessToken(
            Principal principal,
            @RequestParam Map<String, String> parameters
    ) throws HttpRequestMethodNotSupportedException {
        // 从请求中获取客户端ID,通常在请求头中的Authorization字段中,经过加密,例如 Basic Y2xpZW50OnNlY3JldA==,明文等于 client:secret
        String clientId = RequestUtils.getOAuth2ClientId();
        log.info("OAuth认证授权 客户端ID:{},请求参数:{}", clientId, JSONUtils.toJSONString(parameters));
​
        // 使用TokenEndpoint来获取OAuth2访问令牌
        OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
​
        // 将用户ID从黑名单中移除,这是一个安全措施
        String userId = String.valueOf(accessToken.getAdditionalInformation().get(SecurityConstant.JWT_ID));
        redisTemplate.delete(SecurityConstant.USERID_BLACKLIST_PREFIX + clientId + RedisKeys.SPLIT + userId);
​
        return Response.success(accessToken);
    }

新增key:

 // API接口,用于注销操作
    @ApiOperation(value = "注销")
    @DeleteMapping("/logout")
    public Response logout() {
        // 从JWT令牌中获取有关令牌的信息
        JSONObject payload = JwtUtils.getJwtPayload();
        String jti = payload.getStr(SecurityConstant.JWT_JTI); // JWT唯一标识
        Long expireTime = payload.getLong(SecurityConstant.JWT_EXP); // JWT过期时间戳(单位:秒)
​
        // 根据令牌过期时间,将令牌添加到缓存作为黑名单以限制访问
        if (expireTime != null) {
            long currentTime = System.currentTimeMillis() / 1000; // 当前时间(单位:秒)
            if (expireTime > currentTime) { // 如果令牌尚未过期,添加至缓存并设置缓存时间为令牌剩余有效时间
                redisTemplate.opsForValue().set(SecurityConstant.TOKEN_BLACKLIST_PREFIX + jti, null, (expireTime - currentTime), TimeUnit.SECONDS);
            }
        } else { // 如果令牌永不过期,则永久将其加入黑名单
            redisTemplate.opsForValue().set(SecurityConstant.TOKEN_BLACKLIST_PREFIX + jti, null);
        }
        return Response.success("注销成功");
    }

4.2.3.2 缓存权限-角色列表:

在鉴权时,缓存权限-角色列表,加快鉴权的速度.

根据四个不同的端,设置了四个特定的key,value为hash,存储<权限,对应的角色id集合>的列表.

存入redis:

   public boolean refreshPermRolesRules() {
        //首先根据特定的四个key删除跟此业务相关的缓存数据.(此处体现使用特定key的好处,处理起来比较简单)
        redisTemplate.delete(SecurityConstant.ADMIN_WEB_URL_PERM_ROLES_KEY);
        redisTemplate.delete(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
        redisTemplate.delete(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY);
        redisTemplate.delete(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY);
​
        //查询数据库,获取要缓存的数据
        List<String> adminWebPerms = menuHelper.listPerms(MenuPlatformEnum.ADMIN_WEB.getPlatform());
        List<String> adminAppPerms = menuHelper.listPerms(MenuPlatformEnum.ADMIN_APP.getPlatform());
        List<String> useAppPerms = menuHelper.listPerms(MenuPlatformEnum.USE_APP.getPlatform());
        List<String> maintainAppPerms = menuHelper.listPerms(MenuPlatformEnum.MAINTAIN_APP.getPlatform());
​
        //修改数据格式,便于存储
        if (CollectionUtil.isNotEmpty(adminWebPerms) || CollectionUtil.isNotEmpty(adminAppPerms)
                || CollectionUtil.isNotEmpty(useAppPerms) || CollectionUtil.isNotEmpty(maintainAppPerms)) {
            // 初始化URL【权限->角色ID(集合)】规则
            Map<String, List<Long>> webUrlPermRolesRuleMap = new HashMap<>();
            Map<String, List<Long>> appUrlPermRolesRuleMap = new HashMap<>();
            Map<String, List<Long>> useUrlPermRolesRuleMap = new HashMap<>();
            Map<String, List<Long>> maintainUrlPermRolesRuleMap = new HashMap<>();
            for (String perm : adminWebPerms) {
                List<Long> roleIdsList = menuHelper.listRoleIdByPermission(
                        menuHelper.listPermissionByPerm(perm, MenuPlatformEnum.ADMIN_WEB.getPlatform()),
                        MenuPlatformEnum.ADMIN_WEB.getPlatform()
                );
                if (CollectionUtil.isNotEmpty(roleIdsList)) {
                    webUrlPermRolesRuleMap.put(perm, roleIdsList);
                }
            }
            for (String perm : adminAppPerms) {
                List<Long> roleIdsList = menuHelper.listRoleIdByPermission(
                        menuHelper.listPermissionByPerm(perm, MenuPlatformEnum.ADMIN_APP.getPlatform()),
                        MenuPlatformEnum.ADMIN_APP.getPlatform()
                );
                if (CollectionUtil.isNotEmpty(roleIdsList)) {
                    appUrlPermRolesRuleMap.put(perm, roleIdsList);
                }
            }
            for (String perm : useAppPerms) {
                List<Long> roleIdsList = menuHelper.listRoleIdByPermission(
                        menuHelper.listPermissionByPerm(perm, MenuPlatformEnum.USE_APP.getPlatform()),
                        MenuPlatformEnum.USE_APP.getPlatform()
                );
                if (CollectionUtil.isNotEmpty(roleIdsList)) {
                    useUrlPermRolesRuleMap.put(perm, roleIdsList);
                }
            }
            for (String perm : maintainAppPerms) {
                List<Long> roleIdsList = menuHelper.listRoleIdByPermission(
                        menuHelper.listPermissionByPerm(perm, MenuPlatformEnum.MAINTAIN_APP.getPlatform()),
                        MenuPlatformEnum.MAINTAIN_APP.getPlatform()
                );
                if (CollectionUtil.isNotEmpty(roleIdsList)) {
                    maintainUrlPermRolesRuleMap.put(perm, roleIdsList);
                }
            }
​
​
            //存入redis
            redisTemplate.opsForHash().putAll(SecurityConstant.ADMIN_WEB_URL_PERM_ROLES_KEY, webUrlPermRolesRuleMap);
            redisTemplate.opsForHash().putAll(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY, appUrlPermRolesRuleMap);
            redisTemplate.opsForHash().putAll(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY, useUrlPermRolesRuleMap);
            redisTemplate.opsForHash().putAll(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY, maintainUrlPermRolesRuleMap);
        }
        return true;
    }

设置自启动:

/**
 * 容器启动完成时加载角色权限规则至Redis缓存
 */
@Component
@AllArgsConstructor
public class InitPermissionRolesCache implements CommandLineRunner {
​
    private SysPermissionService sysPermissionService;
​
    @Override
    public void run(String... args) {
        sysPermissionService.refreshPermRolesRules();
    }
}
​

缓存数据的获取和使用在网关部分:

 @SneakyThrows
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
​
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        if (request.getMethod() == HttpMethod.OPTIONS) { // 预检请求放行
            return Mono.just(new AuthorizationDecision(true));
        }
​
        PathMatcher pathMatcher = new AntPathMatcher(); // 【声明定义】Ant路径匹配模式,“请求路径”和缓存中权限规则的“URL权限标识”匹配
        String method = request.getMethodValue();
        String path = request.getURI().getPath();
        String restfulPath = method + " " + path;
​
        // 如果token以"bearer "为前缀,到此方法里说明JWT有效即已认证
        String token = request.getHeaders().getFirst(SecurityConstant.AUTHORIZATION_KEY);
        if (StrUtil.isEmpty(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstant.JWT_PREFIX) ) {
            return Mono.just(new AuthorizationDecision(false));
        }
​
        // 解析JWT
        token = StrUtil.replaceIgnoreCase(token, SecurityConstant.JWT_PREFIX, Strings.EMPTY);
        String payload = StrUtil.toString(JWSObject.parse(token).getPayload());
        JSONObject jsonObject = JSONUtil.parseObj(payload);
        // 获取 ClientId
        String clientId = jsonObject.getStr(SecurityConstant.CLIENT_ID_KEY);
​
​
        /**
         * 鉴权开始
         *
         * 缓存取 [URL权限-角色权限集合] 规则数据
         * urlPermRolesRules = [{'key':'GET /admin/user/*','value':['role1', 'role2']},...]
         */
        // 判断是否存在权限对应数据
        if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        if (!redisTemplate.hasKey(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        if (!redisTemplate.hasKey(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        Map<String, Object> webUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
        Set<String> webUrlPermsSet = webUrlPermRolesRules.keySet();
        Map<String, Object> appUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
        Set<String> appUrlPermsSet = appUrlPermRolesRules.keySet();
        Map<String, Object> useUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY);
        Set<String> useUrlPermsSet = useUrlPermRolesRules.keySet();
        Map<String, Object> maintainUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY);
        Set<String> maintainUrlPermsSet = maintainUrlPermRolesRules.keySet();
        // 判断端
        Map<String, Object> urlPermRolesRules = null;
        if (SecurityConstant.ADMIN_WEB_CLIENT_ID.equals(clientId)) {
            urlPermRolesRules = webUrlPermRolesRules;
            // 如果访问的接口路径在web端没有,禁止访问
            if (!webUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        } else if (SecurityConstant.ADMIN_APP_CLIENT_ID.equals(clientId)) {
            urlPermRolesRules = appUrlPermRolesRules;
            // 如果访问的接口路径在app端没有,禁止访问
            if (!appUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        }else if(SecurityConstant.USE_CLIENT_ID.equals(clientId)){
            urlPermRolesRules = useUrlPermRolesRules;
            // 如果访问的接口路径在使用app端没有,禁止访问
            if (!useUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        }else if(SecurityConstant.MAINTAIN_CLIENT_ID.equals(clientId)){
            urlPermRolesRules = maintainUrlPermRolesRules;
            // 如果访问的接口路径在维保app端没有,禁止访问
            if (!maintainUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        }
​
        // 根据请求路径获取有访问权限的角色列表
        List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
        boolean requireCheck = false; // 是否需要鉴权,默认未设置拦截规则不需鉴权
​
        for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
            String perm = permRoles.getKey();
            if (pathMatcher.match(perm, restfulPath)) {
                List<String> roles = Convert.toList(String.class, permRoles.getValue());
                authorizedRoles.addAll(roles);
                if (!requireCheck) {
                    requireCheck = true;
                }
            }
        }
        // 没有设置拦截规则放行
        if (requireCheck == false) {
            return Mono.just(new AuthorizationDecision(true));
        }
​
        // 判断JWT中携带的用户角色权限是否有权限访问
        Mono<AuthorizationDecision> authorizationDecisionMono = mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(authority -> {
                    String permCode = StrUtil.removePrefix(authority, SecurityConstant.AUTHORITY_PREFIX);
                    boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(permCode);
                    return hasAuthorized;
                })
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
        return authorizationDecisionMono;
    }

4.2.3.3 缓存穿透和缓存击穿:

缓存击穿(Cache Breakdown)和缓存穿透(Cache Penetration)是与缓存相关的两个常见问题,它们可能导致缓存失效或不起作用。

缓存击穿(Cache Breakdown):

定义: 缓存击穿是指某个热点数据在缓存中过期或被删除,而此时有大量的请求同时访问这个数据,导致这些请求直接访问数据库或其他数据源,而不经过缓存。

原因: 缓存击穿通常发生在以下情况下,一个热点数据过期或被删除,此时有大量并发请求同时访问这个数据。

解决方法:

  1. 使用互斥锁(Mutex Lock): 在缓存失效的时候,通过互斥锁来保证只有一个请求能够进入数据库查询,其他请求等待。

  2. 设置热点数据永不过期: 对于热点数据,可以设置它们的缓存时间非常长,甚至永不过期,以确保在缓存失效的时候有足够的时间来更新缓存。

缓存穿透(Cache Penetration):

定义: 缓存穿透是指对于某个查询请求,缓存和数据库中都没有相应的数据,导致每次请求都需要访问数据库,浪费数据库资源。

原因: 缓存穿透通常发生在对于一些数据库中不存在的数据,但是有大量请求查询这些数据的情况下。

解决方法:

  1. 空对象缓存: 当数据库查询结果为空时,也将这个空结果缓存一段时间,避免对数据库的频繁查询。

  2. 布隆过滤器(Bloom Filter): 使用布隆过滤器判断某个查询的键是否在缓存中,如果不在,可以直接返回,避免对数据库的查询。

综合解决方法: 在解决缓存穿透和缓存击穿的问题时,可以使用综合的手段,例如使用互斥锁解决缓存击穿,同时使用空对象缓存或布隆过滤器解决缓存穿透,以更好地保护缓存和数据源。