基于 Guava 实现 dict 字典数据的本地缓存

This commit is contained in:
YunaiV
2022-06-17 19:44:28 +08:00
parent d1271f8bff
commit d3200910db
18 changed files with 184 additions and 289 deletions

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.module.system.api.dict;
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.system.convert.dict.DictDataConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import org.springframework.stereotype.Service;
@ -22,4 +25,16 @@ public class DictDataApiImpl implements DictDataApi {
dictDataService.validDictDatas(dictType, values);
}
@Override
public DictDataRespDTO getDictData(String dictType, String value) {
DictDataDO dictData = dictDataService.getDictData(dictType, value);
return DictDataConvert.INSTANCE.convert02(dictData);
}
@Override
public DictDataRespDTO parseDictData(String dictType, String label) {
DictDataDO dictData = dictDataService.parseDictData(dictType, label);
return DictDataConvert.INSTANCE.convert02(dictData);
}
}

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.system.convert.dict;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.*;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@ -29,6 +29,4 @@ public interface DictDataConvert {
DictDataRespDTO convert02(DictDataDO bean);
List<DictDataRespDTO> convertList03(Collection<DictDataDO> list);
}

View File

@ -8,11 +8,9 @@ import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPage
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@Mapper
@ -23,6 +21,11 @@ public interface DictDataMapper extends BaseMapperX<DictDataDO> {
.eq(DictDataDO::getValue, value));
}
default DictDataDO selectByDictTypeAndLabel(String dictType, String label) {
return selectOne(new LambdaQueryWrapper<DictDataDO>().eq(DictDataDO::getDictType, dictType)
.eq(DictDataDO::getLabel, label));
}
default List<DictDataDO> selectByDictTypeAndValues(String dictType, Collection<String> values) {
return selectList(new LambdaQueryWrapper<DictDataDO>().eq(DictDataDO::getDictType, dictType)
.in(DictDataDO::getValue, values));
@ -46,7 +49,4 @@ public interface DictDataMapper extends BaseMapperX<DictDataDO> {
.eqIfPresent(DictDataDO::getStatus, reqVO.getStatus()));
}
@Select("SELECT COUNT(*) FROM system_dict_data WHERE update_time > #{maxUpdateTime}")
Long selectCountByUpdateTimeGt(Date maxUpdateTime);
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.module.system.mq.consumer.dict;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.dict.DictDataRefreshMessage;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link DictDataRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class DictDataRefreshConsumer extends AbstractChannelMessageListener<DictDataRefreshMessage> {
@Resource
private DictDataService dictDataService;
@Override
public void onMessage(DictDataRefreshMessage message) {
log.info("[onMessage][收到 DictData 刷新消息]");
dictDataService.initLocalCache();
}
}

View File

@ -1,19 +0,0 @@
package cn.iocoder.yudao.module.system.mq.message.dict;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 字典数据数据刷新 Message
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DictDataRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.dict-data.refresh";
}
}

View File

@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.system.mq.producer.dict;
import cn.iocoder.yudao.module.system.mq.message.dict.DictDataRefreshMessage;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* DictData 字典数据相关消息的 Producer
*/
@Component
public class DictDataProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link DictDataRefreshMessage} 消息
*/
public void sendDictDataRefreshMessage() {
DictDataRefreshMessage message = new DictDataRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -1,12 +1,11 @@
package cn.iocoder.yudao.module.system.service.dict;
import cn.iocoder.yudao.framework.dict.core.service.DictDataFrameworkService;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import java.util.Collection;
import java.util.List;
@ -16,12 +15,7 @@ import java.util.List;
*
* @author ruoyi
*/
public interface DictDataService extends DictDataFrameworkService {
/**
* 初始化字典数据的本地缓存
*/
void initLocalCache();
public interface DictDataService {
/**
* 创建字典数据
@ -94,4 +88,21 @@ public interface DictDataService extends DictDataFrameworkService {
*/
void validDictDatas(String dictType, Collection<String> values);
/**
* 获得指定的字典数据
*
* @param dictType 字典类型
* @param value 字典数据值
* @return 字典数据
*/
DictDataDO getDictData(String dictType, String value);
/**
* 解析获得指定的字典数据,从缓存中
*
* @param dictType 字典类型
* @param label 字典数据标签
* @return 字典数据
*/
DictDataDO parseDictData(String dictType, String label);
}

View File

@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO;
@ -13,20 +12,15 @@ import cn.iocoder.yudao.module.system.convert.dict.DictDataConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO;
import cn.iocoder.yudao.module.system.dal.mysql.dict.DictDataMapper;
import cn.iocoder.yudao.module.system.mq.producer.dict.DictDataProducer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableTable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@ -47,88 +41,12 @@ public class DictDataServiceImpl implements DictDataService {
.comparing(DictDataDO::getDictType)
.thenComparingInt(DictDataDO::getSort);
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
@Resource
private DictTypeService dictTypeService;
@Resource
private DictDataMapper dictDataMapper;
@Resource
private DictDataProducer dictDataProducer;
/**
* 字典数据缓存,第二个 key 使用 label
*
* key1字典类型 dictType
* key2字典标签 label
*/
private ImmutableTable<String, String, DictDataDO> labelDictDataCache;
/**
* 字典数据缓存,第二个 key 使用 value
*
* key1字典类型 dictType
* key2字典值 value
*/
private ImmutableTable<String, String, DictDataDO> valueDictDataCache;
/**
* 缓存字典数据的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@Override
@PostConstruct
public synchronized void initLocalCache() {
// 获取字典数据列表,如果有更新
List<DictDataDO> dataList = loadDictDataIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(dataList)) {
return;
}
// 构建缓存
ImmutableTable.Builder<String, String, DictDataDO> labelDictDataBuilder = ImmutableTable.builder();
ImmutableTable.Builder<String, String, DictDataDO> valueDictDataBuilder = ImmutableTable.builder();
dataList.forEach(dictData -> {
labelDictDataBuilder.put(dictData.getDictType(), dictData.getLabel(), dictData);
valueDictDataBuilder.put(dictData.getDictType(), dictData.getValue(), dictData);
});
labelDictDataCache = labelDictDataBuilder.build();
valueDictDataCache = valueDictDataBuilder.build();
maxUpdateTime = CollectionUtils.getMaxValue(dataList, DictDataDO::getUpdateTime);
log.info("[initLocalCache][缓存字典数据,数量为:{}]", dataList.size());
}
/**
* 如果字典数据发生变化,从数据库中获取最新的全量字典数据。
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前字典数据的最大更新时间
* @return 字典数据列表
*/
private List<DictDataDO> loadDictDataIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadDictDataIfUpdate][首次加载全量字典数据]");
} else { // 判断数据库中是否有更新的字典数据
if (dictDataMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
return null;
}
log.info("[loadDictDataIfUpdate][增量加载全量字典数据]");
}
// 第二步,如果有更新,则从数据库加载所有字典数据
return dictDataMapper.selectList();
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
initLocalCache();
}
@Override
public List<DictDataDO> getDictDatas() {
List<DictDataDO> list = dictDataMapper.selectList();
@ -153,16 +71,6 @@ public class DictDataServiceImpl implements DictDataService {
return dictDataMapper.selectById(id);
}
@Override
public DictDataRespDTO getDictDataFromCache(String type, String value) {
return DictDataConvert.INSTANCE.convert02(valueDictDataCache.get(type, value));
}
@Override
public DictDataRespDTO parseDictDataFromCache(String type, String label) {
return DictDataConvert.INSTANCE.convert02(labelDictDataCache.get(type, label));
}
@Override
public Long createDictData(DictDataCreateReqVO reqVO) {
// 校验正确性
@ -171,9 +79,6 @@ public class DictDataServiceImpl implements DictDataService {
// 插入字典类型
DictDataDO dictData = DictDataConvert.INSTANCE.convert(reqVO);
dictDataMapper.insert(dictData);
// 发送刷新消息
dictDataProducer.sendDictDataRefreshMessage();
return dictData.getId();
}
@ -185,9 +90,6 @@ public class DictDataServiceImpl implements DictDataService {
// 更新字典类型
DictDataDO updateObj = DictDataConvert.INSTANCE.convert(reqVO);
dictDataMapper.updateById(updateObj);
// 发送刷新消息
dictDataProducer.sendDictDataRefreshMessage();
}
@Override
@ -197,9 +99,6 @@ public class DictDataServiceImpl implements DictDataService {
// 删除字典数据
dictDataMapper.deleteById(id);
// 发送刷新消息
dictDataProducer.sendDictDataRefreshMessage();
}
@Override
@ -259,7 +158,8 @@ public class DictDataServiceImpl implements DictDataService {
if (CollUtil.isEmpty(values)) {
return;
}
ImmutableMap<String, DictDataDO> dictDataMap = valueDictDataCache.row(dictType);
Map<String, DictDataDO> dictDataMap = CollectionUtils.convertMap(
dictDataMapper.selectByDictTypeAndValues(dictType, values), DictDataDO::getValue);
// 校验
values.forEach(value -> {
DictDataDO dictData = dictDataMap.get(value);
@ -272,4 +172,14 @@ public class DictDataServiceImpl implements DictDataService {
});
}
@Override
public DictDataDO getDictData(String dictType, String value) {
return dictDataMapper.selectByDictTypeAndValue(dictType, value);
}
@Override
public DictDataDO parseDictData(String dictType, String label) {
return dictDataMapper.selectByDictTypeAndLabel(dictType, label);
}
}

View File

@ -1,36 +1,32 @@
package cn.iocoder.yudao.module.system.service.dict;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO;
import cn.iocoder.yudao.module.system.dal.mysql.dict.DictDataMapper;
import cn.iocoder.yudao.module.system.mq.producer.dict.DictDataProducer;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import com.google.common.collect.ImmutableTable;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import static cn.hutool.core.bean.BeanUtil.getFieldValue;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.when;
@Import(DictDataServiceImpl.class)
public class DictDataServiceTest extends BaseDbUnitTest {
@ -42,39 +38,6 @@ public class DictDataServiceTest extends BaseDbUnitTest {
private DictDataMapper dictDataMapper;
@MockBean
private DictTypeService dictTypeService;
@MockBean
private DictDataProducer dictDataProducer;
/**
* 测试加载到新的字典数据的情况
*/
@Test
@SuppressWarnings("unchecked")
public void testInitLocalCache() {
// mock 数据
DictDataDO dictData01 = randomDictDataDO();
dictDataMapper.insert(dictData01);
DictDataDO dictData02 = randomDictDataDO();
dictDataMapper.insert(dictData02);
// 调用
dictDataService.initLocalCache();
// 断言 labelDictDataCache 缓存
ImmutableTable<String, String, DictDataDO> labelDictDataCache =
(ImmutableTable<String, String, DictDataDO>) getFieldValue(dictDataService, "labelDictDataCache");
assertEquals(2, labelDictDataCache.size());
assertPojoEquals(dictData01, labelDictDataCache.get(dictData01.getDictType(), dictData01.getLabel()));
assertPojoEquals(dictData02, labelDictDataCache.get(dictData02.getDictType(), dictData02.getLabel()));
// 断言 valueDictDataCache 缓存
ImmutableTable<String, String, DictDataDO> valueDictDataCache =
(ImmutableTable<String, String, DictDataDO>) getFieldValue(dictDataService, "valueDictDataCache");
assertEquals(2, valueDictDataCache.size());
assertPojoEquals(dictData01, valueDictDataCache.get(dictData01.getDictType(), dictData01.getValue()));
assertPojoEquals(dictData02, valueDictDataCache.get(dictData02.getDictType(), dictData02.getValue()));
// 断言 maxUpdateTime 缓存
Date maxUpdateTime = (Date) getFieldValue(dictDataService, "maxUpdateTime");
assertEquals(ObjectUtils.max(dictData01.getUpdateTime(), dictData02.getUpdateTime()), maxUpdateTime);
}
@Test
public void testGetDictDataPage() {
@ -148,8 +111,6 @@ public class DictDataServiceTest extends BaseDbUnitTest {
// 校验记录的属性是否正确
DictDataDO dictData = dictDataMapper.selectById(dictDataId);
assertPojoEquals(reqVO, dictData);
// 校验调用
verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
}
@Test
@ -170,8 +131,6 @@ public class DictDataServiceTest extends BaseDbUnitTest {
// 校验是否更新正确
DictDataDO dictData = dictDataMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, dictData);
// 校验调用
verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
}
@Test
@ -186,8 +145,6 @@ public class DictDataServiceTest extends BaseDbUnitTest {
dictDataService.deleteDictData(id);
// 校验数据不存在了
assertNull(dictDataMapper.selectById(id));
// 校验调用
verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
}
@Test