• 网关与auth微服务缓存打通


    🌞 Sun Frame:SpringBoot 的轻量级开发框架(个人开源项目推荐)

    Sun Frame Banner

    轻松高效的现代化开发体验

    Sun Frame 是我个人开源的一款基于 SpringBoot 的轻量级框架,专为中小型企业设计。它提供了一种快速、简单且易于扩展的开发方式。

    我们的开发文档记录了整个项目从0到1的任何细节,实属不易,请给我们一个Star!🌟
    您的支持是我们持续改进的动力。

    🌟 亮点功能

    • 组件化开发:灵活选择,简化流程。
    • 高性能:通过异步日志和 Redis 缓存提升性能。
    • 易扩展:支持多种数据库和消息队列。

    📦 spring cloud模块概览

    • Nacos 服务:高效的服务注册与发现。
    • Feign 远程调用:简化服务间通信。
    • 强大网关:路由与限流。

    常用工具

    • 日志管理:异步处理与链路追踪。
    • Redis 集成:支持分布式锁与缓存。
    • Swagger 文档:便捷的 API 入口。
    • 测试支持:SpringBoot-Test 集成。
    • EasyCode:自定义EasyCode模板引擎,一键生成CRUD。

    🔗 更多信息


    1.缓存一致性问题

    1、更新了数据库,再更新缓存

    假设数据库更新成功,缓存更新失败,在缓存失效和过期的时候,读取到的都是老数据缓存。

    2、更新缓存,更新数据库

    缓存更新成功了,数据库更新失败,是不是读取的缓存的都是错误的。

    以上两种,全都不推荐。

    3、先删除缓存,再更新数据库

    有一定的使用量。即使数据库更新失败。缓存也可以会刷。

    存在的问题是什么?

    高并发情况下!!

    比如说有两个线程,一个是 A 线程,一个是 B 线程。

    A 线程把数据删了,正在更新数据库,这个时候 B 线程来了,发现缓存没了,又查数据,又放入缓存。缓存里面存的就一直是老数据了。

    延迟双删。更新完数据库之后,再删一次。

    扩展思路
    1、消息队列补偿

    删除失败的缓存,作为消息打入 mq,mq 消费者进行监听,再次进行重试刷缓存。

    2、canal

    监听数据库的变化,做一个公共服务,专门来对接缓存刷新。优点业务解耦,业务太多冗余代码复杂度。

    2.auth微服务在用户注册时,将当前用户的角色和权限都放到redis

    1.sun-club-auth-domain
    1.pom.xml 引入依赖
            
            <dependency>
                <groupId>com.fasterxml.jackson.coregroupId>
                <artifactId>jackson-coreartifactId>
                <version>2.12.7version>
            dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.coregroupId>
                <artifactId>jackson-databindartifactId>
                <version>2.12.7version>
            dependency>
            <dependency>
                <groupId>com.google.code.gsongroupId>
                <artifactId>gsonartifactId>
                <version>2.8.6version>
            dependency>
    
    2.RedisConfig.java
    package com.sunxiansheng.auth.domain.redis;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.JsonTypeInfo;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    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.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * Description: 原生 redis 的 template 的序列化器会产生乱码问题,重写改为 jackson
     * @Author sun
     * @Create 2024/6/5 14:16
     * @Version 1.0
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            redisTemplate.setKeySerializer(redisSerializer);
            redisTemplate.setHashKeySerializer(redisSerializer);
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
            return redisTemplate;
        }
    
        private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
            Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            jsonRedisSerializer.setObjectMapper(objectMapper);
            return jsonRedisSerializer;
        }
    
    }
    
    3.RedisUtil.java
    package com.sunxiansheng.auth.domain.redis;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    /**
     * Description: RedisUtil工具类
     * @Author sun
     * @Create 2024/6/5 14:17
     * @Version 1.0
     */
    @Component
    @Slf4j
    public class RedisUtil {
    
        @Resource
        private RedisTemplate redisTemplate;
    
        private static final String CACHE_KEY_SEPARATOR = ".";
    
        /**
         * 构建缓存key
         */
        public String buildKey(String... strObjs) {
            return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));
        }
    
        /**
         * 是否存在key
         */
        public boolean exist(String key) {
            return redisTemplate.hasKey(key);
        }
    
        /**
         * 删除key
         */
        public boolean del(String key) {
            return redisTemplate.delete(key);
        }
    
        public void set(String key, String value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
            return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
        }
    
        public String get(String key) {
            return (String) redisTemplate.opsForValue().get(key);
        }
    
        public Boolean zAdd(String key, String value, Long score) {
            return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
        }
    
        public Long countZset(String key) {
            return redisTemplate.opsForZSet().size(key);
        }
    
        public Set<String> rangeZset(String key, long start, long end) {
            return redisTemplate.opsForZSet().range(key, start, end);
        }
    
        public Long removeZset(String key, Object value) {
            return redisTemplate.opsForZSet().remove(key, value);
        }
    
        public void removeZsetList(String key, Set<String> value) {
            value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
        }
    
        public Double score(String key, Object value) {
            return redisTemplate.opsForZSet().score(key, value);
        }
    
        public Set<String> rangeByScore(String key, long start, long end) {
            return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
        }
    
        public Object addScore(String key, Object obj, double score) {
            return redisTemplate.opsForZSet().incrementScore(key, obj, score);
        }
    
        public Object rank(String key, Object obj) {
            return redisTemplate.opsForZSet().rank(key, obj);
        }
    
    
    }
    
    4.AuthUserDomainServiceImpl.java register方法新增逻辑
    // 要把当前用户的角色和权限都放到redis里
    
    // 1、存储角色
    // 构建一个角色的key
    String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName());
    // 构建一个角色列表作为value
    List<AuthRole> roleList = new ArrayList<>();
    // 向角色列表中添加角色
    roleList.add(authRole);
    // 将角色列表序列化并放到redis中
    redisUtil.set(roleKey, new Gson().toJson(roleList));
    
    // 2、存储权限
    // 查询当前用户拥有的权限
    // 1.注册的时候,用户只有一个角色,先根据这个角色id去角色权限关联表中查询多条关联的记录
    AuthRolePermission authRolePermission = new AuthRolePermission();
    // 设置逻辑删除
    authRolePermission.setIsDeleted(IsDeleteFlagEnum.UN_DELETED.getCode());
    // 设置角色id
    authRolePermission.setRoleId(roleId);
    // 查询出一个角色权限关联的列表
    List<AuthRolePermission> authRolePermissionList = authRolePermissionService.queryByCondition(authRolePermission);
    // 2.根据查询出来的列表,得到所有的权限id
    List<Long> permissionIdList = authRolePermissionList.stream().map(AuthRolePermission::getPermissionId).collect(Collectors.toList());
    // 根据权限id,查询所有的权限,就是根据ids批量查询
    List<AuthPermission> permissionList = authPermissionService.queryByIds(permissionIdList);
    // 将权限列表序列化并放到redis中
    String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName());
    redisUtil.set(permissionKey, new Gson().toJson(permissionList));
    

    image-20240607164025000

    2.sun-club-auth-infra
    1.AuthRolePermissionService.java
    /**
     * 根据角色id查询角色权限关联表
     * @param authRolePermission
     * @return
     */
    List<AuthRolePermission> queryByCondition(AuthRolePermission authRolePermission);
    
    2.AuthRolePermissionServiceImpl.java
    /**
     * 根据角色id查询角色权限关联表
     * @param authRolePermission
     * @return
     */
    @Override
    public List<AuthRolePermission> queryByCondition(AuthRolePermission authRolePermission) {
        return this.authRolePermissionDao.queryAllByLimit(authRolePermission);
    }
    
    3.AuthPermissionService.java
    /**
     * 通过ids查询数据
     * @param ids
     * @return
     */
    public List<AuthPermission> queryByIds(List<Long> ids);
    
    4.AuthPermissionServiceImpl.java
    /**
     * 通过ids查询数据
     * @param ids
     * @return
     */
    public List<AuthPermission> queryByIds(List<Long> ids) {
        return authPermissionDao.queryByIds(ids);
    }
    
    5.AuthPermissionDao.java
    /**
     * 通过ID批量查询
     *
     * @param ids
     * @return
     */
    List<AuthPermission> queryByIds(@Param("ids") List<Long> ids);
    
    6.AuthPermissionDao.xml
    <select id="queryByIds" resultMap="AuthPermissionMap">
        select id, name, parent_id, type, menu_url, status, `show`, icon, permission_key, created_by, created_time,
        update_by, update_time, is_deleted
        from auth_permission
        where id in
        <foreach collection="ids" item="id" open="(" close=")" separator=",">
            #{id}
        foreach>
    select>
    
    3.测试

    image-20240607164355210

    image-20240607164348873

    3.gateway鉴权时可以获取权限/角色列表

    1.sun-club-
    1.复制两个entity到这个模块
    1.AuthPermission.java
    package com.sunxiansheng.club.gateway.entity;
    
    import lombok.Data;
    
    import java.io.Serializable;
    import java.util.Date;
    
    /**
     * (AuthPermission)实体类
     *
     * @author makejava
     * @since 2024-06-06 17:16:58
     */
    @Data
    public class AuthPermission implements Serializable {
    
        private Long id;
        /**
         * 权限名称
         */
        private String name;
        /**
         * 父id
         */
        private Long parentId;
        /**
         * 权限类型 0菜单 1操作
         */
        private Integer type;
        /**
         * 菜单路由
         */
        private String menuUrl;
        /**
         * 状态 0启用 1禁用
         */
        private Integer status;
        /**
         * 展示状态 0展示 1隐藏
         */
        private Integer show;
        /**
         * 图标
         */
        private String icon;
        /**
         * 权限唯一标识
         */
        private String permissionKey;
        /**
         * 创建人
         */
        private String createdBy;
        /**
         * 创建时间
         */
        private Date createdTime;
        /**
         * 更新人
         */
        private String updateBy;
        /**
         * 更新时间
         */
        private Date updateTime;
        /**
         * 是否被删除 0为删除 1已删除
         */
        private Integer isDeleted;
    
    }
    
    2.AuthRole.java
    package com.sunxiansheng.club.gateway.entity;
    
    import lombok.Data;
    
    import java.io.Serializable;
    import java.util.Date;
    
    /**
     * (AuthRole)实体类
     *
     * @author makejava
     * @since 2024-06-06 14:30:38
     */
    @Data
    public class AuthRole implements Serializable {
    
        private Long id;
        /**
         * 角色名称
         */
        private String roleName;
        /**
         * 角色唯一标识
         */
        private String roleKey;
        /**
         * 创建人
         */
        private String createdBy;
        /**
         * 创建时间
         */
        private Date createdTime;
        /**
         * 更新人
         */
        private String updateBy;
        /**
         * 更新时间
         */
        private Date updateTime;
        /**
         * 是否被删除 0未删除 1已删除
         */
        private Integer isDeleted;
    
    
    }
    
    2.StpInterfaceImpl.java 根据loginId和前缀获取权限/角色列表
    /**
     * 根据loginId和前缀获取权限/角色列表
     * @param loginId
     * @param prefix
     * @return
     */
    private List<String> getAuth(String loginId, String prefix) {
        // 得到该用户在redis中存储的key
        String authKey = redisUtil.buildKey(prefix, loginId.toString());
        // 从redis中获取列表
        String authValue = redisUtil.get(authKey);
        // 判空
        if (StringUtils.isBlank(authValue)) {
            return Collections.emptyList();
        }
        List<String> authList = new LinkedList<>();
        // 根据前缀来决定将内容反序列化为什么形式
        if (authRolePrefix.equals(prefix)) {
            // 如果是角色列表的前缀,就反序列化为角色类型的
            List<AuthRole> authRoleList = new Gson().fromJson(authValue, new TypeToken<List<AuthRole>>() {
            }.getType());
            // 得到roleKey的列表,放到authList中
            authList = authRoleList.stream().map(AuthRole::getRoleKey).collect(Collectors.toList());
        } else if (authPermissionPrefix.equals(prefix)) {
            // 如果是权限列表,就反序列化为权限类型的
            List<AuthPermission> authPermissionList = new Gson().fromJson(authValue, new TypeToken<List<AuthPermission>>() {
            }.getType());
            // 得到permissionKey,放到authList中
            authList = authPermissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList());
        }
        return authList;
    }
    
    3.sun-club-auth-application-controller
    在UserController.java可以设置用户登录时的token对应的loginId,这里设置成鸡翅

    image-20240607171955303

    3.测试
    1.首先登录,生成token和loginId(这里写死为鸡翅)

    image-20240607172214503

    2.然后携带token进行登录,后端就可以找到对应的loginId,在验证登录成功之后会进行鉴权
    1.在gateway的SaTokenConfigure.java可以配置鉴权的类型

    image-20240607172332923

    2.下面是分别两种方式,从redis中取出的角色列表和权限列表

    image-20240607171208574

    image-20240607171451314

  • 相关阅读:
    程序员该知道大型网站架构的发展历程吗?如何有效地增加服务器?
    Tortoise 没有显示绿色图标
    Oracle数据库SQL*Plus命令行执行SQL语句时,中文乱码报错解决方法
    YOLOv8 加持 MobileNetv3,目标检测新篇章
    Python获取 小黑子 弹幕数据+制作词云分析.........
    AMRT3D数字孪生引擎详解
    2024能源动力、机械自动化与航天航空技术国际学术会议(ICEPMAT2024)
    放弃朋友圈转战小程序,20天转化率达到70%!
    实战型开发1/3--结果&业务导向
    电子词典项目
  • 原文地址:https://blog.csdn.net/m0_64637029/article/details/140950426