Redis實(shí)戰(zhàn)(11)-哈希Hash典型應(yīng)用場景實(shí)戰(zhàn)之系統(tǒng)數(shù)據(jù)字典實(shí)時(shí)觸發(fā)緩存存儲

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。



摘要:前文我們已經(jīng)介紹并實(shí)戰(zhàn)了Redis的數(shù)據(jù)類型哈希Hash的相關(guān)命令行及其對應(yīng)的Java單元測試的實(shí)戰(zhàn)代碼,本文我們將以實(shí)際項(xiàng)目中典型的應(yīng)用場景“系統(tǒng)數(shù)據(jù)字典模塊的實(shí)時(shí)觸發(fā)存儲”為案例,學(xué)以致用,一起踐行哈希Hash在實(shí)際項(xiàng)目下的實(shí)戰(zhàn)應(yīng)用,感受感受其在實(shí)際業(yè)務(wù)場景下的作用!

內(nèi)容:在前文我們已經(jīng)簡單介紹了Redis的數(shù)據(jù)類型~哈希Hash的底層存儲結(jié)構(gòu),很顯然,哈希Hash跟其他的數(shù)據(jù)結(jié)構(gòu)還是有諸多不同之處的。其他的據(jù)結(jié)構(gòu)幾乎都是:Key-Value的存儲,而Hash則是:Key – [Field-Value] 的存儲,也就是說其他數(shù)據(jù)結(jié)構(gòu)的Value一般是確切的值,而Hash的Value是一系列的鍵值對,通常我們是這樣子稱呼Hash的存儲的:大Key為實(shí)際的Key,小Key為Field,而具體的取值為Field對應(yīng)的值。如下圖所示:


說實(shí)在的,它的作用還是很強(qiáng)大的,特別是在存儲“同種對象類型”的數(shù)據(jù)列表時(shí)哈希Hash更能體現(xiàn)其優(yōu)勢,除此之外,其最大的、直觀上的作用便是“減少了緩存Key的數(shù)量”,而這主要還得得益于哈希Hash底層存儲數(shù)據(jù)時(shí)的存儲方式,如上圖所示!

接下來,我們便以實(shí)際項(xiàng)目開發(fā)中典型、常見的應(yīng)用場景“系統(tǒng)數(shù)據(jù)字典實(shí)時(shí)觸發(fā)緩存存儲”為案例一起來踐行哈希Hash的作用。

對于“數(shù)據(jù)字典模塊”,相信很多小伙伴都有所聽聞過,毫不夸張地講,幾乎每個(gè)項(xiàng)目都會有一個(gè)獨(dú)立的功能模塊,用于管理項(xiàng)目中各個(gè)業(yè)務(wù)模塊經(jīng)常出現(xiàn)的“通用化、共性的、需要配置起來的東西”,這些通用化的東西我們可以稱之為“數(shù)據(jù)字典”,對于這些東西我們一般會單獨(dú)開辟一個(gè)獨(dú)立的功能模塊,如“數(shù)據(jù)字典模塊”進(jìn)行單獨(dú)維護(hù)管理!

對于上面這個(gè)解釋,可能有些小伙伴有點(diǎn)懵,下面我們舉個(gè)栗子吧,比如經(jīng)??梢砸姷降臄?shù)據(jù)字典:“性別Sex~其取值可以有:男=1;女=0;未知=2”;比如“支付狀態(tài)PayStatus~其取值可以有:1=未支付;2=已支付;3=已取消支付;4=已退款…”;再比如“訂單審核狀態(tài)ReviewStatus~1=已保存/未審核;2=已審核;3=審核成功;4=審核失敗…”等等可以將其配置在“數(shù)據(jù)字典功能模塊”中將其維護(hù)起來,如下圖所示:


看到上面這張圖,有些機(jī)靈的小伙伴可能會立即聯(lián)想到哈希Hash的底層存儲結(jié)構(gòu)(本文開篇的那張圖),會發(fā)現(xiàn)驚人的相似,就拿“性別Sex”這一數(shù)據(jù)字典為例,它的取值為“Female-女性”、“Male-男性”,這不就相當(dāng)于哈希Hash的底層存儲結(jié)構(gòu)嗎~Key=Sex,F(xiàn)ield-Value對包含兩隊(duì),分別是:Field=Female ~ Value=女性;Field=Male ~ Value=男性。

理解了這種數(shù)據(jù)關(guān)聯(lián)以及存儲之后,在后文的實(shí)戰(zhàn)中你就會發(fā)現(xiàn)代碼很容易理解,并且在實(shí)戰(zhàn)過后你或許會發(fā)出驚嘆:“原來如此!”

除此之外,還有一種現(xiàn)象需要跟小伙伴分享分享,那就是“數(shù)據(jù)字典功能模塊”一旦配置好了某個(gè)“數(shù)據(jù)字典”之后,我們基本上會在好幾個(gè)月內(nèi)都不會去重新修改它了,即有點(diǎn)“一勞永逸”的感覺!基于這個(gè)前提,我們可以將前端發(fā)起的請求實(shí)時(shí)訪問數(shù)據(jù)庫DB的“數(shù)據(jù)字典” 優(yōu)化為 基于緩存Redis的哈希Hash進(jìn)行存儲與訪問,并且這種存儲是“實(shí)時(shí)”的,那我們就開始吧!

(1)同樣的道理,工欲善其事,必先利其器,我們首先需要建立一個(gè)數(shù)據(jù)庫表sys_config用于存儲管理員添加的數(shù)據(jù)字典,其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 '選項(xiàng)編碼',
`value` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '選項(xiàng)取值',
`order_by` int(11) DEFAULT '1' COMMENT '排序',
`is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_type_code` (`type`,`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='字典配置表';

采用Mybatis的逆向工程生成該數(shù)據(jù)庫表的實(shí)體類Entity、Mapper操作接口及其對應(yīng)的用于操作動態(tài)SQL的Mapper.xml,在這里我們只貼出SysConfigMapper接口中一個(gè)相當(dāng)重要的方法吧:  

//查詢目前數(shù)據(jù)字典表中所有可用的-已激活的數(shù)據(jù)字典列表
List<SysConfig> selectActiveConfigs();

其對應(yīng)的動態(tài)SQL實(shí)現(xiàn)如下所示:  

  <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)緊接著,我們建立一個(gè)HashController,用于“新增數(shù)據(jù)字典”、“獲取緩存中所有的數(shù)據(jù)字典”以及“獲取特定編碼的數(shù)據(jù)字典取值列表”,其完整的源代碼如下所示:  

/**數(shù)據(jù)類型Hash散列-減少key存儲、類似于map-可以通過鍵取得其 “值” (可以對象列表...)
* @Author:debug (SteadyJack) **/
@RestController
@RequestMapping("hash")
public class HashController extends AbstractController {

@Autowired
private HashService hashService;
//新增數(shù)據(jù)字典
@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;
}
//獲取緩存中所有的數(shù)據(jù)字典
@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;
}
//獲取緩存中某個(gè)特定編碼下數(shù)據(jù)字典的取值列表
@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下那幾個(gè)方法的實(shí)現(xiàn)邏輯即為真正要做的事情,其完整源代碼如下所示:  

/**hash數(shù)據(jù)類型-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:添加數(shù)據(jù)字典及其對應(yīng)的選項(xiàng)(field-value)
@Transactional(rollbackFor = Exception.class)
public Integer addSysConfig(SysConfig config) throws Exception{
int res=sysConfigMapper.insertSelective(config);
if (res>0){
//TODO:實(shí)時(shí)觸發(fā)數(shù)據(jù)字典的hash存儲
hashRedisService.cacheConfigMap();
}
return config.getId();
}

//TODO:取出緩存中所有的數(shù)據(jù)字典列表
public Map<String,List<SysConfig>> getAll() throws Exception{
return hashRedisService.getAllCacheConfig();
}

//TODO:取出緩存中特定的數(shù)據(jù)字典列表
public List<SysConfig> getByType(final String type) throws Exception{
return hashRedisService.getCacheConfigByType(type);
}
}

(4)而hashService中實(shí)現(xiàn)數(shù)據(jù)字典的實(shí)時(shí)存取又是交給了HashRedisService相應(yīng)的方法邏輯進(jìn)行處理,其對應(yīng)的完整源代碼如下所示:  

/**hash緩存服務(wù) @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:實(shí)時(shí)獲取所有有效的數(shù)據(jù)字典列表-轉(zhuǎn)化為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:所有的數(shù)據(jù)字典列表遍歷 -> 轉(zhuǎn)化為 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("實(shí)時(shí)獲取所有有效的數(shù)據(jù)字典列表-轉(zhuǎn)化為map-存入hash緩存中-發(fā)生異常:",e.fillInStackTrace());
}
}

//TODO:從緩存hash中獲取所有的數(shù)據(jù)字典配置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中獲取所有的數(shù)據(jù)字典配置map-發(fā)生異常:",e.fillInStackTrace());
}
return map;
}

//TODO:從緩存hash中獲取特定的數(shù)據(jù)字典列表
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中獲取特定的數(shù)據(jù)字典列表-發(fā)生異常:",e.fillInStackTrace());
}
return list;
}
}

至此,我們已經(jīng)完成了哈希Hash典型應(yīng)用場景“系統(tǒng)數(shù)據(jù)字典的實(shí)時(shí)存取”的代碼實(shí)戰(zhàn)了,相應(yīng)的代碼的含義我們也在代碼中做了相應(yīng)的注釋!如果有疑問的地方,各位小伙伴可以加Debug的聯(lián)系方式進(jìn)行交流(代碼中就有我的交流聯(lián)系方式哦?。?,下面我們基于Postman進(jìn)行一波測試吧!

A.首先是往數(shù)據(jù)庫中已有的某個(gè)數(shù)據(jù)字典添加某些具體的取值列表(Field-Value),如下幾張圖所示:




B.最后是往數(shù)據(jù)庫中添加一個(gè)全新的數(shù)據(jù)字典及其對應(yīng)的取值列表(Field-Value),如下幾張圖所示:




好了,本篇文章我們就介紹到這里了,建議各位小伙伴一定要照著文章提供的樣例代碼擼一擼,只有擼過才能知道這玩意是咋用的,否則就成了“空談?wù)摺保?br>

對Redis相關(guān)技術(shù)棧以及實(shí)際應(yīng)用場景實(shí)戰(zhàn)感興趣的小伙伴可以前往Debug搭建的技術(shù)社區(qū)的課程中心進(jìn)行學(xué)習(xí)觀看:https://www.fightjava.com/web/index/course/detail/12

其他相關(guān)的技術(shù),感興趣的小伙伴可以關(guān)注底部Debug的技術(shù)公眾號,或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長!

補(bǔ)充:

1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來進(jìn)行查看學(xué)習(xí):

https://gitee.com/steadyjack/SpringBootRedis

2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學(xué)習(xí):https://www.fightjava.com/web/index/course/detail/12

3、關(guān)注一下Debug的技術(shù)微信公眾號,最新的技術(shù)文章、課程以及技術(shù)專欄將會第一時(shí)間在公眾號發(fā)布哦!