线上问题排查实例分析|Redis使用不同编码引发的问题

news2024/9/22 19:26:55

前言

某个周末的晚上突然收到一波耗时上升报警,仔细一看报警消息,原来是出现了慢查请求导致集群耗时大幅上升,此时业务同学也收到上游服务受影响报警。在处理问题过程中,运维同学发现 Redis 集群中只有部分实例出现 cpu 利用率上升,慢查日志也集中在这几个实例,而上游业务此时没有上线或是业务模型变化。因为是少量热 key 访问导致部分 Redis 实例负载高,执行限流对业务有损,执行扩容也无法达到快速止损的目标,幸好业务侧有提前制定的降级预案,快速沟通后采用了业务侧降级方案进行了止损。

问题回顾

虽然故障通过业务侧降级预案及时止损, 但作为 Redis 服务提供方,进行复盘弄清问题根因,制定预案是必须要做的工作,这样才能避免问题再次发生或是发生时更快止损。通过了解业务场景,发现是因为城市A大雨,排队订单上升3倍。咦?这个情景有点熟悉,以前也是这个城市在大雨时出过这个问题,这也是为什么业务侧会有一个降级预案存在。了解背景后,接下来就要接受业务同学们的灵魂拷问了。

  • 为什么昨天同一时间段,该集群 QPS 更高但是没有出现类似问题?

  • 这个集群存储了多个城市的数据,为什么只有城市A这个城市下大雨会出问题,而别的城市没有问题,难道是别的城市不下大雨?或是大家不用排队功能?

答案显然不是,一定还有技术层面的问题没有搞明白,接下来带大家回顾当时我们的排查问题思路。

  • 首先从观察故障期间的一些现象入手,我们发现如下几个现象:
    当时出现了5个热 key,都属于同一个城市A,该城市因为当天晚上大雨导致排队订单量超过平时3倍;

  • 这5个热 key 都是 hash 数据结构,里面存储的每个 key 有400多个元素;

  • 同样的业务逻辑,另外一个城市B,也是5个 hash 表,QPS 比城市A这个城市高,key 中的元素数量也比城市A要高,但是城市B反而并没有出现耗时高的问题;

  • 在出现问题的时刻,这5个热 key 上大部分命令都是 HEXISTS(作用是查看某个 field 是否在 hash 表中),此命令是O(1)复杂度。

此时脑中灵光一现,在查看热 key 的 meta 信息时,发现城市A的这5个 key 使用的内部编码是 ziplist,此时再对比一下城市B的5个 key,发现使用的 hashtable 编码,这个发现让我们似乎看到解决问题的曙光,这是目前发现的城市A和城市B最大的不同之处,后续的分析也证明了这个方向是正确的。

问题剖析

为什么 Redis 的 hash 数据结构会采用两种不同的编码方式,不同编码方式又会带来什么样的不同后果呢?

Redis 为什么内部使用不同编码?
Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。例如:SET mykey "HelloWorld"这个命令会创建两个 Redis 对象,一个字符串对象"mykey"做 key,一个字符串对象"HelloWorld"作为 value。Redis 对象的结构体如下:

typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;

在这里插入图片描述
一个 Redis 对象可以有多个类型(type), 如字符串、列表、哈希、集合、有序集合等,同时每种类型也可以设置为不同的编码方式(encoding):int、 hash(ht)、zipmap、ziplist、intset、skiplist、embstr、quicklist、 stream。通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率, 使得 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。

两种编码方式的数据结构

编码方式1: ziplist

<zlbytes><zltail><zllen><entry>...<entry><zlend>

在这里插入图片描述

编码方式2: hashtable 编码

在这里插入图片描述
为什么要使用 ziplist 编码?

  • 在哈希对象包含的元素比较少时, Redis 使用压缩列表作为列表对象的底层实现,因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到 CPU 缓存中;

  • 随着列表对象包含的元素越来越多,使用压缩列表来保存元素性能就会变差,Redis 就会把这个列表对象在底层从压缩列表编码转成 hashtable 编码,hashtable 编码是一个更适合保存大量元素的双端链表。

命令执行时对两种编码结构处理逻辑

当执行 hset/hmset 命令时,代码中会去检查哈希的对象编码以及相关条件来判断该采用哪种编码方式,大致流程如下:

1、检查 value 大小
先是在 hashTypeTryConversion() 中检查,当编码是 OBJ_ENCODING_ZIPLIST,并且写入的 value > server.hash_max_ziplist_value(我们线上默认设置为64 Bytes),会执行转码函数 hashTypeConvert()

2、检查 field 的个数
在 hashTypeSet() 中,如果编码是 OBJ_ENCODING_ZIPLIST,并且元素个数 >server.hash_max_ziplist_entries(我们线上默认设置为512个),就会执行转码函数 hashTypeConvert()

3、hashTypeConvert() 函数实现
在 hashTypeConvert()中 会调用 hashTypeConvertZiplist() 函数,先创建一个字典 dict,然后逐个复制 ziplist 中的 field 和 value 并创建对应 Redis 对象后放入 dict 中,在 ziplist 中的数据全部复制并加入 dict 后,释放原来的 ziplist,然后把 key 指向这个 dict,这样这个哈希对象的编码就从 ziplist 转换成了 hashtable。这个过程是阻塞式的,直到 hashTypeConvert 函数执行完毕,hset/hmset 命令才算完成。

源码如下:

void hashTypeConvertZiplist(robj *o, int enc) {
    serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);
 
    if (enc == OBJ_ENCODING_ZIPLIST) {
        /* Nothing to do... */
 
    } else if (enc == OBJ_ENCODING_HT) {
        hashTypeIterator *hi;
        dict *dict;
        int ret;
 
        hi = hashTypeInitIterator(o);
        dict = dictCreate(&hashDictType, NULL);
 
        while (hashTypeNext(hi) != C_ERR) {
            robj *field, *value;
 
            field = hashTypeCurrentObject(hi, OBJ_HASH_KEY);
            field = tryObjectEncoding(field);
            value = hashTypeCurrentObject(hi, OBJ_HASH_VALUE);
            value = tryObjectEncoding(value);
            ret = dictAdd(dict, field, value);
            if (ret != DICT_OK) {
                serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",
                    o->ptr,ziplistBlobLen(o->ptr));
                serverAssert(ret == DICT_OK);
            }
        }
 
        hashTypeReleaseIterator(hi);
        zfree(o->ptr);
 
        o->encoding = OBJ_ENCODING_HT;
        o->ptr = dict;
 
    } else {
        serverPanic("Unknown hash encoding");
    }
}

两种编码的时间复杂度对比

说完了一些背景知识,下面该进入真正的主题了,为什么城市A这次热 key 问题会造成业务影响,首先我们来看一下哈希对象使用 ziplist 和 hashtable 两种编码不同命令的时间复杂度:

命令ziplist 编码实现方法hashtable 编码的实现方法
HSET首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用用ziplistPush函数, 将值推入到压缩列表的表尾。时间复杂度:O(1)调用 dictAdd 函数, 将新节点添加到字典里面。时间复杂度:O(1)
HGET首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。时间复杂度:O(N)调用 dictFind 函数, 在字典中查找给定键, 然后调用 dictGetVal 函数, 返回该键所对应的值。时间复杂度:O(1)
HEXISTS调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。时间复杂度:O(N)调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。时间复杂度:O(1)
HDEL调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。时间复杂度:O(N)调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。时间复杂度:O(1)
HLEN调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。时间复杂度:O(1)调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。时间复杂度:O(1)
HGETALL遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。时间复杂度:O(N)遍历整个字典, 用 dictGetKey 函数返回字典的键, 用 dictGetVal 函数返回字典的值。时间复杂度:O(N)

注:字典 dict 结构的操作,可以简单认为是O(1),虽然如果 hash 有冲突时会多个 key
放到链表,相应的很多操作就需要遍历这个链表,但是一般这个链表不会太大,跟整个 hash 表大小比较来说可以认为O(1)。

从上面的对比不难看出:HEXISTS 命令使用 hashtable 时间复杂度为O(1),而使用 ziplist 编码为O(N),当出问题时,城市A存储的 hash 表元素为400个(采用 ziplist 编码),而城市B的 hash 表元素个数超过了512个(采用hashtable编码),这就解释了为什么访问城市A的 key 性能更差

测试方案

讲完了理论,接下来就需要用实际测试来证明上面的推论(这里仅针对 HEXISTS 一个命令做性能测试说明)。

测试方法如下:

  • 在同一个 Redis 实例分别生成两个 hash 对象:mykey001 和 mykey002,同样都是500个 filed,每个 value=60B;

  • 当压测 mykey001 时,hash-max-ziplist-entries 参数=512,编码将会使用 ziplist,压测 mykey002 时 hash-max-ziplist-entries 参数=256,因此编码为 hashtable;

  • 每轮发送相同的压力,这里测试时为2万 QPS,命令为 HEXISTS。

测试用例

测试数据: 两轮测试 QPS 是相同的
在这里插入图片描述
测试数据: P99耗时对比
在这里插入图片描述
测试数据: CPU 利用率对比
在这里插入图片描述
测试数据: 哈希对象占用内存对比

编码方式500个元素
ziplist4.3MB
hashtable19.6MB

测试结论

  • hashtable 编码时(HEXISTS命令)的P99耗时下降14%(70us -> 60us)

  • hashtable 编码时(HEXISTS命令)Redis 的 cpu 利用率下降72%(50% vs 14%)

  • hashtable 编码的内存占用是 ziplist 的4.6倍

补充说明:

  • 每个哈希对象存储500个 field,每个 field 长度8个 Byte, 每个 value 是一个整数类型的时间戳

  • 内存占用通过查看 info 中 used_memory_rss 指标统计的。这个指标其实从 /proc/$pid/stat 采集的,应该是最精确的内存占用数据了。单个对象是通过 debug object 查看,这个不太精确。

一个小插曲,之前测试时,使用 debug object 在获取 key 的内存占用量,忽略一个问题,这个命令是复用了 rdbSaveObject 这个函数,在这个函数中计算Redis 对象的长度时,如果系统参数 rdbcompression 设置为 yes,且长度超过20B就进行压缩,就导致了 debug object 输出的 serializedlength 值失真,在3.2.8版本上 ziplist 在 rdbSaveObject 函数中会进行压缩,hashtable 不会做压缩,因此 ziplist 编码时产生的 rdb 文件会小很多。在3.2.8之后的版本针对 hashtable 在 rdbSaveObject 函数中也做了压缩,产生的 rdb 文件大小相差就没那么大,这里只针对该case跑的3.2.8版本做了测试。

# 使用ziplist编码
redis-cli -p 5555 config set hash-max-ziplist-entries 512
OK
redis-cli -p 5555 config set rdbcompression yes
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:ziplist serializedlength:3692 lru:5959713 lru_seconds_idle:35
redis-cli -p 5555 config set rdbcompression no
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:ziplist serializedlength:34585 lru:5959713 lru_seconds_idle:35
 
 
 
 
# 使用ht编码
redis-cli -p 5555 config set hash-max-ziplist-entries 256
OK
redis-cli -p 5555 hset mykey002 mykey000000000000000000000001500 8c1dfc543d72c0826fc0968276932c88af7ed1deebd74d9c95c129fe6371500
(integer) 1
redis-cli -p 5555 config set rdbcompression yes
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:hashtable serializedlength:33651 lru:5959748 lru_seconds_idle:0
redis-cli -p 5555 config set rdbcompression no
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:hashtable serializedlength:33666 lru:5959748 lru_seconds_idle:

优化思路

经过上面的测试我们会发现 ziplist 在成本上会有优势,但是对于某些原来是O(1)的操作会变成O(N),如果追求性能,对成本不关注,或是只需对少数 Key 追求性能,可以从以下方面来进行优化:

1、Redis 服务端优化:
通过调整 hash-max-ziplist-entries 或是 hash-max-ziplist-value 配置项,从而控制哈希对象的编码方式,例如把 hash-max-ziplist-entries 从512改成256,那么超过256个 field 就会自动转换成 hashtable,但是要注意,如果你的哈希对象大部分都是超过256个 field,这么修改有可能会造成内存使用量大幅上升的情况,需要注意你的钱包。

2、业务侧优化方式:
在服务端做参数调整虽然简单,但是需要根据业务实际情况确定参数,且实际使用中元素数量会发生变化,调整参数没有可操作的条件,且在 key 数量多的时候还会造成不必要的成本浪费。如果是只有部分哈希对象需要关注性能,可以考虑在业务层针对单个 key 做优化。

  • 满足 hash-max-ziplist-value 条件
    例如在哈希对象中存入一个特殊 field,让这个 field 的 value 超过64B,这样这个 key 就自动变成 hashtable 编码,但是需要业务能识别这个特殊的 field,避免出现 bug。

  • 满足 hash-max-ziplist-entries 条件
    很多业务的哈希对象是经过一次拆分了,通过取模 hash 的方式拆分到多个哈希对象,也可以通过将拆分的哈希对象减少,从而达到满足单个哈希对象中 field 个数超过512个的条件。

3、业务使用建议:

  • 尽量避免热 key 现象
    我们使用的是 Redis 的 cluster 版本,如果把业务的大部分流量都集中在某个或某几个 key 上,就无法充分发挥分布式集群的作用,压力都集中在个别的 Redis 实例上,从而出现热点瓶颈。

  • 使用 list/hash/set/zset 等数据结构时要注意性能问题
    需要根据自己的单个 key 中的 field 个数,value 大小,以及使用的命令等综合考虑性能和成本,如果需要了解自己的 key 的实际编码个数可以通过命令:object encoding 查看。

结语

通过这次问题分析,我们可以看到 Redis 内部提供的不同编码会带来不同的性能和成本差别,建议大家在使用 Redis 时,也可以多了解自己的访问场景,根据实际情况来做一些调优。同时也提醒我们,时刻保持对问题根因的探究精神,才可以使我们自己的技术和业务能力不断提升。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1545829.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Rust】——提取函数消除重复代码和泛型

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

手机和键盘的数字键盘排序为什么是不同的?

不知道你有没有注意有一个问题。我们的手机输入法中的数字键盘&#xff0c;电脑上通用的数字键盘&#xff0c;计算器上的数字键盘等排序是不同的&#xff0c;从观察者角度看&#xff0c;0-9的数字排列有从上到下的排列&#xff0c;还有从下到上的排列。为什么会出现不同的排列方…

哈希冲突解决的几种方式

目录 哈希冲突 哈希冲突-避免方式1-哈希函数的设计 1. 直接定制法--(常用) 2. 除留余数法--(常用) 3. 平方取中法--(了解) 哈希冲突-避免方式2-负载因子调节 哈希冲突-解决方式1-闭散列 1.线性探测 2.二次探测 哈希冲突-解决方式2-开散列(哈希桶) 哈希冲突 在上文中…

编程语言那么多,为什么说C++无可替代?

C语言之所以没有被替代正是因为它自身的独特优势&#xff0c;尤其是在某些领域发挥着重要的作用。 先来说说C语言的优势: 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「C的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后…

el-form表单怎么一次验证两个el-form-item

项目场景&#xff1a; 在项目中有【设置密码】以及【确认密码】输入&#xff0c;希望在两者一致的时候&#xff0c;两个框的错误提示都消失。 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 重现步骤&#xff1a; 1、第一个密码框 输入密码123456lyy2、确…

HashMap集合 --java学习笔记

HashMap集合 HashMap(由键决定特点):无序、不重复、无索引 (用的最多) HashMap的底层原理 HashMap跟Hashset的底层原理是一一样的&#xff0c;都是基于哈希表实现的Hashset&#xff1a;Set系列集合&#xff1a;Hashset、LinkedHashset、TreeSet --java学习笔记-CSDN博客实际…

变压吸附制氮设备原理及行业应用概览

随着科技的不断进步&#xff0c;氮气的制备和应用在各个领域应用广泛。变压吸附制氮设备作为一种高效、节能的氮气制备技术&#xff0c;逐渐被大家所熟知。本期小编将详细介绍变压吸附制氮设备的原理及其应用。 一、变压吸附制氮设备的原理 变压吸附制氮设备主要利用分子筛的特…

Camtasia2024永久免费专业的屏幕录制和视频剪辑软件

Camtasia2024专业的屏幕录制和视频剪辑软件&#xff0c;3000多万专业人士在全球范围内使用Camtasia展示产品&#xff0c;教授课程&#xff0c;培训他人&#xff0c;以更快的速度和更吸引人的方式进行沟通和屏幕分享。使您在Windows和Mac上进行录屏和剪辑创作专业外观的视频变得…

5G双域专网+零信任的神奇魔法

引言 在当今数字化程度不断提升的社会中&#xff0c;信息安全已经成为企业和组织发展的关键要素之一。特别是在网络连接日益广泛的环境下&#xff0c;对于数据的保护和隐私的维护变得尤为重要。随着5G技术的飞速发展&#xff0c;5G双域专网为企业提供了更快速、更可靠的连接&a…

【优选算法】专题1 -- 双指针 -- 复写0

前言&#xff1a; 补充一下前文没有写到的双指针入门知识&#xff1a;专题1 -- 双指针 -- 移动零 目录 基础入门知识&#xff1a; 1. 复写零&#xff08;easy&#xff09; 1. 题⽬链接&#xff1a;1089.复习0 - 力扣&#xff08;LeetCode&#xff09; 2. 题⽬描述&#xff…

智慧城市解决方案大全:标准规范顶层设计指南、整体解决方案、厂商售前宣讲PPT、招投标、智慧城市白皮书等全套680份,一次性打包下载

关键词&#xff1a;智慧城市&#xff0c;智慧城市解决方案&#xff0c;智慧城市发展的前景与趋势&#xff0c;智慧城市概念主力流出&#xff0c;智慧城市项目包括哪些方面&#xff0c;智慧城市项目方案&#xff0c;智慧城市宣传片&#xff0c;智慧城市白皮书&#xff0c;智慧城…

海外仓系统开发可定制开发吗?大概要多少钱呢?

随着跨境电商的不断发展&#xff0c;海外仓作为跨境电商的重要环节&#xff0c;已经越来越重要了&#xff0c;而海外仓系统作为海外仓的实用性工具&#xff0c;更是有着不可替代的作用的。但每家公司的业务所需都是不一样的&#xff0c;因此越来越多企业开始关注海外仓系统的开…

零售商品计划新篇章:智能管理系统的挑战与机遇

在零售企业管理中&#xff0c;商品计划管理在零售企业运营中占据核心地位。面对日益激烈的市场竞争和消费者需求的多样化&#xff0c;零售企业在商品计划管理方面面临着诸多挑战和需求。以下针对这些挑战和需求的分析&#xff0c;以及对一套智能商品计划管理系统应具备的功能和…

EPSON推出的实时时钟模块RX8130CE功耗低至300nA、从容应对各种使用场景

随着科技的进步和消费者需求的不断变化&#xff0c;笔记本电脑市场继续展现出强劲的发展势头一方面移动性和轻薄性成为主流&#xff0c;另外一方面性能在不断提升&#xff0c;功能也日益丰富。实时时钟模组&#xff0c;作为提供时间和定时功能的单元模块&#xff0c;是笔记本电…

Vue+ELement UI el-table移入或选中某行时改变颜色

起因 出库按钮 置灰时&#xff0c;鼠标移入到表格的某行时&#xff0c;行背景颜色与按钮背景颜色会被覆盖住 最初颜色 实现效果 修改行背景颜色 <style>/* 用来设置当前页面element全局table 选中某行时的背景色*/.el-table__body tr.current-row>td{background-c…

2024年大模型面试准备(三):聊一聊大模型的幻觉问题

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学&#xff0c;针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何备战、面试常考点分享等热门话题进行了深入的讨论。 合集在这…

后端常问面经之Spring和Mybatis框架

Spring的IOC介绍一下&#xff1a; 所谓控制就是对象的创建、初始化、销毁。 创建对象&#xff1a;原来是 new 一个&#xff0c;现在是由 Spring 容器创建。 初始化对象&#xff1a;原来是对象自己通过构造器或者 setter 方法给依赖的对象赋值&#xff0c;现在是由 Spring 容器…

百能云板开启1-6层陶瓷pcb板定制服务

普通PCB通常是由铜箔和基板粘合而成&#xff0c;而基板材质大多数为玻璃纤维&#xff08;FR-4&#xff09;&#xff0c;酚醛树脂&#xff08;FR-3&#xff09;等材质&#xff0c;粘合剂通常是酚醛、环氧等。在PCB加工过程中由于热应力、化学因素、生产工艺不当等原因&#xff0…

【第十二届“泰迪杯”数据挖掘挑战赛】【2024泰迪杯】B题基于多模态特征融合的图像文本检索—解题全流程(持续更新)

2024 年(第 12 届)“泰迪杯”数据挖掘挑战赛B题 解题全流程&#xff08;持续更新&#xff09; -----基于多模态特征融合的图像文本检索 一、写在前面&#xff1a; ​ 本题的全部资料打包为“全家桶”&#xff0c; “全家桶”包含&#xff1a;模型数据、全套代码、训练好的模…

万亿功能性食品市场爆火,北美膳食健康品牌GNITE如何抓住“朋克养生”年轻人!

近几年&#xff0c;年轻人的养生意识不断提升&#xff0c;“吃出健康”理念盛行&#xff0c;在中国年轻人独有的“懒养生”理念加持下&#xff0c;功能性软糖精准击中年轻人的健康焦虑&#xff0c;助眠、美白、护眼、补铁、减脂……等产品在新消费领域兴起&#xff0c;消费热度…