缓存最佳实践和问题排查指南
业务使用现状
由于缓存机制在提供给业务团队使用后,陆续收到不同层度的反馈,总结如下几方面:
- 缓存失效
- 使用同一个缓存声明实例,并且传参是同一个实例对象,但是每次会执行到
get方法逻辑重新 loadingCache
- 使用同一个缓存声明实例,并且传参是同一个实例对象,但是每次会执行到
- 缓存拉取的结果与业务预期不符
- 使用同一个缓存声明实例,传参是 QSiteQuerySO(skuCode=“SKU20210627000001”),但是返回的是(skuCode=“SKU20210616000001”)的数据
- 机制本身存在的缺陷
- 入参类型是
String,但是实现类get方法接收到的字符串前后多加了一对双引号,如传参是abc,接收到参数是"abc"
- 入参类型是
最佳实践原则
-
优先推荐使用**普通类型(字符串、数值类型、布尔类型、日期类型…)**作为入参类型,可以减少表达式解析的步骤,而且拼接出来的缓存 key 由开发人员灵活定义,在 Redis 存储和后续问题排查时很方便;
-
如果由于业务场景需要,不得不使用复杂对象(BO、SO、QO…)作为入参,则建议搭配使用
keyPath或multiKeyPath注解属性来声明MVEL表达式实现属性导航。 -
使用表达式语言实现属性导航从而解析得到的结果应尽量为普通类型,因为这样产生的 Redis key 对系统和开发人员来说是一个明确的值,更加直观和易于理解。
trantor-parent < 0.17.37.RELEASE:会出现缓存失效场景,后面有介绍;trantor-parent >= 0.17.47.RELEASE:支持对解析结果为复杂对象进行toJson处理转换成JSON文本,但是我们不推荐使用这种做法;- 以后的版本(暂未定):不支持解析结果非
NULL且是一个能序列化为JSON的对象,直接抛出FunctionException异常。
最佳实践示例
注意:假设下面的示例都在gaia层设计,因此生成的缓存key中会携带gaia的appKey信息,如果是在bbc,则会携带bbc的appKey
模型示例
- ItemBO(商品)
@Data@Model(name = "商品")public class ItemBO extends BaseModel<Long> {
@Field(name = "商品名称") private String itemName;
@Field(name = "商品编号") private String itemNumber;
@Field(name = "商品价格") private BigDecimal itemPrice;
@Field(name = "品牌") @LinkMeta private BrandBO brand;}- BrandBO(品牌)
@Data@Model(name = "品牌")public class BrandBO extends BaseModel<Long> {
@Field(name = "品牌编号") private String brandCode;
@Field(name = "品牌名称") private String brandName;
@Field(name = "品牌名称") private String brandName;}缓存实例代码
字符串入参
@Cache(name = "item cache", desc = "Get the whole item cache via item code")public interface TestStringKeyCache extends FunctionCache<String, ItemBO> {}| 入参对象 | redis hash key | redis hash field | 是否推荐 |
|---|---|---|---|
| SKU20210627000001 | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestStringKeyCache | SKU20210627000001 | 是 |
| SKU20210616000001 | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestStringKeyCache | SKU20210616000001 | 是 |
| null | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestStringKeyCache | null | 否 |
长整型入参
@Cache(name = "item cache", desc = "Get the whole item cache via item id")public interface TestLongKeyCache extends FunctionCache<Long, ItemBO> {}| 入参对象 | redis hash key | redis hash field | 是否推荐 |
|---|---|---|---|
| 705933611 | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestLongKeyCache | 705933611 | 是 |
| 387394721 | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestLongKeyCache | 387394721 | 是 |
| null | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestLongKeyCache | null | 否 |
业务模型入参
入参业务模型对象:
{ "id":4001, "itemName":"Huawei/华为 Mate 40 Pro", "itemNumber":"H4001", "itemPrice":8499.00, "brand":{ "brandCode":"Huawei202105011006", "brandName":"Huawei/华为", "id":1003 }}- keyPath
@Cache(name = "Brand cache", desc = "Get brand cache via item object", keyPath = "id")public interface TestBusinessModelKeyCache extends FunctionCache<ItemBO, BrandBO> {}- 推荐用法
| keyPath | redis hash key | redis hash field |
|---|---|---|
| id | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestBusinessModelKeyCache | Id:4001 |
| itemNumber | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestBusinessModelKeyCache | itemNumber:H4001 |
| brand.id | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestBusinessModelKeyCache | brand.id:1003 |
| brand.brandCode | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestBusinessModelKeyCache | brand.brandCode:Huawei202105011006 |
- 不推荐用法
| keyPath | trantor-parent version | redis hash field |
|---|---|---|
| 未配置 | < 0.17.37.RELEASE | 随机 |
| 未配置 | >= 0.17.37.RELEASE | 入参对象 JSON 文本 |
| brand | < 0.17.47.RELEASE | 随机 |
| brand | >= 0.17.47.RELEASE | brand 对象 JSON 文本 |
- multiKeyPath
@Cache(name = "Brand cache", desc = "Get brand cache via item object", multiKeyPath = {"id", "itemNumber"})public interface TestMultiKeyPathCache extends FunctionCache<ItemBO, BrandBO> {}- 推荐用法
| multiKeyPath | redis hash key | redis hash field |
|---|---|---|
| {“id”,“itemNumber”} | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestMultiKeyPathCache | Id:4001:itemNumber:H4001 |
| {“itemNumber”} | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestMultiKeyPathCache | itemNumber:H4001 |
| {“id”,“brand.id”} | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestMultiKeyPathCache | Id:4001:brand.id:1003 |
| {“brand.brandCode”} | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestMultiKeyPathCache | brand.brandCode:Huawei202105011006 |
| {“brand.id”,“brand.brandCode”} | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestMultiKeyPathCache | brand.id:1003:brand.brandCode:Huawei202105011006 |
- 不推荐用法
| multiKeyPath | trantor-parent version | redis hash field |
|---|---|---|
| 未配置 | < 0.17.37.RELEASE | 随机 |
| 未配置 | >= 0.17.37.RELEASE | 入参对象 JSON 文本 |
| {“brand”} | < 0.17.47.RELEASE | 随机 |
| {“brand”} | >= 0.17.47.RELEASE | brand 对象 JSON 文本 |
QModel 入参
@Cache(keyPath = "...")public interface TestQModelKeyCache extends FunctionCache<QItemBO, List<ItemBO>> {}属性为单值,以 id 为例
{ "__hasFields": [ "id", "brand" ], "queryParams": { "page": { "no": 0, "size": 10, "skipCount": false } }, "id": { "type": "One", "fullMatch": false, "value": 653656589 }, "brand": { "__hasFields": [ "id" ], "queryParams": { "page": { "no": 0, "size": 10, "skipCount": false } }, "id": { "type": "One", "fullMatch": false, "value": 35407588 } }}- 推荐用法
| keyPath | redis hash key | redis hash field |
|---|---|---|
| id.value | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | id.value:653656589 |
| brand.id.value | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | brand.id.value:35407588 |
- 不推荐用法一
| keyPath | redis hash key | redis hash field |
|---|---|---|
| id.values | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | id.values:null |
| itemNumber | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | itemNumber:null |
| brand.brandCode | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | brand.brandCode:null |
- 不推荐用法二
| keyPath | trantor-parent version | redis hash field |
|---|---|---|
| 未配置 | < 0.17.37.RELEASE | 随机 |
| 未配置 | >= 0.17.37.RELEASE | 入参对象 JSON 文本 |
| brand | < 0.17.47.RELEASE | 随机 |
| brand | >= 0.17.47.RELEASE | brand 对象 JSON 文本 |
| brand.id | < 0.17.47.RELEASE | 随机 |
| brand.id | >= 0.17.47.RELEASE | brand.id 对象 JSON 文本 |
属性为多值集合,以 id 为例
{ "__hasFields": [ "id", "brand" ], "queryParams": { "page": { "no": 0, "size": 10, "skipCount": false } }, "id": { "type": "Collection", "fullMatch": false, "values": [ 894122342, 721724120, 144477230 ] }, "brand": { "__hasFields": [ "id" ], "queryParams": { "page": { "no": 0, "size": 10, "skipCount": false } }, "id": { "type": "Collection", "fullMatch": false, "values": [ 241061633, 380415043 ] } }}- 推荐用法
| keyPath | redis hash key | redis hash field |
|---|---|---|
| id.values | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | id.values:{894122342,721724120,144477230} |
| brand.id.values | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | brand.id.values:{241061633,380415043} |
- 不推荐用法一
| keyPath | redis hash key | redis hash field |
|---|---|---|
| id.value | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | id.value:null |
| itemNumber | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | itemNumber:null |
| brand.brandCode | functionCache:v3:gaia:io.terminus.trantor.example.cache.TestQModelKeyCache | brand.brandCode:null |
- 不推荐用法二
| keyPath | trantor-parent version | redis hash field |
|---|---|---|
| 未配置 | < 0.17.37.RELEASE | 随机 |
| 未配置 | >= 0.17.37.RELEASE | 入参对象 JSON 文本 |
| brand | < 0.17.47.RELEASE | 随机 |
| brand | >= 0.17.47.RELEASE | brand 对象 JSON 文本 |
| brand.id | < 0.17.47.RELEASE | 随机 |
| brand.id | >= 0.17.47.RELEASE | brand.id 对象 JSON 文本 |
问题排查指南
回顾文档开头总结的几类问题,业务开发人员能自主排查分析的也只有缓存失败和预期不符几种场景,而且排查思路类似.
排查思路:
缓存失效
针对缓存失效的问题,无非两种原因:
-
缓存穿透
- LoadingCache 计算逻辑返回
NULL,导致不对结果进行缓存; expire过期时间设置过短,导致缓存过早淘汰。
- LoadingCache 计算逻辑返回
-
缓存 key 没有命中
-
传参不一致导致缓存 key 的解析结果不一致,这种情况对于一个稳定状态的系统还是比较少见;
-
传参一致但是缓存 key 的解析结果不一致
- 入参是复杂对象,但是未定义
keyPath或multiKeyPath,导致 key 解析结果直接返回这个入参对象; - 入参是复杂对象,定义了
keyPath或multiKeyPath,但是计算缓存 key 的结果也存在复杂对象;
trantor-parent 0.17.37.RELEASE之前的版本由于对于复杂对象是直接进行toString()处理,导致返回了对象的字符串表示形式(对象的 class 名称 + @ + hashCode 的十六进制字符串),而且由于对象是经过反序列化生成,因此每次的结果也是不一致。 - 入参是复杂对象,但是未定义
-
缓存结果与业务预期不符
推测是由于缓存 key 碰撞导致,比如两条 sku 单据 SKU20210627000001 和 SKU20210616000001,解析得到同一个缓存 key,导致后者查询到了前者的缓存信息。
排查过程
首先需要了解 trantor 缓存机制在 Redis 中的存储结构是一种 Hash 结构,每个缓存的声明接口都对应于独立的一个 Hash 结构。
核心处理类是 io.terminus.trantorframework.cache.proxy.invoker.TrantorCacheHandler,内部提供了几个核心方法:
-
getHashKey
计算 Redis hash key,这个计算结构是可以在运行前可以确定的,使用
"functionCache:" + 缓存声明接口的全限定名;示例:
functionCache:v3:gaia:io.terminus.trantor.example.cache.TestBusinessModelKeyCache -
resolveCacheKeyPath
计算 Redis hash field,这个值是在运行时结合入参对象和
keyPath或multiKeyPath计算确定,在缓存开发文档里有讲解,可以结合上文的最佳实践示例加以理解。
知道上面的核心类和方法后,对于问题的排查就可以根据解决思路开展了,比如对于缓存失效的场景,我们可以借助 Arthas 工具的 watch 在线上对 resolveCacheKeyPath 方法入参、返回值、异常情况进行监控,指令如下:
watch io.terminus.trantorframework.cache.proxy.invoker.TrantorCacheHandler resolveCacheKeyPath '{params,returnObj,throwExp}' -v -n 5 -x 8 '1==1'当然,上述指令会监控所有目标方法的调用情况,在线上环境请求频繁的情况很难分别和定位我们需要的信息,所以我们可以结合业务场景,通过编写 watch 指令的 OGNL 表达式查看更详细的入参值,以及使用条件表达式对入参进行过滤,如下:
以上文的 ItemBO 为例
watch io.terminus.trantorframework.cache.proxy.invoker.TrantorCacheHandler resolveCacheKeyPath '{params[0].itemNumber,returnObj,throwExp}' -v -n 5 -x 8 "params[0].skuCode=='H4001'"复现问题,查看监控信息,可以获取到 returnObj 返回值,例如 itemNumber:H4001
[arthas@1]$ watch io.terminus.trantorframework.cache.proxy.invoker.TrantorCacheHandler resolveCacheKeyPath '{params[0].itemNumber,returnObj,throwExp}' -v -n 5 -x 8 "params[0].skuCode=='H4001'"Press Q or Ctrl+C to abort.Affect(class count: 1 , method count: 1) cost in 276 ms, listenerId: 15Condition express: params[0].skuCode=='H4001' , result: truemethod=io.terminus.trantorframework.cache.proxy.invoker.TrantorCacheHandler.resolveCacheKeyPath location=AtExitts=2021-08-11 18:09:56; [cost=0.662324ms] result=@ArrayList[ @String[ @String[H4001], ], @String[itemNumber:H4001], null,]得到我们需要的 Redis hash field,然后使用 Redis 客户端连接上 Redis server 使用 Redis 指令检查缓存是否存在和存储的信息是否符合预期,指令示例:
hget "functionCache:v3:gaia:io.terminus.trantor.example.cache.TestBusinessModelKeyCache" "itemNumber:H4001"