Java服务_数据服务缓存策略实战梳理与常见分布式缓存实现方案

Java服务_数据服务缓存策略实战梳理与常见分布式缓存实现方案

1.hbase服务缓存策略

1. 将请求参数进行排序

1
2
//将请求参数进行排序
String cacheKey = ReqUtils.sortReqToString(req);

其中这个将请求参数进行排序方法sortReqToString,本质就是将object类型按key的字典序排序,将array类型按元素的字典序排序。有一个点要注意就是每个请求参数中唯一化的参数注意要去掉,比如请求时间、uuid等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public static String sortReqToString(JSONObject reqParam){
String reqStr = sortJSONObject(reqParam);
ServiceCache.Service service = ServiceCache.getServiceRouteBean(reqParam);
if(service != null){
String cacheSuffix = service.getCacheSuffix();
if(!StringUtils.isEmpty(cacheSuffix)){
return reqStr + cacheSuffix;
}
return reqStr;
}
return reqStr;
}


private static String sortJSONObject(JSONObject obj) {
if(obj == null || obj.entrySet().size() == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append("{");
List<String> keylist = new ArrayList<String>(obj.keySet());
Collections.sort(keylist);
int size = keylist.size();
for(int i=0; i < size; i++) {
String key = keylist.get(i);
if(P_APPKEY.equals(key) || "accessTime".equals(key)) {
continue;
}
sb.append(key);
sb.append(":");
Object value = obj.get(key);
if(value instanceof JSONObject) {
sb.append(sortJSONObject((JSONObject) value));
} else if(value instanceof JSONArray) {
sb.append(sortJSONArray((JSONArray) value));
} else {
sb.append((value == null) ? "" : value.toString());
}
if(i < size-1) {
sb.append(",");
}
}
sb.append("}");
return sb.toString();
}


private static String sortJSONArray(JSONArray array) {
if(array == null || array.size() == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
List<String> list = new ArrayList<String>();
for(Object obj : array) {
list.add((obj == null) ? "" : obj.toString());
}
Collections.sort(list);
sb.append("[");
int size = list.size();
for(int i=0; i < size; i++) {
String str = list.get(i);
sb.append(str);
if(i < size-1) {
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}

重要:此处根据服务名给请求同一个服务的所有入参打上了统一的后缀,这样方便按照后缀一次性删除一个服务的所有缓存。在设计缓存系统时一定要预留一个清空缓存的方法,有的时候线上方法返回数据时错误的,改正重新上线之后必须要清空缓存才行。

2.缓存中有就直接从缓存中取JSONObject类型的请求结果

1
2
3
4
5
6
// 先从缓存中查询
JSONObject Cacheobj = (JSONObject) CacheUtils.get(cacheKey);
if (null != Cacheobj) {
logger.debug("The Cache hit......");
return Cacheobj;
}

3.缓存中没有走数据服务请求一遍数据库,并将请求结果存入缓存中

1
CacheUtil.putCache(cacheKey, obj, cacheRule, tags);

2.ck服务缓存策略

使用公司内部基于方法的服务缓存,原理与spring@Cache类似,就是将一个方法的入参作为缓存key,返回作为缓存value,当有相同的入参请求时直接返回缓存结果,不需要进入方法体内部执行方法代码。是一个适用于分布式服务的缓存。

配置缓存方法

1
2
3
4
@CacheEnabled(expiredTimeStr = "0 0 8 * * ?", validator = JudgeBlankValidator.class)
public <R> R invokeMethod(String methodName, String cacheSuffix, JSONObject requestBody, @ParamExcluded JSONObject requestHeader, @ParamExcluded String appSuffix) {
return invokeMethodImpl(methodName, requestBody, requestHeader, appSuffix, CK, OFFLINE);
}

在这个缓存方法内部和外部分别加一层监控或者日志记录就可以轻松地统计缓存命中率了。

CacheConfiguration

CacheConfiguration中是对缓存redis集群、缓存ttl等参数的初始化配置类,这个是公司组件当中封装好的,直接用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class CacheConfiguration {
private static final Logger logger = Logger.getLogger(CacheConfiguration.class);
private static final String[] STATUSES = new String[]{"on", "off"};
public static String APP_CODE;
public static boolean CACHE_STATUS;
public static long MAX_EXPIRED_TIME;
public static long MIN_EXPIRED_TIME;
public static boolean KM_STATUS;
public static boolean KM_ADMIN_AUTHORITY;
public static int KM_MAX_PAGE_SIZE;
public static boolean KM_EH_SCHEDULED_STATUS;
public static boolean KM_RELOAD_STATUS;
public static boolean KM_RL_SCHEDULED_STATUS;
public static boolean CACHE_PROFILER_STATUS;
public static String CACHE_PROFILER_APP_NAME;
public static String DEFAULT_GROUP;
private static Properties config = new Properties();

public CacheConfiguration() {
}

public static String getValue(String key) {
String value = config.getProperty(key);
return StringUtils.isBlank(value) ? null : value.trim();
}

public static String getNotNullValue(String key) {
String value = config.getProperty(key);
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException(key + " must be not null!");
} else {
return value.trim();
}
}

public static String getValue(String key, String defaultValue) {
String value = config.getProperty(key, defaultValue);
return StringUtils.isBlank(value) ? null : value.trim();
}

static {
logger.info("CacheConfiguration start...");
List fileList = FileUtil.loadFiles(Arrays.asList("properties"), Arrays.asList("bdp-ard-cache"));

try {
config.load(new FileInputStream((File)fileList.get(0)));
APP_CODE = getNotNullValue("cache.appcode");
CACHE_STATUS = STATUSES[0].equalsIgnoreCase(getValue("cache.status", STATUSES[1]));
MAX_EXPIRED_TIME = Long.parseLong(getValue("cache.max.expired.time", "86400"));
MIN_EXPIRED_TIME = Long.parseLong(getValue("cache.min.expired.time", "60"));
KM_STATUS = CACHE_STATUS && STATUSES[0].equalsIgnoreCase(getValue("keymanager.status", STATUSES[0]));
KM_ADMIN_AUTHORITY = STATUSES[0].equalsIgnoreCase(getValue("keymanager.admin.authority", STATUSES[1]));
KM_MAX_PAGE_SIZE = Integer.parseInt(getValue("keymanager.max.pagesize", "1000"));
KM_EH_SCHEDULED_STATUS = KM_STATUS && STATUSES[0].equalsIgnoreCase(getValue("keymanager.scheduled.exception.handler.status", STATUSES[1]));
KM_RELOAD_STATUS = KM_STATUS && STATUSES[0].equalsIgnoreCase(getValue("keymanager.reload.status", STATUSES[1]));
KM_RL_SCHEDULED_STATUS = KM_STATUS && STATUSES[0].equalsIgnoreCase(getValue("keymanager.reload.scheduled.status", STATUSES[1]));
DEFAULT_GROUP = getNotNullValue("default.group");
CACHE_PROFILER_STATUS = KM_STATUS && STATUSES[0].equalsIgnoreCase(getValue("cache.profiler.status", STATUSES[1]));
CACHE_PROFILER_APP_NAME = getValue("cache.profiler.app.name", "");
} catch (Exception var2) {
CACHE_STATUS = false;
KM_STATUS = false;
KM_EH_SCHEDULED_STATUS = false;
KM_RELOAD_STATUS = false;
KM_RL_SCHEDULED_STATUS = false;
CACHE_PROFILER_STATUS = false;
logger.error((Object)null, var2);
}

logger.info("CacheConfiguration end...");
}
}

3.实时离线对比榜单二级缓存

背景:有一个交易&浏览top200sku&spu商品实时榜单,每6s会根据数据情况实时更新一次,同时还需要提供每个商品的同比&环比日期离线数据。对于数据服务来说,top200商品数据是一个方法一次查询直接出的,那么对于方法粒度的缓存命中率非常低,因为每次请求的200个商品只要有任意一个变动都会时缓存不命中。

在上述背景之下考虑添加商品粒度的二级缓存,该二级缓存的存储粒度为单个商品粒度的指标数据,那么方法思路即为一级缓存未命中时,将200个商品轮询请求缓存,命中的商品直接加入返回结果,未命中的放在一起最终再一次性请求数据库,再将返回的数据按商品粒度逐个存入缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//添加二级缓存
public JSONObject getRTTrendsProductCompareAccLastDot(JSONObject jsonObject) {
List<LinkedHashMap<String, Object>> result = new ArrayList<LinkedHashMap<String, Object>>();
JSONObject obj;
String methodName = "getRTTrendsProductCompareAccLastDot";

JSONObject requestBody = jsonObject.getJSONObject(P_BODY);
JSONObject requestDimensions = requestBody.getJSONObject(P_DIMENSIONS);
JSONObject requestDimensionsBackup = JSONObject.parseObject(requestDimensions.toJSONString());
String proType = requestDimensions.getString("ProType");

if ("sku".equals(proType)) {
JSONArray skuList = requestDimensions.getJSONArray("SkuId");
JSONArray unCachedSku = new JSONArray();
for (int i = 0; i < skuList.size(); i++) {
String skuId = skuList.getString(i);
requestDimensions.put("SkuId", skuId);
String cacheKey = sortJSONObject(requestBody);
String cacheRep = (String) CacheUtils.get(cacheKey, "rtcomp");
if (cacheRep == null) {
unCachedSku.add(skuId);
}else {
BrandLog.logInfo(methodName + " hit cache!");
BrandLog.logInfo(methodName + " cache key : " + cacheKey);
BrandLog.logInfo(methodName + " cache res : " + cacheRep);
if (!"NODATASKU".equals(cacheRep)) {
result.add(JSONObject.parseObject(cacheRep, LinkedHashMap.class));
}
}
}
requestDimensions.put("SkuId", unCachedSku);
}else if ("spu".equals(proType)) {
JSONArray spuList = requestDimensions.getJSONArray("SpuId");
JSONArray unCachedSpu = new JSONArray();
for (int i = 0; i < spuList.size(); i++) {
String spuId = spuList.getString(i);
requestDimensions.put("SpuId", spuId);
String cacheKey = sortJSONObject(requestBody);
String cacheRep = (String) CacheUtils.get(cacheKey, "rtcomp");
if (cacheRep == null) {
unCachedSpu.add(spuId);
}else {
BrandLog.logInfo(methodName + " hit cache!");
BrandLog.logInfo(methodName + " cache key : " + cacheKey);
BrandLog.logInfo(methodName + " cache res : " + cacheRep);
if (!"NODATASPU".equals(cacheRep)) {
result.add(JSONObject.parseObject(cacheRep, LinkedHashMap.class));
}
}
}
requestDimensions.put("SpuId", unCachedSpu);
}

if (("sku".equals(proType) && requestDimensions.getJSONArray("SkuId").size() != 0) || ("spu".equals(proType) && requestDimensions.getJSONArray("SpuId").size() != 0)) {

List<LinkedHashMap<String, Object>> dbDate = queryRTTrendsProductCompareAccLastDot(jsonObject, "getRTTrendsProductCompareAccLastDot");

BrandLog.logInfo(methodName + " unhit cache!");
BrandLog.logInfo(methodName + " real DB request : " + JSONObject.toJSONString(jsonObject));
BrandLog.logInfo(methodName + " real DB response : " + JSONObject.toJSONString(dbDate));

String[] tags = {methodName, "com.jd.ad.service.RTimpl.BrandRTOverviewService"};
if ("sku".equals(proType)) {
JSONArray unCachedSku = requestDimensions.getJSONArray("SkuId");
if (unCachedSku != null) {
for (int i = 0; i < unCachedSku.size(); i++) {
String skuId = unCachedSku.getString(i);
requestDimensions.put("SkuId", skuId);
String cacheKey = sortJSONObject(requestBody);
String cacheStr = "NODATASKU";
for (LinkedHashMap<String, Object> singledbDate : dbDate) {
if (skuId.equals(singledbDate.get("SkuId").toString())) {
cacheStr = JSONObject.toJSONString(singledbDate);
break;
}
}
BrandLog.logInfo(methodName + " key caching... " + cacheKey);
BrandLog.logInfo(methodName + " res caching... " + cacheStr);
CacheUtils.put(cacheKey, cacheStr, 86400L, tags, "rtcomp");
}
}
}else if ("spu".equals(proType)) {
JSONArray unCachedSpu = requestDimensions.getJSONArray("SpuId");
if (unCachedSpu != null) {
for (int i = 0; i < unCachedSpu.size(); i++) {
String spuId = unCachedSpu.getString(i);
requestDimensions.put("SpuId", spuId);
String cacheKey = sortJSONObject(requestBody);
String cacheStr = "NODATASPU";
for (LinkedHashMap<String, Object> singledbDate : dbDate) {
if (spuId.equals(singledbDate.get("SpuId").toString())) {
cacheStr = JSONObject.toJSONString(singledbDate);
break;
}
}
BrandLog.logInfo(methodName + " key caching... " + cacheKey);
BrandLog.logInfo(methodName + " res caching... " + cacheStr);
CacheUtils.put(cacheKey, cacheStr, 86400L, tags, "rtcomp");
}
}
}

if (dbDate != null && dbDate.size() > 0) {
result.addAll(dbDate);
}
}

requestBody.put(P_DIMENSIONS, requestDimensionsBackup);
obj = new TransformForClickHouse().transform(result);
return obj;
}

4.常见分布式缓存实现方案

4.1 缓存重要指标

缓存最重要的两个指标就是缓存命中率和移除策略。

命中率是指从缓存中读取次数与总读取次数的比值,命中率越高越好。

移除策略是指缓存中的数据如果删除的策略,主要有四种:

1)FIFO先进先出

2)LRU最久未使用先出

3)LFU被使用次数最少的先出

4)TTL按指定时间自动过期

4.2 基于Redis实现分布式缓存

4.2.1 搭建环境

添加核心依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class RedisConfig {

@Autowired
private RedisTemplate redisTemplate;

//序列化设置一下
@PostConstruct
public void setRedisTemplate() {
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
}

}
4.2.2 缓存实现

实现流程非常简单,就是先请求缓存,命中则直接返回,未命中则请求DB并存入缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public DsAprovinces selectById(Integer id) {

String key = String.valueOf(id);
DsAprovinces dsAprovinces= null;
//1.从redis查
dsAprovinces = (DsAprovinces)redisTemplate.opsForValue().get(key);

//2.如果不空
if(null!= dsAprovinces) {
System.out.println("redis中查到了");
return dsAprovinces;
}

//3.查询数据库
dsAprovinces = dsAprovincesMapper.selectById(id);

//3.放入缓存
if(null!= dsAprovinces) {
System.out.println("从数据库中查,放入缓存....");
redisTemplate.opsForValue().set(key,dsAprovinces);
redisTemplate.expire(key,60, TimeUnit.SECONDS); //60秒有效期
}

return dsAprovinces;
}
4.2.3 数据库增删改联动

缓存中数据key对应value需要变动时,需要将缓存中的该数据删除,而且需要使用双删保证,防止在删除缓存与更新数据库的间歇中又有对该数据的请求,导致缓存中仍旧继续存储更新前的数据。

1
2
3
4
5
6
7
8
9
//更新:确保机制:实行双删
public int update(DsAprovinces dsAprovinces) {

redisTemplate.delete(String.valueOf(dsAprovinces.getId()));
int i = dsAprovincesMapper.updateById(dsAprovinces);
redisTemplate.delete(String.valueOf(dsAprovinces.getId()));

return i;
}

4.3 基于Spring Cache实现缓存

4.3.1 Spring Cache介绍

Spring Cache是Spring框架提供的辅助缓存快捷实现的封装接口,本身还是需要我们基于它的规则提供和配置redis等缓存集群。它具有:1)基于注解,代码清爽简洁;2)支持多种缓存产品,Guava、EhCache、Redis等;3)可以实现复杂逻辑;4)可以对缓存进行回滚等优点。

Spring Cache支持的常用注解:

  • @Cacheable //查询
  • @CachePut //增改
  • @CacheEvict //删除
  • @Caching //组合多个注解
  • @CacheConfig //在类上添加,抽取公共配置
4.3.2 Spring Cache使用代码案例

创建配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
@EnableCaching //开启spring缓存
public class MyCacheConfig extends CachingConfigurerSupport {

//使用redis做为缓存
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//1.redis缓存管理器
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(connectionFactory);
//2.设置一些参数 //统一设置20s有效期
builder.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(2000)));
builder.transactionAware();
RedisCacheManager build = builder.build();
return build;
}

//可以自定义key的生成策略
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... objects) {
//1.缓冲
StringBuilder sb = new StringBuilder();
//2.类名
sb.append(target.getClass().getSimpleName());
//3.方法名
sb.append(method.getName());
//4.参数值
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}

在service层实现类的方法上添加缓存注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
@Primary
@CacheConfig(cacheNames = "provinces") //key键会添加provinces::
public class AprovincesServiceImpl2 implements IAprovincesService {

@Autowired
private DsAprovincesMapper dsAprovincesMapper;

@Autowired
private RedisTemplate redisTemplate;

@Override
@Cacheable(key = "#id")
//@Cacheable
public DsAprovinces selectById(Integer id) {

//3.查询数据库
DsAprovinces dsAprovinces = dsAprovincesMapper.selectById(id);

return dsAprovinces;
}

//更新
@CachePut(key = "#dsAprovinces.id")
public DsAprovinces update(DsAprovinces dsAprovinces) {
dsAprovincesMapper.updateById(dsAprovinces);

return dsAprovinces;
}

//添加
@CachePut(key = "#dsAprovinces.id")
public DsAprovinces save(DsAprovinces dsAprovinces) {

dsAprovincesMapper.insert(dsAprovinces);

return dsAprovinces;
}

//删除
@CacheEvict
public int delete(Integer id) {
int i = dsAprovincesMapper.deleteById(id);
return i;
}
}
4.3.3 Spring Cache缺点

任何一种技术都不是十全十美的,Spring Cache也不例外,它也有一些缺点:1)不能保证数据的一致性,添加和修改数据时,是先修改数据库,然后再进行更新缓存,如果不满意延迟导致过程中的数据不一致性问题,需要自行实现双删;2)过期时间都是统一配置,需要自行实现个性化过期时间设置。

参考文献

Java大牛必会|分布式缓存实现方案之Spring Cache