文章目录
- 🌞 Sun Frame:SpringBoot 的轻量级开发框架(个人开源项目推荐)
- 🌟 亮点功能
- 📦 spring cloud模块概览
- 常用工具
- 🔗 更多信息
- 1.缓存一致性问题
- 1、更新了数据库,再更新缓存
- 2、更新缓存,更新数据库
- 3、先删除缓存,再更新数据库
- 扩展思路
- 1、消息队列补偿
- 2、canal
- 2.auth微服务在用户注册时,将当前用户的角色和权限都放到redis里
- 1.sun-club-auth-domain
- 1.pom.xml 引入依赖
- 2.RedisConfig.java
- 3.RedisUtil.java
- 4.AuthUserDomainServiceImpl.java register方法新增逻辑
- 2.sun-club-auth-infra
- 1.AuthRolePermissionService.java
- 2.AuthRolePermissionServiceImpl.java
- 3.AuthPermissionService.java
- 4.AuthPermissionServiceImpl.java
- 5.AuthPermissionDao.java
- 6.AuthPermissionDao.xml
- 3.测试
- 3.gateway鉴权时可以获取权限/角色列表
- 1.sun-club-
- 1.复制两个entity到这个模块
- 1.AuthPermission.java
- 2.AuthRole.java
- 2.StpInterfaceImpl.java 根据loginId和前缀获取权限/角色列表
- 3.sun-club-auth-application-controller
- 在UserController.java可以设置用户登录时的token对应的loginId,这里设置成鸡翅
- 3.测试
- 1.首先登录,生成token和loginId(这里写死为鸡翅)
- 2.然后携带token进行登录,后端就可以找到对应的loginId,在验证登录成功之后会进行鉴权
- 1.在gateway的SaTokenConfigure.java可以配置鉴权的类型
- 2.下面是分别两种方式,从redis中取出的角色列表和权限列表
🌞 Sun Frame:SpringBoot 的轻量级开发框架(个人开源项目推荐)
轻松高效的现代化开发体验
Sun Frame 是我个人开源的一款基于 SpringBoot 的轻量级框架,专为中小型企业设计。它提供了一种快速、简单且易于扩展的开发方式。
我们的开发文档记录了整个项目从0到1的任何细节,实属不易,请给我们一个Star!🌟
您的支持是我们持续改进的动力。
您的支持是我们持续改进的动力。
🌟 亮点功能
- 组件化开发:灵活选择,简化流程。
- 高性能:通过异步日志和 Redis 缓存提升性能。
- 易扩展:支持多种数据库和消息队列。
📦 spring cloud模块概览
- Nacos 服务:高效的服务注册与发现。
- Feign 远程调用:简化服务间通信。
- 强大网关:路由与限流。
常用工具
- 日志管理:异步处理与链路追踪。
- Redis 集成:支持分布式锁与缓存。
- Swagger 文档:便捷的 API 入口。
- 测试支持:SpringBoot-Test 集成。
- EasyCode:自定义EasyCode模板引擎,一键生成CRUD。
🔗 更多信息
- 开源地址:Gitee Sun Frame
- 详细文档:语雀文档
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.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.7</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.7</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</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));
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.测试
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;
}