Redis实战(11)-哈希Hash典型应用场景实战之系统数据字典实时触发缓存存储

作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。



摘要:前文我们已经介绍并实战了Redis的数据类型哈希Hash的相关命令行及其对应的Java单元测试的实战代码,本文我们将以实际项目中典型的应用场景“系统数据字典模块的实时触发存储”为案例,学以致用,一起践行哈希Hash在实际项目下的实战应用,感受感受其在实际业务场景下的作用!

内容:在前文我们已经简单介绍了Redis的数据类型~哈希Hash的底层存储结构,很显然,哈希Hash跟其他的数据结构还是有诸多不同之处的。其他的据结构几乎都是:Key-Value的存储,而Hash则是:Key – [Field-Value] 的存储,也就是说其他数据结构的Value一般是确切的值,而Hash的Value是一系列的键值对,通常我们是这样子称呼Hash的存储的:大Key为实际的Key,小Key为Field,而具体的取值为Field对应的值。如下图所示:


说实在的,它的作用还是很强大的,特别是在存储“同种对象类型”的数据列表时哈希Hash更能体现其优势,除此之外,其最大的、直观上的作用便是“减少了缓存Key的数量”,而这主要还得得益于哈希Hash底层存储数据时的存储方式,如上图所示!

接下来,我们便以实际项目开发中典型、常见的应用场景“系统数据字典实时触发缓存存储”为案例一起来践行哈希Hash的作用。

对于“数据字典模块”,相信很多小伙伴都有所听闻过,毫不夸张地讲,几乎每个项目都会有一个独立的功能模块,用于管理项目中各个业务模块经常出现的“通用化、共性的、需要配置起来的东西”,这些通用化的东西我们可以称之为“数据字典”,对于这些东西我们一般会单独开辟一个独立的功能模块,如“数据字典模块”进行单独维护管理!

对于上面这个解释,可能有些小伙伴有点懵,下面我们举个栗子吧,比如经常可以见到的数据字典:“性别Sex~其取值可以有:男=1;女=0;未知=2”;比如“支付状态PayStatus~其取值可以有:1=未支付;2=已支付;3=已取消支付;4=已退款…”;再比如“订单审核状态ReviewStatus~1=已保存/未审核;2=已审核;3=审核成功;4=审核失败…”等等可以将其配置在“数据字典功能模块”中将其维护起来,如下图所示:


看到上面这张图,有些机灵的小伙伴可能会立即联想到哈希Hash的底层存储结构(本文开篇的那张图),会发现惊人的相似,就拿“性别Sex”这一数据字典为例,它的取值为“Female-女性”、“Male-男性”,这不就相当于哈希Hash的底层存储结构吗~Key=Sex,Field-Value对包含两队,分别是:Field=Female ~ Value=女性;Field=Male ~ Value=男性。

理解了这种数据关联以及存储之后,在后文的实战中你就会发现代码很容易理解,并且在实战过后你或许会发出惊叹:“原来如此!”

除此之外,还有一种现象需要跟小伙伴分享分享,那就是“数据字典功能模块”一旦配置好了某个“数据字典”之后,我们基本上会在好几个月内都不会去重新修改它了,即有点“一劳永逸”的感觉!基于这个前提,我们可以将前端发起的请求实时访问数据库DB的“数据字典” 优化为 基于缓存Redis的哈希Hash进行存储与访问,并且这种存储是“实时”的,那我们就开始吧!

(1)同样的道理,工欲善其事,必先利其器,我们首先需要建立一个数据库表sys_config用于存储管理员添加的数据字典,其DDL如下所示:

CREATE TABLE `sys_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '字典类型',
`name` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '字典名称',
`code` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '选项编码',
`value` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '选项取值',
`order_by` int(11) DEFAULT '1' COMMENT '排序',
`is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_type_code` (`type`,`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='字典配置表';

采用Mybatis的逆向工程生成该数据库表的实体类Entity、Mapper操作接口及其对应的用于操作动态SQL的Mapper.xml,在这里我们只贴出SysConfigMapper接口中一个相当重要的方法吧:  

//查询目前数据字典表中所有可用的-已激活的数据字典列表
List<SysConfig> selectActiveConfigs();

其对应的动态SQL实现如下所示:  

  <select id="selectActiveConfigs" resultType="com.boot.debug.redis.model.entity.SysConfig">
SELECT <include refid="Base_Column_List"/>
FROM sys_config
WHERE is_active = 1
ORDER BY type, order_by ASC
</select>

(2)紧接着,我们建立一个HashController,用于“新增数据字典”、“获取缓存中所有的数据字典”以及“获取特定编码的数据字典取值列表”,其完整的源代码如下所示:  

/**数据类型Hash散列-减少key存储、类似于map-可以通过键取得其 “值” (可以对象列表...)
* @Author:debug (SteadyJack) **/
@RestController
@RequestMapping("hash")
public class HashController extends AbstractController {

@Autowired
private HashService hashService;
//新增数据字典
@RequestMapping(value = "put",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse put(@RequestBody @Validated SysConfig config, BindingResult result){
String checkRes= ValidatorUtil.checkResult(result);
if (StrUtil.isNotBlank(checkRes)){
return new BaseResponse(StatusCode.Fail.getCode(),checkRes);
}
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
hashService.addSysConfig(config);

}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
//获取缓存中所有的数据字典
@RequestMapping(value = "get",method = RequestMethod.GET)
public BaseResponse get(){
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
response.setData(hashService.getAll());

}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
//获取缓存中某个特定编码下数据字典的取值列表
@RequestMapping(value = "get/type",method = RequestMethod.GET)
public BaseResponse getType(@RequestParam String type){
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
response.setData(hashService.getByType(type));

}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}

(3)其中,hashService下那几个方法的实现逻辑即为真正要做的事情,其完整源代码如下所示:  

/**hash数据类型-service
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/10/31 21:07
**/
@Service
public class HashService {
private static final Logger log= LoggerFactory.getLogger(HashService.class);

@Autowired
private SysConfigMapper sysConfigMapper;

@Autowired
private HashRedisService hashRedisService;

//TODO:添加数据字典及其对应的选项(field-value)
@Transactional(rollbackFor = Exception.class)
public Integer addSysConfig(SysConfig config) throws Exception{
int res=sysConfigMapper.insertSelective(config);
if (res>0){
//TODO:实时触发数据字典的hash存储
hashRedisService.cacheConfigMap();
}
return config.getId();
}

//TODO:取出缓存中所有的数据字典列表
public Map<String,List<SysConfig>> getAll() throws Exception{
return hashRedisService.getAllCacheConfig();
}

//TODO:取出缓存中特定的数据字典列表
public List<SysConfig> getByType(final String type) throws Exception{
return hashRedisService.getCacheConfigByType(type);
}
}

(4)而hashService中实现数据字典的实时存取又是交给了HashRedisService相应的方法逻辑进行处理,其对应的完整源代码如下所示:  

/**hash缓存服务 @Author:debug (SteadyJack)  weixin-> debug0868 qq-> 1948831260**/
@Service
public class HashRedisService {
private static final Logger log= LoggerFactory.getLogger(HashRedisService.class);
@Autowired
private SysConfigMapper sysConfigMapper;

@Autowired
private RedisTemplate redisTemplate;
//TODO:实时获取所有有效的数据字典列表-转化为map-存入hash缓存中
@Async
public void cacheConfigMap(){
try {
List<SysConfig> configs=sysConfigMapper.selectActiveConfigs();
if (configs!=null && !configs.isEmpty()){
Map<String,List<SysConfig>> dataMap= Maps.newHashMap();

//TODO:所有的数据字典列表遍历 -> 转化为 hash存储的map
configs.forEach(config -> {
List<SysConfig> list=dataMap.get(config.getType());
if (list==null || list.isEmpty()){
list= Lists.newLinkedList();
}
list.add(config);
dataMap.put(config.getType(),list);
});
//TODO:存储到缓存hash中
HashOperations<String,String,List<SysConfig>> hashOperations=redisTemplate.opsForHash();
hashOperations.putAll(Constant.RedisHashKeyConfig,dataMap);
}
}catch (Exception e){
log.error("实时获取所有有效的数据字典列表-转化为map-存入hash缓存中-发生异常:",e.fillInStackTrace());
}
}

//TODO:从缓存hash中获取所有的数据字典配置map
public Map<String,List<SysConfig>> getAllCacheConfig(){
Map<String,List<SysConfig>> map=Maps.newHashMap();
try {
HashOperations<String,String,List<SysConfig>> hashOperations=redisTemplate.opsForHash();
map=hashOperations.entries(Constant.RedisHashKeyConfig);
}catch (Exception e){
log.error("从缓存hash中获取所有的数据字典配置map-发生异常:",e.fillInStackTrace());
}
return map;
}

//TODO:从缓存hash中获取特定的数据字典列表
public List<SysConfig> getCacheConfigByType(final String type){
List<SysConfig> list=Lists.newLinkedList();
try {
HashOperations<String,String,List<SysConfig>> hashOperations=redisTemplate.opsForHash();
list=hashOperations.get(Constant.RedisHashKeyConfig,type);
}catch (Exception e){
log.error("从缓存hash中获取特定的数据字典列表-发生异常:",e.fillInStackTrace());
}
return list;
}
}

至此,我们已经完成了哈希Hash典型应用场景“系统数据字典的实时存取”的代码实战了,相应的代码的含义我们也在代码中做了相应的注释!如果有疑问的地方,各位小伙伴可以加Debug的联系方式进行交流(代码中就有我的交流联系方式哦!),下面我们基于Postman进行一波测试吧!

A.首先是往数据库中已有的某个数据字典添加某些具体的取值列表(Field-Value),如下几张图所示:




B.最后是往数据库中添加一个全新的数据字典及其对应的取值列表(Field-Value),如下几张图所示:




好了,本篇文章我们就介绍到这里了,建议各位小伙伴一定要照着文章提供的样例代码撸一撸,只有撸过才能知道这玩意是咋用的,否则就成了“空谈者”!

对Redis相关技术栈以及实际应用场景实战感兴趣的小伙伴可以前往Debug搭建的技术社区的课程中心进行学习观看:https://www.fightjava.com/web/index/course/detail/12

其他相关的技术,感兴趣的小伙伴可以关注底部Debug的技术公众号,或者加Debug的微信,拉你进“微信版”的真正技术交流群!一起学习、共同成长!

补充:

1、本文涉及到的相关的源代码可以到此地址,check出来进行查看学习:

https://gitee.com/steadyjack/SpringBootRedis

2、目前Debug已将本文所涉及的内容整理录制成视频教程,感兴趣的小伙伴可以前往观看学习:https://www.fightjava.com/web/index/course/detail/12

3、关注一下Debug的技术微信公众号,最新的技术文章、课程以及技术专栏将会第一时间在公众号发布哦!