您的位置:首页 > 数据库 > Redis

redis存储树结构数据

2020-02-12 10:54 381 查看

本文主要讲解两方面内容:1.redis如何存储树结构数据。2.java操作redis时选取哪种序列化器。

1. redis如何存储树结构数据

先抛出结论,树结构数据在redis中的存储形式如下:

1.1 前置条件

  1. spring-boot-starter-data-redis(2.1.8)
  2. fastjson(1.2.61)
  3. redis可视化工具 Redis Desktop Manager

1.2 树结构数据在redis中的存储形式(数据结构)


假设有如上典型的组织机构数据,前端需要按层级展示,分层级查询指定节点的子集。

1.2.1 redis中Key的设计

redisKey={NAME_SPACE}:{level}:{parentId}
NAME_SPACE:该数据所在命名空间
level:当前层级
parentId:父节点id,可为空

1.2.2 查询

  1. 按层级查询:输入指定层级,返回该层级下的所有节点列表

入参:

{
"level":3
}

返回:

[
{
"nodeId": "010101",
"nodeName": "人事部"
},
{
"nodeId": "010102",
"nodeName": "技术部"
},
{
"nodeId": "010301",
"nodeName": "人事部"
},
{
"nodeId": "010202",
"nodeName": "技术部"
},
{
"nodeId": "010201",
"nodeName": "人事部"
}
]
  1. 查询指定节点的所有子节点:输入该节点id和当前层级

入参:

{
"nodeId":"0102",
"level":2
}

返回:

[
{
"nodeId": "010202",
"nodeName": "技术部"
},
{
"nodeId": "010201",
"nodeName": "人事部"
}
]

1.2.3 后台实现

主要代码:

/**
* 查询 指定层级下某个父节点的所有子节点
*
* @param level    层级
* @param parentId 父节点id,为空时查询该层级下所有节点
* @return 指定层级下某个父节点的所有子节点
*/
@SuppressWarnings("unchecked")
public List<OrgBean> getSubTree(int level, @Nullable String parentId) {
List<OrgBean> beanList;
if (StringUtils.isBlank(parentId) && level != 1) {
//parentId 为空,模糊匹配查询
beanList = patternSearch(level);
} else {
//parentId 不为空,精确匹配查询
beanList = exactlySearch(level, parentId);
}
return beanList;
}

@SuppressWarnings("unchecked")
private List<OrgBean> patternSearch(int level) {
//SCAN 0 MATCH {NAME_SPACE}:{level}:* COUNT 10000
String pattern = getRedisKey("*", level);
logger.info("redisKey:{}", pattern);
List<String> keys = (List<String>) redisTemplate.execute(connection -> {
List<String> keyStrList = new ArrayList<>();
RedisKeyCommands keysCmd = connection.keyCommands();
//采用 SCAN 命令,迭代遍历所有key
Cursor<byte[]> cursor = keysCmd.scan(ScanOptions.scanOptions().match(pattern).count(10000L).build());
while (cursor.hasNext()) {
keyStrList.add(new String(cursor.next(), StandardCharsets.UTF_8));
}
return keyStrList;
}, true);
if (isNotEmpty(keys)) {
return keys.stream().flatMap(key -> {
List list = listOperations.range(key, 0, -1);
return deserializeJsonList(list, OrgBean.class).stream();
}).collect(toList());
} else {
return Collections.emptyList();
}
}

@SuppressWarnings("unchecked")
private List<OrgBean> exactlySearch(int level, String parentId) {
List<OrgBean> beanList = new ArrayList<>();
String redisKey = getRedisKey(parentId, level);
logger.info("redisKey:{}", redisKey);
Boolean hasKey = redisTemplate.hasKey(redisKey);
if (Boolean.valueOf(true).equals(hasKey)) {
List jsonList = listOperations.range(redisKey, 0, -1);
beanList = deserializeJsonList(jsonList, OrgBean.class);
}
return beanList;
}

private <T> List<T> deserializeJsonList(List jsonList, Class<T> clazz) {
ArrayList<T> beanList = new ArrayList<>();
if (isNotEmpty(jsonList)) {
//反序列化为指定类型的bean
for (Object o : jsonList) {
if (nonNull(o)) {
T bean = JSON.toJavaObject((JSONObject) o, clazz);
beanList.add(bean);
}
}
}
return beanList;
}
/**
* redisKey: {NAME_SPACE}:{level}:{parentId}
*/
private String getRedisKey(String parentId, int level) {
return concat(DELIMIT, NAME_SPACE, level, parentId);
}
/**
* 连接字符串通用方法
*
* @param delimit 分隔符
* @param element 字符串元素
* @return 按指定分隔符连接的字符串
*/
private String concat(String delimit, Object... element) {
if (isNull(element) || element.length == 0) {
return null;
}
return Stream.of(element).filter(Objects::nonNull)
.map(String::valueOf).filter(StringUtils::isNoneBlank).collect(joining(delimit));
}

2 java操作redis,设置序列化器

org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
org.springframework.data.redis.serializer.StringRedisSerializer
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
com.alibaba.fastjson.support.spring.FastJsonRedisSerializer
com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer
spring-boot-starter-data-redis 默认采用 JdkSerializationRedisSerializer,这样序列化后的数据无法通过工具直观地展示,通常redis的Key采用StringRedisSerializer,value采用JSON格式化后存储。
json序列化器分别有两种:JsonRedisSerializer和GenericJsonRedisSerializer,Jackson和FastJson分别有对应的实现。

2.1 两种序列化器的区别

1)JsonRedisSerializer 序列化器,针对指定类型的javaBean,初始化的时候需指定泛型。反序列化时,需要做类型转换。
2)GenericJsonRedisSerializer 序列化器,无需指定特定类型,redis服务器中将存储具体数据的类型信息。反序列化时,直接得到既定类型,不需要做类型转换。
举例说明:

2.1.1 *JsonRedisSerializer 序列化器,构造方法:

com.alibaba.fastjson.support.spring.FastJsonRedisSerializer
public FastJsonRedisSerializer(Class<T> type) {
this.type = type;
}
org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}.
*
* @param type
*/
public Jackson2JsonRedisSerializer(Class<T> type) {
this.javaType = getJavaType(type);
}

2.1.2 *JsonRedisSerializer 序列化器,redis服务器中存储的数据格式:


2.1.3 Generic*JsonRedisSerializer 序列化器,构造方法:

com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer
public class GenericFastJsonRedisSerializer implements RedisSerializer<Object> {}
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {
/**
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing.
*/
public GenericJackson2JsonRedisSerializer() {
this((String) null);
}
}

2.1.4 Generic*JsonRedisSerializer 序列化器,redis服务器中存储的数据格式:


2.1.5 *JsonRedisSerializer 序列化器,反序列化后的类型

public void testDeserialize() {
Object val1 = fastJsonTemp.opsForValue().get("emp1_fastJson");
Object val2 = genericFastJsonTemp.opsForValue().get("emp1_genericFastJson");
Object val3 = jacksonTemp.opsForValue().get("emp1_jackson");
Object val4 = genericJacksonTemp.opsForValue().get("emp1_genericJackson");
//class com.alibaba.fastjson.JSONObject
log.info(val1.getClass().toString());
//class com.fcg.redis.orgtree.Employee
log.info(val2.getClass().toString());
//class java.util.LinkedHashMap
log.info(val3.getClass().toString());
//class com.fcg.redis.orgtree.Employee
log.info(val4.getClass().toString());
}

2.2 选用FastJsonRedisSerializer

理由:

  • 项目中统一采用FastJson,作为JSON序列化工具,因其效率较高。
  • Redis中仅需存储数据,不宜限定该数据的具体JavaBean类型,利于不同项目间共享缓存数据。

缺点:

  • 由于原始数据缺少类型信息,取出数据后如果需要按照既定类型操作(setters/getters),要多一步转换操作,如:
{
//取出数据类型为 JSONObject
List jsonList = listOperations.range(redisKey, 0, -1);
//反序列化为指定类型
beanList = deserializeJsonList(jsonList, OrgBean.class);
}
private <T> List<T> deserializeJsonList(List jsonList, Class<T> clazz) {
ArrayList<T> beanList = new ArrayList<>();
if (isNotEmpty(jsonList)) {
//反序列化为指定类型的bean
for (Object o : jsonList) {
if (nonNull(o)) {
T bean = JSON.toJavaObject((JSONObject) o, clazz);
beanList.add(bean);
}
}
}
return beanList;
}

2.3 RedisTemplate 配置

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
//泛型用Object,可序列化所有类型
FastJsonRedisSerializer<Object> jsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(jsonRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(stringRedisSerializer);
template.setDefaultSerializer(jsonRedisSerializer);
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template;
}

在使用RedisTemplate时,不要指定泛型,可序列化所有类型,否则在使用ListOperations 时,会把整个List作为一个元素:

public class OrgService {
@Resource(name = "redisTemplate")
private RedisTemplate redisTemplate;
@Resource(name = "redisTemplate")
private ListOperations listOperations;
}

源码地址

https://gitee.com/thanksm/redis_learn/tree/master/orgtree

  • 点赞 1
  • 收藏
  • 分享
  • 文章举报
thanksm1 发布了5 篇原创文章 · 获赞 1 · 访问量 682 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: