redis存储树结构数据

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

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

先抛出结论,树结构数据在redis中的存储形式如下:redis存储树结构数据_第1张图片

1.1 前置条件

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

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

redis存储树结构数据_第2张图片
假设有如上典型的组织机构数据,前端需要按层级展示,分层级查询指定节点的子集。

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)Generic
JsonRedisSerializer 序列化器,无需指定特定类型,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服务器中存储的数据格式:

redis存储树结构数据_第3张图片
redis存储树结构数据_第4张图片

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服务器中存储的数据格式:

redis存储树结构数据_第5张图片
redis存储树结构数据_第6张图片

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;
}

3 怎么获取节点信息

有时候除了获取子节点信息外,还需要获取节点本身的信息,这时需要在redis中增加存储节点自身数据,修改结构如下:
子节点数据: key={NAME_SPACE}:{level}:{parentId},val=List[{childNode}]
节点本身数据:key={NAME_SPACE}:{level}:{nodeId},val={nodeInfo}
redis存储树结构数据_第7张图片
详细代码请参考git仓库。

源码地址

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

你可能感兴趣的:(redis)