本文主要讲解两方面内容:1.redis如何存储树结构数据。2.java操作redis时选取哪种序列化器。
假设有如上典型的组织机构数据,前端需要按层级展示,分层级查询指定节点的子集。
redisKey={NAME_SPACE}:{level}:{parentId}
NAME_SPACE:该数据所在命名空间
level:当前层级
parentId:父节点id,可为空
入参:
{
"level":3
}
返回:
[
{
"nodeId": "010101",
"nodeName": "人事部"
},
{
"nodeId": "010102",
"nodeName": "技术部"
},
{
"nodeId": "010301",
"nodeName": "人事部"
},
{
"nodeId": "010202",
"nodeName": "技术部"
},
{
"nodeId": "010201",
"nodeName": "人事部"
}
]
入参:
{
"nodeId":"0102",
"level":2
}
返回:
[
{
"nodeId": "010202",
"nodeName": "技术部"
},
{
"nodeId": "010201",
"nodeName": "人事部"
}
]
主要代码:
/**
* 查询 指定层级下某个父节点的所有子节点
*
* @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));
}
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分别有对应的实现。
1)JsonRedisSerializer 序列化器,针对指定类型的javaBean,初始化的时候需指定泛型。反序列化时,需要做类型转换。
2)GenericJsonRedisSerializer 序列化器,无需指定特定类型,redis服务器中将存储具体数据的类型信息。反序列化时,直接得到既定类型,不需要做类型转换。
举例说明:
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);
}
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);
}
}
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());
}
理由:
缺点:
{
//取出数据类型为 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;
}
@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;
}
有时候除了获取子节点信息外,还需要获取节点本身的信息,这时需要在redis中增加存储节点自身数据,修改结构如下:
子节点数据: key={NAME_SPACE}:{level}:{parentId},val=List[{childNode}]
节点本身数据:key={NAME_SPACE}:{level}:{nodeId},val={nodeInfo}
详细代码请参考git仓库。
https://gitee.com/thanksm/redis_learn/tree/master/orgtree