缓存有大key?你得知道的一些手段
背景:
最近系统内缓存CPU使用率一直报警,超过设置的70%报警阀值,针对此场景,需要对应解决缓存是否有大key使用问题,扫描缓存集群的大key,针对每个key做优化处理。
以下是扫描出来的大key,此处只放置了有效关键信息。
图1
大key介绍:
想要解决大key,首先我们得知道什么定义为大key。
什么是大KEY:
大key 并不是指 key 的值很大,而是 key 对应的 value 很大(非常占内存)。此处为中间件给出的定义:
大KEY带来的影响:
知道了大key的定义,那么我们也得知道大key的带来的影响:
大KEY解决手段:
1、历史key未使用
场景描述:
针对这种key场景,其实存在着历史原因,可能是伴随着某个业务下线或者不使用,往往对应实现的缓存操作代码会删除,但是对于缓存数据往往不会做任何处理,久而久之,这种脏数据会一直堆积,占用着资源。那么如果确定已经无使用,并且可以确认有持久化数据(如mysql、es等)备份的话,可以直接将对应key删除。
实例经验:
如图1上面的元素个数488649,其实整个系统查看了下,没有使用的地方,最近也没有访问,相信也是因为一直没有用到, 否则系统内一旦用了这个key来操作hgetall、smembers等,那么缓存服务应该就会不可用了。
2、元素数过多
场景描述:
针对于Set、HASH这种场景,如果元素数量超过5000就视为大的key,以上面图1为例,可以看到元素个数有的甚至达到了1万以上。针对这种的如果对应value值不大,我们可以采取平铺的形式,
实例经验:
比如系统内历史的设计是存储下每个品牌对应的名称,那么就设置了统一的key,然后不同的品牌id作为fild,操作了hSet和hGet来存储获取数据,降低查询外围服务的频率。但是随着品牌数量的增长,导致元素逐步增多,元素个数就超过了大key的预设值了。这种根据场景,我们其实存储本身只有一个品牌名称,那么我们就针对于品牌id对应加上一个统一前缀作为唯一key,采用平铺方式缓存对应数据即可。那么针对这种数据的替换,我这里也总结了下具体要实现的步骤:
修改代码查询和赋值逻辑:
历史数据刷新到新缓存key:
为了避免上线之后出现缓存雪崩,因为替换了新的key,我们需要通过现有的HASH的数据刷新到新的缓存中,所以需要历史数据处理。
通过hGetAll获取所以元素数据
循环缓存元素数据操作存储新的缓存key和value。
public String refreshHistoryData(){
try {
String key = "historyKey";
Map<String, String> redisInfoMap= redisUtils.hGetAll(key);
if (redisInfoMap.isEmpty()){
return "查询缓存无数据";
}
for (Map.Entry<String, String> entry : redisInfoMap.entrySet()) {
String redisVal = entry.getValue();
String filedKey = entry.getKey();
String newDataRedisKey = "newDataKey"+filedKey;
redisUtils.set(newDataRedisKey,redisVal);
}
return "success";
}catch (Exception e){
LOG.error("refreshHistoryData 异常:",e);
}
return "failed";
}
注意:这里一定要先刷历史数据,再上线代码业务逻辑的修改。防止引发 缓存雪崩
3、大对象转换存储形式
场景描述:
复杂的大对象可以尝试将对象分拆成几个key-value, 使用mGet和mSet操作对应值或者pipeline的形式,最后拼装成需要返回的大对象。这样意义在于可以分散单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
实例经验:
这里以系统内订单对象为例:订单对象Order基础属性有几十个,如订单号、金额、时间、类型等,除此之外还要包含订单下的商品OrderSub、预售信息PresaleOrder、发票信息OrderInvoice、订单时效OrderPremiseInfo、订单轨迹OrderTrackInfo、订单详细费用OrderFee等信息。
那么对于每个订单相关信息,我们可以设置为单独的key,把订单信息和几个相关的关联数据每个按照单独key存储,接着通过mGet方式获取每个信息之后,最后封装成整体Order对象。下面仅展示关键伪代码以mSet和mGet实现:
缓存定义:
public enum CacheKeyConstant {
/**
* 订单基础缓存key
*/
REDIS_ORDER_BASE_INFO("ORDER_BASE_INFO"),
/**
* 订单商品缓存key
*/
ORDER_SUB_INFO("ORDER_SUB_INFO"),
/**
* 订单预售信息缓存key
*/
ORDER_PRESALE_INFO("ORDER_PRESALE_INFO"),
/**
* 订单履约信息缓存key
*/
ORDER_PREMISE_INFO("ORDER_PREMISE_INFO"),
/**
* 订单发票信息缓存key
*/
ORDER_INVOICE_INFO("ORDER_INVOICE_INFO"),
/**
* 订单轨迹信息缓存key
*/
ORDER_TRACK_INFO("ORDER_TRACK_INFO"),
/**
* 订单详细费用信息缓存key
*/
ORDER_FEE_INFO("ORDER_FEE_INFO"),
;
/**
* 前缀
*/
private String prefix;
/**
* 项目统一前缀
*/
public static final String COMMON_PREFIX = "XXX";
CacheKeyConstant(String prefix){
this.prefix = prefix;
}
public String getPrefix(String subKey) {
if(StringUtil.isNotEmpty(subKey)){
return COMMON_PREFIX + prefix + "_" + subKey;
}
return COMMON_PREFIX + prefix;
}
public String getPrefix() {
return COMMON_PREFIX + prefix;
}
}
缓存存储:
/**
* @description 刷新订单到缓存
* @param order 订单信息
*/
public boolean refreshOrderToCache(Order order){
if(order == null || order.getOrderId() == null){
return ;
}
String orderId = order.getOrderId().toString();
//设置存储缓存数据
Map<String,String> cacheOrderMap = new HashMap<>(16);
cacheOrderMap.put(CacheKeyConstant.ORDER_BASE_INFO.getPrefix(orderId), JSON.toJSONString(buildBaseOrderVo(order)));
cacheOrderMap.put(CacheKeyConstant.ORDER_SUB_INFO.getPrefix(orderId), JSON.toJSONString(order.getCustomerOrderSubs()));
cacheOrderMap.put(CacheKeyConstant.ORDER_PRESALE_INFO.getPrefix(orderId), JSON.toJSONString(order.getPresaleOrderData()));
cacheOrderMap.put(CacheKeyConstant.ORDER_INVOICE_INFO.getPrefix(orderId), JSON.toJSONString(order.getOrderInvoice()));
cacheOrderMap.put(CacheKeyConstant.ORDER_TRACK_INFO.getPrefix(orderId), JSON.toJSONString(order.getOrderTrackInfo()));
cacheOrderMap.put(CacheKeyConstant.ORDER_PREMISE_INFO.getPrefix(orderId), JSON.toJSONString( order.getPresaleOrderData()));
cacheOrderMap.put(CacheKeyConstant.ORDER_FEE_INFO.getPrefix(orderId), JSON.toJSONString(order.getOrderFeeVo()));
superRedisUtils.mSetString(cacheOrderMap);
}
缓存获取:
/**
* @description 通过订单号获取缓存数据
* @param orderId 订单号
* @return Order 订单实体信息
*/
public Order getOrderFromCache(String orderId){
if(StringUtils.isBlank(orderId)){
return null;
}
//定义查询缓存集合key
List<String> queryOrderKey = Arrays.asList(CacheKeyConstant.ORDER_BASE_INFO.getPrefix(orderId),CacheKeyConstant.ORDER_SUB_INFO.getPrefix(orderId),
CacheKeyConstant.ORDER_PRESALE_INFO.getPrefix(orderId),CacheKeyConstant.ORDER_INVOICE_INFO.getPrefix(orderId),CacheKeyConstant.ORDER_TRACK_INFO.getPrefix(orderId),
CacheKeyConstant.ORDER_PREMISE_INFO.getPrefix(orderId),CacheKeyConstant.ORDER_FEE_INFO.getPrefix(orderId));
//查询结果
List<String> result = redisUtils.mGet(queryOrderKey);
//基础信息
if(CollectionUtils.isEmpty(result)){
return null;
}
String[] resultInfo = result.toArray(new String[0]);
//基础信息
if(StringUtils.isBlank(resultInfo[0])){
return null;
}
BaseOrderVo baseOrderVo = JSON.parseObject(resultInfo[0],BaseOrderVo.class);
Order order = coverBaseOrderVoToOrder(baseOrderVo);
//订单商品
if(StringUtils.isNotBlank(resultInfo[1])){
List<OrderSub> orderSubs =JSON.parseObject(result.get(1), new TypeReference<List<OrderSub>>(){});
order.setCustomerOrderSubs(orderSubs);
}
//订单预售
if(StringUtils.isNotBlank(resultInfo[2])){
PresaleOrderData presaleOrderData = JSON.parseObject(resultInfo[2],PresaleOrderData.class);
order.setPresaleOrderData(presaleOrderData);
}
//订单发票
if(StringUtils.isNotBlank(resultInfo[3])){
OrderInvoice orderInvoice = JSON.parseObject(resultInfo[3],OrderInvoice.class);
order.setOrderInvoice(orderInvoice);
}
//订单轨迹
if(StringUtils.isNotBlank(resultInfo[5])){
OrderTrackInfo orderTrackInfo = JSON.parseObject(resultInfo[5],OrderTrackInfo.class);
order.setOrderTrackInfo(orderTrackInfo);
}
//订单履约信息
if(StringUtils.isNotBlank(resultInfo[6])){
List<OrderPremiseInfo> orderPremiseInfos =JSON.parseObject(result.get(6), new TypeReference<List<OrderPremiseInfo>>(){});
order.setPremiseInfos(orderPremiseInfos);
}
//订单费用明细信息
if(StringUtils.isNotBlank(resultInfo[7])){
OrderFeeVo orderFeeVo = JSON.parseObject(resultInfo[7],OrderFeeVo.class);
order.setOrderFeeVo(orderFeeVo);
}
return order;
}
注意:获取缓存的结果跟传入的key的顺序保持对应即可。
缓存util方法封装:
/**
*
* @description 同时将多个 key-value (域-值)对设置到缓存中。
* @param mappings 需要插入的数据信息
*/
public void mSetString(Map<String, String> mappings) {
CallerInfo callerInfo = Ump.methodReg(UmpKeyConstants.REDIS.REDIS_STATUS_READ_MSET);
try {
redisClient.getClientInstance().mSetString(mappings);
} catch (Exception e) {
Ump.funcError(callerInfo);
}finally {
Ump.methodRegEnd(callerInfo);
}
}
/**
*
* @description 同时将多个key的结果返回。
* @param queryKeys 查询的缓存key集合
*/
public List<String> mGet(List<String> queryKeys) {
CallerInfo callerInfo = Ump.methodReg(UmpKeyConstants.REDIS.REDIS_STATUS_READ_MGET);
try {
return redisClient.getClientInstance().mGet(queryKeys.toArray(new String[0]));
} catch (Exception e) {
Ump.funcError(callerInfo);
}finally {
Ump.methodRegEnd(callerInfo);
}
return new ArrayList<String>(queryKeys.size());
}
这里附上通过pipeline的util封装,可参考。
/**
* @description pipeline放松查询数据
* @param redisKeyList
* @return java.util.List<java.lang.String>
*/
public List<String> getValueByPipeline(List<String> redisKeyList) {
if(CollectionUtils.isEmpty(redisKeyList)){
return null;
}
List<String> resultInfo = new ArrayList<>(redisKeyList);
CallerInfo callerInfo = Ump.methodReg(UmpKeyConstants.REDIS.REDIS_STATUS_READ_GET);
try {
PipelineClient pipelineClient = redisClient.getClientInstance().pipelineClient();
//添加批量查询任务
List<JimFuture> futures = new ArrayList<>();
redisKeyList.forEach(redisKey -> {
futures.add(pipelineClient.get(redisKey.getBytes()));
});
//处理查询结果
pipelineClient.flush();
//可以等待future的返回结果,来判断命令是否成功。
for (JimFuture future : futures) {
resultInfo.add(new String((byte[])future.get()));
}
} catch (Exception e) {
log.error("getValueByPipeline error:",e);
Ump.funcError(callerInfo);
return new ArrayList<>(redisKeyList.size());
}finally {
Ump.methodRegEnd(callerInfo);
}
return resultInfo;
}
注意:Pipeline不建议用来设置缓存值,因为本身不是原子性的操作。
4、压缩存储数据
压缩方法结果:
单个元素时:
| 压缩方法 | 压缩前大小Byte | 压缩后大小Byte | 压缩耗时 | 解压耗时 | 压缩解压后比对结果 |
| DefaultOutputStream | 446(0.43kb) | 254 (0.25kb) | 1ms | 0ms | 相同 |
| GzipOutputStream | 446(0.43kb) | 266 (0.25kbM) | 1ms | 1ms | 相同 |
| ZlibCompress | 446(0.43kb) | 254 (0.25kb) | 1ms | 0ms | 相同 |
四百个元素集合:
| 压缩方法 | 压缩前大小Byte | 压缩后大小Byte | 压缩耗时 | 解压耗时 | 压缩解压后比对结果 |
| DefaultOutputStream | 6732(6.57kb) | 190 (0.18kb) | 2ms | 0ms | 相同 |
| GzipOutputStream | 6732(6.57kb) | 202 (0.19kb) | 1ms | 1ms | 相同 |
| ZlibCompress | 6732(6.57kb) | 190 (0.18kb) | 1ms | 0ms | 相同 |
四万个元素集合时:
| 压缩方法 | 压缩前大小Byte | 压缩后大小Byte | 压缩耗时 | 解压耗时 | 压缩解压后比对结果 |
| DefaultOutputStream | 640340(625kb) | 1732 (1.69kb) | 37ms | 2ms | 相同 |
| GzipOutputStream | 640340(625kb) | 1744 (1.70kb) | 11ms | 3ms | 相同 |
| ZlibCompress | 640340(625kb) | 1732 (1.69kb) | 69ms | 2ms | 相同 |
压缩代码样例
DefaultOutputStream
public static byte[] compressToByteArray(String text) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Deflater deflater = new Deflater();
DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(outputStream, deflater);
deflaterOutputStream.write(text.getBytes());
deflaterOutputStream.close();
return outputStream.toByteArray();
}
public static String decompressFromByteArray(byte[] bytes) throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
Inflater inflater = new Inflater();
InflaterInputStream inflaterInputStream = new InflaterInputStream(inputStream, inflater);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inflaterInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
inflaterInputStream.close();
outputStream.close();
byte[] decompressedData = outputStream.toByteArray();
return new String(decompressedData);
}
GZIPOutputStream
public static byte[] compressGzip(String str) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = null;
try {
gzipOutputStream = new GZIPOutputStream(outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
gzipOutputStream.write(str.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
gzipOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return outputStream.toByteArray();
}
public static String decompressGzip(byte[] compressed) throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(compressed);
GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = gzipInputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
gzipInputStream.close();
outputStream.close();
return outputStream.toString("UTF-8");
}
ZlibCompress
public byte[] zlibCompress(String message) throws Exception {
String chatacter = "UTF-8";
byte[] input = message.getBytes(chatacter);
BigDecimal bigDecimal = BigDecimal.valueOf(0.25f);
BigDecimal length = BigDecimal.valueOf(input.length);
byte[] output = new byte[input.length + 10 + new Double(Math.ceil(Double.parseDouble(bigDecimal.multiply(length).toString()))).intValue()];
Deflater compresser = new Deflater();
compresser.setInput(input);
compresser.finish();
int compressedDataLength = compresser.deflate(output);
compresser.end();
return Arrays.copyOf(output, compressedDataLength);
}
public static String zlibInfCompress(byte[] data) {
String s = null;
Inflater decompresser = new Inflater();
decompresser.reset();
decompresser.setInput(data);
ByteArrayOutputStream o = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!decompresser.finished()) {
int i = decompresser.inflate(buf);
o.write(buf, 0, i);
}
s = o.toString("UTF-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
o.close();
} catch (IOException e) {
e.printStackTrace();
}
}
decompresser.end();
return s;
}
可以看到压缩效率比较好,压缩效率可以从几百kb压缩到几kb内;当然也是看具体场景。不过这里就是最好是避免调用量大的场景使用,毕竟解压和压缩数据量大会比较耗费cpu性能。如果是黄金链路使用,还需要具体配合压测,对比前后接口性能。
5、替换存储方案
如果数据量庞大,那么其实本身是不是就不太适合redis这种缓存存储了。可以考虑es或者mongo这种文档式存储结构,存储大的数据格式。
总结:
redis缓存的使用是一个支持业务和功能高并发的很好的使用方案,但是随着使用场景的多样性以及数据的增加,可能逐渐的会出现大key,日常使用中都可以注意以下几点:
以上是我根据现有实际场景总结出的一些解决手段,记录了这些大key的优化经验,希望可以在日常场景中帮助到大家。大家有其他的好的经验,也可以分享出来。
关注公众号
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
美国以俄罗斯安全风险为由封禁卡巴斯基软件
美国政府宣布计划禁止在美国销售俄罗斯卡巴斯基实验室生产的杀毒软件,并要求使用该软件的美国人转用其他供应商。美国商务部工业和安全局表示,该局实施了“首例”禁令,理由是卡巴斯基总部位于俄罗斯,威胁到美国国家安全和用户隐私。 路透社援引知情人士消息称,该软件对计算机系统的特权访问权限可能使其能够从美国计算机中窃取敏感信息或安装恶意软件并阻止关键更新,从而加剧威胁,并指出卡巴斯基的客户涵盖了美国的一些关键基础设施提供商以及州和地方政府。 美国商务部长吉娜·雷蒙多在与记者的电话会议中表示:“俄罗斯已经表明,它有能力,甚至有意图利用卡巴斯基等俄罗斯公司收集美国人的个人信息并将其作为武器。这就是我们不得不采取今天行动的原因。” 卡巴斯基将从7月20日起被禁止向美国消费者和企业销售其软件,但该公司将能够在9月29日之前向现有客户提供软件和安全更新。雷蒙多表示,此后,卡巴斯基将不再被允许向美国客户推送软件更新。 “这意味着你的软件和服务质量会下降。因此我强烈建议你立即寻找卡巴斯基的替代品。继续使用或拥有卡巴斯基产品和服务的美国个人和企业并没有违反法律,你们没有做错任何事,也不会受到任何刑事或民事处罚。然...
-
下一篇
从 Docker Hub 拉取镜像受阻?这些解决方案帮你轻松应对
最近一段时间 Docker 镜像一直是 Pull 不下来的状态,感觉除了挂🪜,想直连 Docker Hub 是几乎不可能的。更糟糕的是,很多原本可靠的国内镜像站,例如一些大厂和高校运营的,也陆续关停了,这对我们这些个人开发者和中小企业来说是挺难受的。之前,通过这些镜像站,我们可以快速、方便地获取所需的 Docker 镜像,现在这条路也不行了。感觉这次动作不小,以后想直接访问 Docker Hub 是不可能了。所以我们得想办法搭建自己的私有镜像仓库。 最近网上有很多解决 Docker Hub 镜像拉不下来的文章,我大概总结一下有以下几种办法: Github Action 利用 Github Action Job 将 Docker Hub 镜像重新打 Tag 推送到阿里云等其他公有云镜像仓库里,这对于需要单个镜像很方便,批量就稍微麻烦一些,如果没🪜Github 访问也是个问题。 CloudFlare Worker 使用 CloudFlare Worker 对 Docker Hub 的访问请求做中转,这种也是最近使用比较多的,因为个人用户的免费计划每天有10万次免费请求,足够个人和中小企...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Crontab安装和使用
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker快速安装Oracle11G,搭建oracle11g学习环境

微信收款码
支付宝收款码