RBAC和Redis咋整到一块儿的?MySQL和Redis的旁路缓存在代码层面怎么实现的?


RBAC(基于角色的访问控制)和Redis的集成是个效率活儿,用Redis当"速查手册",把权限相关的"账本"记下来,省得每次都翻数据库那本"大账本",既快又省力。今天咱拿这段代码开刀,聊聊这俩是怎么搭伙的,还得加点料,把@CacheEvict这个注解掰开了揉碎了讲清楚:它咋跟Redis关联的?它干活时看啥?它从哪儿来的,参数又是干啥的?细节拉满,不整那些云里雾里的术语。


RBAC和Redis是啥,干啥用的?

RBAC是个管权限的办法,核心思路是通过"角色"来分活儿。比如老板说:"管理员能看所有订单,普通用户只能看自己的。"不用给每个员工单独写权限表,给"管理员"和"用户"这两个角色分配权限,用户挂到角色下就行。省事,管理方便。

Redis是个跑得飞快的内存数据库,专门存经常要查的东西。RBAC里权限校验是个高频活儿,每次请求都得问"你有权吗",老去数据库问,累不说,速度还慢。Redis一出场,相当于把权限表抄到小本子上,查起来嗖嗖的。


这代码咋把RBAC和Redis绑一块儿了?

从代码看,系统用Redis缓存RBAC的权限数据,减少数据库压力。具体咋干的?咱分三块儿聊:缓存定义缓存操作 (重点讲@CacheEvict)、权限校验

1. 缓存定义:给Redis划好地盘

先看RbacCacheNames.java,这块儿是给Redis里的数据起名的地方。用键值对方式,键名得有讲究,不能乱七八糟,不然找不着。代码里定了几个关键的"地盘":

ini 复制代码
public interface RbacCacheNames {
    String RBAC_PREFIX = "TestModule_rbac:"; // 前缀,像个门牌号
    String PERMISSIONS_KEY = RBAC_PREFIX + RBAC_PREFIX + "permission:permissions:"; // 所有权限列表
    String USER_PERMISSIONS_KEY = RBAC_PREFIX + "permission:user_permissions:"; // 用户的权限
    String URI_PERMISSION_KEY = RBAC_PREFIX + "permission:uri_permissions:"; // URI对应的权限
    String MENU_LIST_KEY = RBAC_PREFIX + "menu:list:"; // 菜单列表
    String MENU_ID_LIST_KEY = RBAC_PREFIX + "menu:id_list:"; // 菜单ID列表
}
  • RBAC_PREFIX:前缀"TestModule_rbac:",像小区名字,告诉Redis这是咱RBAC的地盘,别跟别人混了。
  • 各种KEY :后面接具体名字,比如USER_PERMISSIONS_KEY实际用时可能是"TestModule_rbac:permission:user_permissions:用户ID",存的是某个用户的权限列表。

这设计就像给Redis分了几个抽屉,权限、菜单、URI映射各放各的,找起来方便。

2. 缓存操作:角色变了咋更新?(@CacheEvict全解析)

再看UserRoleFeignController.java,这是管用户和角色关系的地方。角色变了(比如加个角色、改个角色),Redis里的缓存得跟着动,不然权限就乱套了。这块儿用到了@CacheEvict注解,咱得好好聊聊。

less 复制代码
@RestController
public class UserRoleFeignController implements UserRoleFeignClient {
​
    @Override
    @Transactional(rollbackFor = Exception.class)
    @CacheEvict(cacheNames = CacheNames.MENU_ID_LIST_KEY, key = "#userRoleDTO.userId")
    public ServerResponseEntity<Void> saveByUserIdAndSysType(UserRoleDTO userRoleDTO) {
        userRoleMapper.insertUserAndUserRole(userRoleDTO.getUserId(), userRoleDTO.getRoleIds());
        return ServerResponseEntity.success();
    }
​
    @Override
    @Transactional(rollbackFor = Exception.class)
    @CacheEvict(cacheNames = CacheNames.MENU_ID_LIST_KEY, key = "#userRoleDTO.userId")
    public ServerResponseEntity<Void> updateByUserIdAndSysType(UserRoleDTO userRoleDTO) {
        userRoleMapper.deleteByUserId(userRoleDTO.getUserId());
        userRoleMapper.insertUserAndUserRole(userRoleDTO.getUserId(), userRoleDTO.getRoleIds());
        return ServerResponseEntity.success();
    }
}

@CacheEvict是啥?咋跟Redis关联的?

@CacheEvict是个"清道夫",告诉系统:这个方法跑完后,把Redis里指定的缓存踢掉。咋跟Redis关联的呢?它其实是Spring框架的缓存抽象(Spring Cache)的一部分。Spring Cache不直接操作Redis,而是通过一个缓存管理器(比如RedisCacheManager)来干活。你得在项目里配置好Redis,让Spring知道用Redis存缓存。@CacheEvict只是告诉Spring:"这里要清缓存",具体清Redis里的哪条记录,靠注解的参数指路。

在这代码里,cacheNames指定了要清的缓存抽屉(MENU_ID_LIST_KEY),key指定了抽屉里的具体"卷宗"(用户ID)。执行时,Spring会把MENU_ID_LIST_KEY和用户ID拼起来,比如"TestModule_rbac:menu:id_list:用户ID",然后通过缓存管理器告诉Redis删掉这条记录。

@CacheEvict需要读方法里的啥操作?

它其实不关心方法里干了啥,也不读具体操作(比如insertUserAndUserRole)。它只看方法跑完后要不要清缓存。清不清、清理啥,完全靠注解的配置:

  • 触发时机 :默认是方法成功执行完后清缓存。如果方法抛异常(比如数据库挂了),搭配@Transactional,事务回滚后缓存也不会清。
  • 参数提取key="#userRoleDTO.userId"用了个SpEL表达式(Spring Expression Language),从方法参数userRoleDTO里掏出userId。这表达式很灵活,能动态生成缓存键。

换句话说,@CacheEvict只管"清",不管"怎么变",逻辑全在注解参数里定了。

@CacheEvict从哪儿来?参数有啥用?

@CacheEvict来自Spring框架的org.springframework.cache.annotation包,是Spring Cache的核心注解之一。要用它,得在项目里加Spring Cache支持(比如加个@EnableCaching),然后配置Redis作为缓存后端。

参数有这么几个,咱挨个儿说:

  • cacheNamesvalue (必填):指定缓存的"抽屉名",可以是数组,多个抽屉一起清。这里是MENU_ID_LIST_KEY,对应"TestModule_rbac:menu:id_list:"。
  • key (可选):具体要清的缓存键,默认是方法签名生成的哈希值。这里用#userRoleDTO.userId,动态取用户ID,拼成完整键。
  • allEntries (默认false):设成true就清整个cacheNames里的所有缓存,不管key。这里没用,默认只清指定键。
  • beforeInvocation(默认false):设成true就在方法执行前清缓存,而不是执行后。这里默认false,方法成功跑完再清。
  • condition (可选):加个条件,比如condition="#userRoleDTO.userId > 0",只有用户ID大于0才清。没用默认都清。

在这代码里,@CacheEvict(cacheNames = CacheNames.MENU_ID_LIST_KEY, key = "#userRoleDTO.userId")意思是:方法跑完后,清掉"TestModule_rbac:menu:id_list:用户ID"这条缓存。简单直接。

为啥只清菜单ID?

角色变了,菜单ID缓存(MENU_ID_LIST_KEY)清了,但用户权限缓存(USER_PERMISSIONS_KEY)没动。可能是菜单跟角色关系更直接,角色一变,菜单得重新算。但权限缓存没清,得看系统咋设计的,可能有其他地方管,或者故意让它过会儿自己过期(TTL)。

3. 权限校验:Redis咋帮忙?

最后看PermissionFeignController.java,这是权限校验的"门卫",每次请求来了,得问"你有权进吗"。

less 复制代码
@RestController
public class PermissionFeignController implements PermissionFeignClient {
​
    @Autowired
    private MenuPermissionService menuPermissionService;
​
    @Override
    public ServerResponseEntity<Boolean> checkPermission(@RequestParam("userId") Long userId, 
            @RequestParam("sysType") Integer sysType, @RequestParam("uri") String uri, 
            @RequestParam("isAdmin") Integer isAdmin, @RequestParam("method") Integer method) {
        List<String> userPermissions = menuPermissionService.listUserPermissions(userId, sysType, BooleanUtil.isTrue(isAdmin));
        List<UriPermissionBO> uriPermissions = menuPermissionService.listUriPermissionInfo(sysType);
        
        AntPathMatcher pathMatcher = new AntPathMatcher();
        for (UriPermissionBO uriPermission : uriPermissions) {
            if (pathMatcher.match(uriPermission.getUri(), uri) && 
                Objects.equals(uriPermission.getMethod(), method)) {
                if (userPermissions.contains(uriPermission.getPermission())) {
                    return ServerResponseEntity.success(Boolean.TRUE);
                }
                return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
            }
        }
        return ServerResponseEntity.success(Boolean.TRUE);
    }
}
  • 从Redis拿数据listUserPermissionslistUriPermissionInfo大概率从Redis里捞用户权限和URI权限映射。Redis快,查起来省时间。
  • 匹配URI :用AntPathMatcher检查请求的URI(比如"/api/order")能不能跟缓存里的URI(比如"/api/**")对上,还得看请求方法(GET、POST)对不对。
  • 比对权限:URI对上了,再看用户权限里有没有这条URI需要的权限,有就放行,没就"没门儿"。

这流程全靠Redis缓存撑着,没缓存就得跑数据库查,慢得像乌龟爬。


这设计牛在哪儿?有啥坑?

牛的地方

  1. 快得飞起:权限校验靠Redis,内存操作比数据库快几十倍,高并发下不卡壳。
  2. 省资源:数据库少干活,压力小,能撑更多用户。
  3. 灵活匹配AntPathMatcher支持通配符,URI权限写个"/**"就能管一片儿,方便。
  4. 缓存管理聪明@CacheEvict动态清缓存,搭配SpEL表达式,精准打击。

可能的坑

  1. 用户权限缓存没清:角色变了,菜单ID缓存清了,但用户权限缓存没动。比如用户从"管理员"降到"普通用户",权限缓存没变,他还能干管理员的事儿。

    • 咋整? 清权限缓存,或者设短TTL让它自己刷新。
  2. 缓存一致性:分布式系统里,多台服务器用Redis,数据没同步好就乱。得用消息队列或分布式锁搞定。

  3. Redis内存压力:用户多、权限杂,缓存占内存多,得盯着,别爆了。

  4. @CacheEvict依赖配置:没配好Redis或Spring Cache,注解就是个摆设,得检查环境。


接地气的总结

RBAC和Redis搭伙,核心是用Redis当"速查手册",把权限、菜单、URI映射记下来,省得老翻数据库。角色变了,@CacheEvict跳出来清菜单缓存,靠Spring Cache和Redis管理器干活,不读方法逻辑,只看参数拼键。校验时Redis和AntPathMatcher配合,速度快、效率高。不过得注意权限缓存更新和内存管理,不然快是快了,准头没了。这套路在电商、后台系统里挺常见,想用的话,把坑填平,Redis调好,缓存策略想全,就能又快又稳!

相关推荐
小钊(求职中)1 小时前
最新Git入门到精通完整教程
java·git·后端·spring·面试
程序视点1 小时前
JVM虚拟机监控及性能调优实战
java·jvm·后端
Asthenia04122 小时前
在Debian中安装VSCode,解决dpkg的Path问题,并配置GCC以运行Hello World.
后端
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS医院药品管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Watink Cpper2 小时前
[MySQL初阶]MySQL(1)MySQL的理解、库的操作、表的操作
linux·运维·服务器·数据库·c++·后端·mysql
m0_748256142 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
WeiLai11122 小时前
面试基础---Spring Cloud微服务负载均衡架构
spring boot·分布式·后端·spring·spring cloud·面试·架构
网络风云3 小时前
Django 5实用指南(十二)异步处理与Celery集成
后端·python·django
土豆炒马铃薯。3 小时前
【Java 基础(人话版)】Java 虚拟机(JVM)
java·开发语言·jvm·后端·java基础·虚拟机
caihuayuan53 小时前
Golang的图形用户界面设计
java·大数据·spring boot·后端·课程设计