我是靠谱客的博主 愤怒小松鼠,最近开发中收集的这篇文章主要介绍如何设计缓存中间层需求描述:工程结构:工程配置:缓存配置:如何使用:如何测试:数据分布:,觉得挺不错的,现在分享给大家,希望可以做个参考。
概述
目录
- 需求描述:
- 工程结构:
- 截图
- 代码
- 工程配置:
- pom.xml
- application.yml
- 缓存配置:
- CacheConfig
- CacheBase
- CachePolicy
- 如何使用:
- User
- IUserService
- UserServiceImpl
- UserServiceCache
- UserController
- 如何测试:
- 数据分布:
需求描述:
传入主键集合获取对象列表,要求高命中率,主键类型可以是Integer、Long、String等类型,并且与业务解耦,不要求实时性。
工程结构:
截图
代码
链接:https://pan.baidu.com/s/1nO7fzPhSoA5WqNSmJ1dkrA
提取码:jv1p
工程配置:
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-starter-redis -->
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.6.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
jetcache:
statIntervalMinutes: 1
areaInCacheName: false
local:
default:
type: linkedhashmap
keyConvertor: fastjson
limit: 100
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 100
maxIdle: 100
maxTotal: 100
host: 127.0.0.1
port: 6379
缓存配置:
CacheConfig
仓库地址:https://github.com/alibaba/jetcache
@Configuration
@EnableMethodCache(basePackages = "com.example.demo.cache")
@EnableCreateCacheAnnotation
public class CacheConfig {}
CacheBase
核心思想:对于批量请求主键集合数据,首先到缓存中查询,将查询到的数据保存下来,然后用批量请求主键集合和刚才查询出来的数据主键集合做一个差值,如果差值为空,说明所有数据都命中了,此时只需要直接返回即可。如果差值存在,那么需要将差值主键集合传入到Lambda中,调用业务进行查找然后返回,我们需要对差值集合主键查询出来的数据,保存到缓存中,同时也要对空值进行处理,防止缓存穿透,以及要设置随机过期时间,防止缓存雪崩,最终返回查询结果。
public abstract class CacheBase {
/**
* 集合加载因子
*/
public static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 获取缓存集合
*
* @return
*/
public abstract Cache<String, String> getCacheMap();
/**
* 获取缓存策略
*
* @return
*/
public abstract CachePolicy getCachePolicy();
/**
* 获取缓存数据
*
* @param guidList 业务主键集合
* @param clazz 返回集合类型
* @param keyMapper 获取对象主键
* @param businessLoader 未命中主键查询
* @param args 缓存键的键前缀
* @param <P> 主键类型,例如:Integer、Long、String、...
* @param <T> 对象类型,例如:User、Student、...
* @return
*/
public <P, T> List<T> getAll(List<P> guidList,
Class<T> clazz,
Function<? super T, ? extends P> keyMapper,
Function<List<P>, List<T>> businessLoader,
Object... args) {
// 获取业务缓存策略
CachePolicy policy = getCachePolicy();
policy.inspect();
String emptyObject = policy.getEmptyObject();
int emptyObjectMinExpiredTime = policy.getEmptyObjectMinExpiredTime();
int emptyObjectMaxExpiredTime = policy.getEmptyObjectMaxExpiredTime();
TimeUnit emptyObjectExpiredTimeUnit = policy.getEmptyObjectExpiredTimeUnit();
int plainObjectMinExpiredTime = policy.getPlainObjectMinExpiredTime();
int plainObjectMaxExpiredTime = policy.getPlainObjectMaxExpiredTime();
TimeUnit plainObjectExpiredTimeUnit = policy.getPlainObjectExpiredTimeUnit();
// 创建最终返回集合
List<T> resultList = new ArrayList(guidList.size());
// 业务主键集合检查
if (CollectionUtils.isEmpty(guidList)) {
return resultList;
}
// 批量从缓存中查询
int initialCapacity = (int) (guidList.size() / DEFAULT_LOAD_FACTOR + 1);
Map<String, String> queryCacheMap = new HashMap<>(initialCapacity);
List<Set<String>> matchKeyList = buildGuidMatchKeyList(guidList, args);
for (Set<String> keySet : matchKeyList) {
Map<String, String> returnMap = getCacheMap().getAll(keySet);
if (!CollectionUtils.isEmpty(returnMap)) {
queryCacheMap.putAll(returnMap);
}
}
// 未命中的主键集合
List<P> missGuidList = new ArrayList<>(guidList.size());
if (queryCacheMap.keySet().size() > 0) {
for (P guid : guidList) {
String matchKey = buildGuidMatchKey(guid, args);
String cacheValue = queryCacheMap.get(matchKey);
// 缓存未命中
if (StringUtils.isEmpty(cacheValue)) {
missGuidList.add(guid);
continue;
}
// 缓存被穿透
if (emptyObject.equals(cacheValue)) {
continue;
}
resultList.add(JSON.parseObject(cacheValue, clazz));
}
} else {
missGuidList = guidList;
}
// 如果全部命中
if (CollectionUtils.isEmpty(missGuidList)) {
return resultList;
}
// 设置初始大小
initialCapacity = (int) (missGuidList.size() / DEFAULT_LOAD_FACTOR + 1);
// 查询业务逻辑
List<T> applyResultList = businessLoader.apply(missGuidList);
Map<P, T> applyResultMap = applyResultList.stream().collect(Collectors.toMap(keyMapper, Function.identity()));
Map<String, T> missGuidMap = new HashMap<>(initialCapacity);
for (Map.Entry<P, T> entry : applyResultMap.entrySet()) {
P guid = entry.getKey();
T value = entry.getValue();
String matchKey = buildGuidMatchKey(guid, args);
missGuidMap.put(matchKey, value);
}
// 防止缓存穿透
Map<String, String> nullMap = new HashMap<>(initialCapacity);
for (P guid : missGuidList) {
String matchKey = buildGuidMatchKey(guid, args);
if (!missGuidMap.containsKey(matchKey)) {
nullMap.put(matchKey, emptyObject);
}
}
if (!CollectionUtils.isEmpty(nullMap)) {
long expiredTime = getRandomNumber(emptyObjectMinExpiredTime, emptyObjectMaxExpiredTime);
getCacheMap().putAll(nullMap, expiredTime, emptyObjectExpiredTimeUnit);
}
// 保存未命中的
Map<String, String> saveMap = new HashMap<>(initialCapacity);
for (Map.Entry<String, T> entry : missGuidMap.entrySet()) {
String matchKey = entry.getKey();
T value = entry.getValue();
String cacheValue = JSON.toJSON(value).toString();
saveMap.put(matchKey, cacheValue);
}
if (!CollectionUtils.isEmpty(saveMap)) {
long expiredTime = getRandomNumber(plainObjectMinExpiredTime, plainObjectMaxExpiredTime);
getCacheMap().putAll(saveMap, expiredTime, plainObjectExpiredTimeUnit);
}
// 返回最终结果
resultList.addAll(applyResultMap.values());
return resultList;
}
/**
* 构建MatchKey集合
*
* @param guidList
* @param args
* @return
*/
private <P> List<Set<String>> buildGuidMatchKeyList(List<P> guidList, Object... args) {
// 获取业务缓存策略
CachePolicy policy = getCachePolicy();
policy.inspect();
int batchSize = policy.getBatchSize();
// 批量切分主键集合
List<Set<String>> matchKeyList = new ArrayList<>();
List<List<P>> partitionList = Lists.partition(guidList, batchSize);
for (List<P> guids : partitionList) {
Set<String> guidSet = new HashSet<>();
for (P guid : guids) {
guidSet.add(buildGuidMatchKey(guid, args));
}
matchKeyList.add(guidSet);
}
return matchKeyList;
}
/**
* 构建MatchKey对象
*
* @param guid
* @param args
* @return
*/
private <P> String buildGuidMatchKey(P guid, Object... args) {
// 获取业务缓存策略
CachePolicy policy = getCachePolicy();
policy.inspect();
String matchKeySplicer = policy.getMatchKeySplicer();
// 构建缓存主键对象
StringBuilder matchKey = new StringBuilder();
for (Object arg : args) {
matchKey.append(arg).append(matchKeySplicer);
}
matchKey.append(guid);
return matchKey.toString();
}
/**
* 获取指定范围数字
*
* @param lowerValue
* @param upperValue
* @return
*/
private long getRandomNumber(int lowerValue, int upperValue) {
return (long) Math.floor(Math.random() * (upperValue - lowerValue + 1) + lowerValue);
}
/**
* 删除缓存数据
*
* @param guidList
* @param args
* @return
*/
public <P> boolean removeAll(List<P> guidList, Object... args) {
if (CollectionUtils.isEmpty(guidList)) {
return false;
}
List<Set<String>> matchKeyList = buildGuidMatchKeyList(guidList, args);
for (Set<String> keySet : matchKeyList) {
getCacheMap().removeAll(keySet);
}
return true;
}
}
CachePolicy
@Getter
@Setter
@ToString
public class CachePolicy {
/**
* 批量处理大小
*/
private int batchSize;
/**
* 匹配键拼接符
*/
private String matchKeySplicer;
/**
* 空值对象数据
*/
private String emptyObject;
/**
* 空值对象数据:最小过期时间
*/
private int emptyObjectMinExpiredTime;
/**
* 空值对象数据:最大过期时间
*/
private int emptyObjectMaxExpiredTime;
/**
* 空值对象数据:过期时间单位
*/
private TimeUnit emptyObjectExpiredTimeUnit;
/**
* 普通对象数据:最小过期时间
*/
private int plainObjectMinExpiredTime;
/**
* 普通对象数据:最大过期时间
*/
private int plainObjectMaxExpiredTime;
/**
* 普通对象数据:过期时间单位
*/
private TimeUnit plainObjectExpiredTimeUnit;
/**
* 默认缓存策略
*/
public CachePolicy() {
this.batchSize = 100;
this.matchKeySplicer = "_";
this.emptyObject = "empty_object";
this.emptyObjectMinExpiredTime = 5;
this.emptyObjectMaxExpiredTime = 10;
this.emptyObjectExpiredTimeUnit = TimeUnit.MINUTES;
this.plainObjectMinExpiredTime = 5;
this.plainObjectMaxExpiredTime = 10;
this.plainObjectExpiredTimeUnit = TimeUnit.MINUTES;
}
/**
* 检验缓存策略
*/
public void inspect() {
if (this.batchSize <= 0) {
throw new IllegalArgumentException("batchSize Must be greater than 0");
}
if (this.matchKeySplicer == null) {
throw new IllegalArgumentException("matchKeySplicer Must be not null");
}
if (this.emptyObject == null) {
throw new IllegalArgumentException("emptyObject Must be not null");
}
if (this.emptyObjectMinExpiredTime <= 0) {
throw new IllegalArgumentException("emptyObjectMinExpiredTime Must be greater than 0");
}
if (this.emptyObjectMaxExpiredTime <= 0) {
throw new IllegalArgumentException("emptyObjectMaxExpiredTime Must be greater than 0");
}
if (this.plainObjectMinExpiredTime <= 0) {
throw new IllegalArgumentException("plainObjectMinExpiredTime Must be greater than 0");
}
if (this.plainObjectMaxExpiredTime <= 0) {
throw new IllegalArgumentException("plainObjectMaxExpiredTime Must be greater than 0");
}
if (this.plainObjectExpiredTimeUnit == null) {
throw new IllegalArgumentException("plainObjectExpiredTimeUnit Must be not null");
}
}
}
如何使用:
User
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Long id;
private String name;
}
IUserService
public interface IUserService {
/**
* 根据主键集合获取用户列表
*
* @param ids
* @return
*/
List<User> selectList(List<Long> ids);
}
UserServiceImpl
@Slf4j
@Service
public class UserServiceImpl implements IUserService {
/**
* 模拟数据库中数据
*/
static Map<Long, User> db = new HashMap<>();
static {
db.put(1L, User.builder().id(1L).name("张三").build());
db.put(2L, User.builder().id(2L).name("李四").build());
db.put(3L, User.builder().id(3L).name("王五").build());
db.put(4L, User.builder().id(4L).name("赵六").build());
}
@Override
public List<User> selectList(List<Long> ids) {
log.info("UserServiceImpl selectList ids = {}", ids);
List<User> result = new ArrayList<>();
for (Long id : ids) {
User user = db.get(id);
if (user != null) {
result.add(user);
}
}
return result;
}
}
UserServiceCache
@Slf4j
@Service
public class UserServiceCache extends CacheBase {
/**
* ==================================================
* 全局缓存配置
* ==================================================
*/
private static final String AREA = "default";
private static final String NAME = "user:list:";
@CachePenetrationProtect
@CreateCache(area = AREA, name = NAME, cacheType = CacheType.REMOTE, expire = 60)
private Cache<String, String> matchCache;
@Override
public Cache<String, String> getCacheMap() {
return this.matchCache;
}
@Override
public CachePolicy getCachePolicy() {
CachePolicy policy = new CachePolicy();
policy.setBatchSize(100);
policy.setMatchKeySplicer("_");
policy.setEmptyObject("empty_object");
policy.setEmptyObjectMinExpiredTime(5);
policy.setEmptyObjectMaxExpiredTime(10);
policy.setEmptyObjectExpiredTimeUnit(TimeUnit.MINUTES);
policy.setPlainObjectMinExpiredTime(5);
policy.setPlainObjectMaxExpiredTime(10);
policy.setPlainObjectExpiredTimeUnit(TimeUnit.MINUTES);
return policy;
}
/**
* ==================================================
* 业务缓存处理
* ==================================================
*/
@Autowired
private IUserService userService;
public List<User> selectList(List<Long> ids) {
log.info("UserServiceCache selectList ids = {}", ids);
return super.getAll(ids, User.class, User::getId, userService::selectList, "user", "id");
}
}
UserController
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserServiceCache userServiceCache;
@PostMapping("selectList")
public List<User> selectList(@RequestBody List<Long> ids) {
return userServiceCache.selectList(ids);
}
@PostMapping("removeList")
public boolean removeList(@RequestBody List<Long> ids) {
return userServiceCache.removeAll(ids, "user", "id");
}
}
如何测试:
数据分布:
第一次访问,缓存中不存在,因此没有命中,hit=0,数据每隔1分钟统计一次,统计间隔可修改
第二次访问,缓存中已存在,因此肯定命中,hit=5,数据每隔1分钟统计一次,统计间隔可修改
以下内容则是数据在Redis中的分布情况,每个Key的前缀参数列表是user、id,_ 是参数连接符
最后
以上就是愤怒小松鼠为你收集整理的如何设计缓存中间层需求描述:工程结构:工程配置:缓存配置:如何使用:如何测试:数据分布:的全部内容,希望文章能够帮你解决如何设计缓存中间层需求描述:工程结构:工程配置:缓存配置:如何使用:如何测试:数据分布:所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复