中国建设银行阜阳分行网站,苏州做外贸网站,海盐网站建设,网站建设送企业邮箱吗1、String 类型的内存空间消耗问题#xff0c;以及选择节省内存开销的数据类型的解决方案。 为什么 String 类型内存开销大#xff1f; 图片 ID 和图片存储对象 ID 都是 10 位数#xff0c;我们可以用两个 8 字节的 Long 类型表示这两个 ID。因为 8 字节的 Long 类型最大可以…1、String 类型的内存空间消耗问题以及选择节省内存开销的数据类型的解决方案。 为什么 String 类型内存开销大 图片 ID 和图片存储对象 ID 都是 10 位数我们可以用两个 8 字节的 Long 类型表示这两个 ID。因为 8 字节的 Long 类型最大可以表示 2 的 64 次方的数值所以肯定可以表示 10 位数。但是为什么 String 类型却用了 64 字节呢
除了记录实际数据String 类型还需要额外的内存空间记录数据长度、空间使用等信息这些信息也叫作元数据。当实际保存的数据较小时元数据的空间开销就显得比较大了有点“喧宾夺主”的意思。 当你保存 64 位有符号整数时String 类型会把它保存为一个 8 字节的 Long 类型整数这种保存方式通常也叫作 int 编码方式。 但是当你保存的数据中包含字符时String 类型就会用简单动态字符串Simple Dynamic StringSDS结构体来保存如下图所示 可以看到在 SDS 中buf 保存实际数据而 len 和 alloc 本身其实是 SDS 结构体的额外开销。 另外对于 String 类型来说除了 SDS 的额外开销还有一个来自于 RedisObject 结构体的开销。因为 Redis 的数据类型有很多而且不同数据类型都有些相同的元数据要记录比如最后一次访问的时间、被引用的次数等所以Redis 会用一个 RedisObject 结构体来统一记录这些元数据同时指向实际数据。 一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针这个指针再进一步指向具体数据类型的实际数据所在例如指向 String 类型的 SDS 结构所在的内存地址可以看一下下面的示意图。关于 RedisObject 的具体结构细节我会在后面的课程中详细介绍现在你只要了解它的基本结构和元数据开销就行了。 为了节省内存空间Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计。 一方面当保存的是 Long 类型整数时RedisObject 中的指针就直接赋值为整数数据了这样就不用额外的指针再指向整数了节省了指针的空间开销。 另一方面当保存的是字符串数据并且字符串小于等于 44 字节时RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。 当然当字符串大于 44 字节时SDS 的数据量就开始变多了Redis 就不再把 SDS 和 RedisObject 布局在一起了而是会给 SDS 分配独立的空间并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。 因为 10 位数的图片 ID 和图片存储对象 ID 是 Long 类型整数所以可以直接用 int 编码的 RedisObject 保存。每个 int 编码的 RedisObject 元数据部分占 8 字节指针部分被直接赋值为 8 字节的整数了。此时每个 ID 会使用 16 字节加起来一共是 32 字节。但是另外的 32 字节去哪儿了呢
Redis 会使用一个全局哈希表保存所有键值对哈希表的每一项是一个 dictEntry 的结构体用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针分别指向 key、value 以及下一个 dictEntry三个指针共 24 字节如下图所示 但是这三个指针只有 24 字节为什么会占用了 32 字节呢这就要提到 Redis 使用的内存分配库 jemalloc 了。 jemalloc 在分配内存时会根据我们申请的字节数 N找一个比 N 大但是最接近 N 的 2 的幂次数作为分配的空间这样可以减少频繁分配的次数。所以在我们刚刚说的场景里dictEntry 结构就占用了 32 字节。
用什么数据结构可以节省内存 Redis 有一种底层数据结构叫压缩列表ziplist这是一种非常节省内存的结构。我们先回顾下压缩列表的构成。表头有三个字段 zlbytes、zltail 和 zllen分别表示列表长度、列表尾的偏移量以及列表中的 entry 个数。压缩列表尾还有一个 zlend表示列表结束。 压缩列表之所以能节省内存就在于它是用一系列连续的 entry 保存数据。每个 entry 的元数据包括下面几部分。这些 entry 会挨个儿放置在内存中不需要再用额外的指针进行连接这样就可以节省指针所占用的空间。
Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的集合类型这样做的最大好处就是节省了 dictEntry 的开销。当你用 String 类型时一个键值对就有一个 dictEntry要用 32 字节空间。但采用集合类型时一个 key 就对应一个集合的数据能保存的数据多了很多但也只用了一个 dictEntry这样就节省了内存。
如何用集合类型保存单值的键值对 在保存单值的键值对时可以采用基于 Hash 类型的二级编码方法。这里说的二级编码就是把一个单值的数据拆分成两部分前一部分作为 Hash 集合的 key后一部分作为 Hash 集合的 value这样一来我们就可以把单值数据保存到 Hash 集合中了。 以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例我们可以把图片 ID 的前 7 位1101000作为 Hash 类型的键把图片 ID 的最后 3 位060和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value。按照这种设计方法我在 Redis 中插入了一组图片 ID 及其存储对象 ID 的记录并且用 info 命令查看了内存开销我发现增加一条记录后内存占用只增加了 16 字
实测老师的例子**长度7位数共100万条数据。使用string占用70mb使用hash ziplist只占用9mb。**效果非常明显。redis版本6.0.6
2 有一亿个keys要统计应该用哪种集合 在 Web 和移动应用的业务场景中我们经常需要保存这样一种信息一个 key 对应了一个数据集合 手机 App 中的每天的用户登录信息一天对应一系列用户 ID 一个网页对应一系列的访问点击。在这些场景中除了记录信息我们往往还需要对集合中的数据进行统计例如 在移动应用中需要统计每天的新增用户数和第二天的留存用户数 在电商网站的商品评论中需要统计评论列表中的最新评论 通常情况下我们面临的用户数量以及访问量都是巨大的比如百万、千万级别的用户数量或者千万级别、甚至亿级别的访问信息。所以我们必须要选择能够非常高效地统计大量数据例如亿级的集合类型。 要想选择合适的集合我们就得了解常用的集合统计模式。介绍集合类型常见的四种统计模式包括聚合统计、排序统计、二值状态统计和基数统计。以刚刚提到的这四个场景为例来聊聊在这些统计模式下什么集合类型能够更快速地完成统计而且还节省内存空间。 聚合统计 所谓的聚合统计就是指统计多个集合元素的聚合结果包括统计多个集合的共有元素交集统计把两个集合相比统计其中一个集合独有的元素差集统计统计多个集合的所有元素并集统计。在刚才提到的场景中统计手机 App 每天的新增用户数和第二天的留存用户数正好对应了聚合统计。 要完成这个统计任务我们可以用一个集合记录所有登录过 App 的用户 ID同时用另一个集合记录每一天登录过 App 的用户 ID。然后再对这两个集合做聚合统计。 执行 SDIFFSTORE 命令计算累计用户 Set 和 user20200804 Set 的差集结果保存在 key 为 user:new 的 Set 中如下所示 SDIFFSTORE user:new user20200804 user:id 可以看到这个差集中的用户 ID 在 user20200804 的 Set 中存在但是不在累计用户 Set 中。所以user:new 这个 Set 中记录的就是 8 月 4 日的新增用户。 当要计算 8 月 4 日的留存用户时我们只需要再计算 user20200803 和 user20200804 两个 Set 的交集就可以得到同时在这两个集合中的用户 ID 了这些就是在 8 月 3 日登录并且在 8 月 4 日留存的用户。执行的命令如下 SINTERSTORE userrem user20200803 user20200804 Set 的差集、并集和交集的计算复杂度较高在数据量较大的情况下如果直接执行这些计算会导致 Redis 实例阻塞。所以我给你分享一个小建议你可以从主从集群中选择一个从库让它专门负责聚合计算或者是把数据读取到客户端在客户端来完成聚合统计这样就可以规避阻塞主库实例和其他从库实例的风险了。
排序统计 这就要求集合类型能对元素保序也就是说集合中的元素可以按序排列这种对元素保序的集合类型叫作有序集合。 在 Redis 常用的 4 个集合类型中List、Hash、Set、Sorted SetList 和 Sorted Set 就属于有序集合。List 是按照元素进入 List 的顺序进行排序的而 Sorted Set 可以根据元素的权重来排序我们可以自己来决定每个元素的权重值。 比如说我们可以根据元素插入 Sorted Set 的时间确定权重值先插入的元素权重小后插入的元素权重大。看起来好像都可以满足需求我们该怎么选择呢 我先说说用 List 的情况。每个商品对应一个 List这个 List 包含了对这个商品的所有评论而且会按照评论时间保存这些评论每来一个新评论就用 LPUSH 命令把它插入 List 的队头。在只有一页评论的时候我们可以很清晰地看到最新的评论但是在实际应用中网站一般会分页显示最新的评论列表一旦涉及到分页操作List 就可能会出现问题了。 List 是通过元素在 List 中的位置来排序的当有一个新元素插入时原先的元素在 List 中的位置都后移了一位比如说原来在第 1 位的元素现在排在了第 2 位。 和 List 相比Sorted Set 就不存在这个问题因为它是根据元素的实际权重来排序和获取数据的。我们可以按评论时间的先后给每条评论设置一个权重值然后再把评论保存到 Sorted Set 中。 Sorted Set 的 ZRANGEBYSCORE 命令就可以按权重排序后返回元素。这样的话即使集合中的元素频繁更新Sorted Set 也能通过 ZRANGEBYSCORE 命令准确地获取到按序排列的数据。 设越新的评论权重越大目前最新评论的权重是 N我们执行下面的命令时就可以获得最新的 10 条评论 ZRANGEBYSCORE comments N-9 N 所以在面对需要展示最新列表、排行榜等场景时如果数据更新频繁或者需要分页显示建议你优先考虑使用 Sorted Set。
二值状态统计 现在我们再来分析下第三个场景二值状态统计。这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。在签到打卡的场景中我们只用记录签到1或未签到0所以它就是非常典型的二值状态 在签到统计时每个用户一天的签到用 1 个 bit 位就能表示一个月假设是 31 天的签到情况用 31 个 bit 位就可以而一年的签到也只需要用 365 个 bit 位根本不用太复杂的集合类型。这个时候我们就可以选择 Bitmap。 SETBIT GETBIT BITCOUNT。注意是从0开始的所以SETBIT uid:sign:3000:202008 2 1 是设置8月3号已经签到。 在统计 1 亿个用户连续 10 天的签到情况时你可以把每天的日期作为 key每个 key 对应一个 1 亿位的 Bitmap每一个 bit 对应一个用户当天的签到情况。接下来我们对 10 个 Bitmap 做“与”操作得到的结果也是一个 Bitmap。最后我们可以用 BITCOUNT 统计下 Bitmap 中的 1 的个数这就是连续签到 10 天的用户总数了。 不过在实际应用时最好对 Bitmap 设置过期时间让 Redis 自动删除不再需要的签到记录以节省内存开销
基数统计 最后我们再来看一个统计场景基数统计。基数统计就是指统计一个集合中不重复的元素个数。对应到我们刚才介绍的场景中就是统计网页的 UV。网页 UV 的统计有个独特的地方就是需要去重一个用户一天内的多次访问只能算作一次。在 Redis 的集合类型中Set 类型默认支持去重所以看到有去重需求时我们可能第一时间就会想到用 Set 类型。当你需要统计 UV 时可以直接用 SCARD 命令这个命令会返回一个集合中的元素个数。 但是如果 page1 非常火爆UV 达到了千万这个时候一个 Set 就要记录千万个用户 ID。对于一个搞大促的电商网站而言这样的页面可能有成千上万个如果每个页面都用这样的一个 Set就会消耗很大的内存空间。 这时候就要用到 Redis 提供的 HyperLogLog 了。HyperLogLog 是一种用于统计基数的数据集合类型它的最大优势就在于当集合元素数量非常多时它计算基数所需的空间总是固定的而且还很小。不过有一点需要你注意一下HyperLogLog 的统计规则是基于概率完成的所以它给出的统计结果是有一定误差的标准误算率是 0.81%。
面向 LBS 应用的 GEO 数据类型 一辆车或一个用户对应一组经纬度并且随着车或用户的位置移动相应的经纬度也会变化。这种数据记录模式属于一个 key例如车 ID对应一个 value一组经纬度 当有很多车辆信息要保存时就需要有一个集合来保存一系列的 key 和 value。Hash 集合类型可以快速存取一系列的 key 和 value正好可以用来记录一系列车辆 ID 和经纬度的对应关系所以我们可以把不同车辆的 ID 和它们对应的经纬度信息存在 Hash 集合中。同时Hash 类型的 HSET 操作命令会根据 key 来设置相应的 value 值所以我们可以用它来快速地更新车辆变化的经纬度信息。到这里Hash 类型看起来是一个不错的选择。 但问题是对于一个 LBS 应用来说除了记录经纬度信息还需要根据用户的经纬度信息在车辆的 Hash 集合中进行范围查询。一旦涉及到范围查询就意味着集合中的元素需要有序但 Hash 类型的元素是无序的显然不能满足我们的要求。 Sorted Set 类型也支持一个 key 对应一个 value 的记录模式其中key 就是 Sorted Set 中的元素而 value 则是元素的权重分数。更重要的是Sorted Set 可以根据元素的权重分数排序支持范围查询。这就能满足 LBS 服务中查找相邻位置的需求了。实际上**GEO 类型的底层数据结构就是用 Sorted Set 来实现的。**这时问题来了Sorted Set 元素的权重分数是一个浮点数float 类型而一组经纬度包含的是经度和纬度两个值是没法直接保存为一个浮点数的那具体该怎么进行保存呢 这就要用到 GEO 类型中的 GeoHash 编码了。
如何定义新的数据类型 首先我们需要了解 Redis 的基本对象结构 RedisObject因为 Redis 键值对中的每一个值都是用 RedisObject 保存的。 RedisObject 的内部组成包括了 type,、encoding,、lru 和 refcount 4 个元数据以及 1 个*ptr指针。 首先我们需要为新数据类型定义好它的底层结构、type 和 encoding 属性值然后再实现新数据类型的创建、释放函数和基本命令。
如何在Redis中保存时间序列数据 在实际应用中时间序列数据通常是持续高并发写入的例如需要连续记录数万个设备的实时状态值。同时时间序列数据的写入主要就是插入新数据而不是更新一个已存在的数据也就是说一个时间序列数据被记录后通常就不会变了因为它就代表了一个设备在某个时刻的状态值。 所以这种数据的写入特点很简单就是插入数据快这就要求我们选择的数据类型在进行数据插入时复杂度要低尽量不要阻塞。看到这儿你可能第一时间会想到用 Redis 的 String、Hash 类型来保存因为它们的插入复杂度都是 O(1)是个不错的选择。但是String 类型在记录小数据时例如刚才例子中的设备温度值元数据的内存开销比较大不太适合保存大量数据。 基于 Hash 和 Sorted Set 保存时间序列数据 关于 Hash 类型我们都知道它有一个特点是可以实现对单键的快速查询。这就满足了时间序列数据的单键查询需求。我们可以把时间戳作为 Hash 集合的 key把记录的设备状态值作为 Hash 集合的 value。当我们想要查询某个时间点或者是多个时间点上的温度数据时直接使用 HGET 命令或者 HMGET 命令就可以分别获得 Hash 集合中的一个 key 和多个 key 的 value 值了。 但是Hash 类型有个短板它并不支持对数据进行范围查询。 为了能同时支持按时间戳范围的查询可以用 Sorted Set 来保存时间序列数据因为它能够根据元素的权重分数来排序。我们可以把时间戳作为 Sorted Set 集合的元素分数把时间点上记录的数据作为元素本身。 如何保证写入 Hash 和 Sorted Set 是一个原子性的操作呢 所谓“原子性的操作”就是指我们执行多个写命令操作时例如用 HSET 命令和 ZADD 命令分别把数据写入 Hash 和 Sorted Set这些命令操作要么全部完成要么都不完成。这里就涉及到了 Redis 用来实现简单的事务的 MULTI 和 EXEC 命令。当多个命令及其参数本身无误时MULTI 和 EXEC 命令可以保证执行这些命令时的原子性相当于mysql事务的begin commit 接下来我们需要继续解决第三个问题如何对时间序列数据进行聚合计算 因为 Sorted Set 只支持范围查询无法直接进行聚合计算所以我们只能先把时间范围内的数据取回到客户端然后在客户端自行完成聚合计算。为了避免客户端和 Redis 实例间频繁的大量数据传输我们可以使用 RedisTimeSeries 来保存时间序列数据 所以如果我们只需要进行单个时间点查询或是对某个时间范围查询的话适合使用 Hash 和 Sorted Set 的组合它们都是 Redis 的内在数据结构性能好稳定性高。但是如果我们需要进行大量的聚合计算同时网络带宽条件不是太好时Hash 和 Sorted Set 的组合就不太适合了。此时使用 RedisTimeSeries 就更加合适一些。
消息队列的考验Redis有哪些解决方案 现在的互联网应用基本上都是采用分布式系统架构进行设计的而很多分布式系统必备的一个基础软件就是消息队列。 消息队列的消息存取需求 不过消息队列在存取消息时必须要满足三个需求分别是消息保序、处理重复的消息消费者从消息队列读取消息时有时会因为网络堵塞而出现消息重传的情况。此时消费者可能会收到多条重复的消息。和保证消息可靠性当消费者重启后可以重新读取消息再次进行处理否则就会出现消息漏处理的问题了。。
基于 List 的消息队列解决方案 List 本身就是按先进先出的顺序对数据进行存取的所以如果使用 List 作为消息队列保存消息的话就已经能满足消息保序的需求了。 如果消费者想要及时处理消息就需要在程序中不停地调用 RPOP 命令比如使用一个 while(1) 循环。如果有新消息写入RPOP 命令就会返回结果否则RPOP 命令返回空值再继续循环。为了解决这个问题Redis 提供了 BRPOP 命令。BRPOP 命令也称为阻塞式读取客户端在没有读到队列数据时自动阻塞直到有新的数据写入队列再开始读取新数据。 消息保序的问题解决了接下来我们还需要考虑解决重复消息处理的问题这里其实有一个要求消费者程序本身能对重复消息进行判断。 一方面消息队列要能给每一个消息提供全局唯一的 ID 号另一方面消费者程序要把已经处理过的消息的 ID 号记录下来。 当消费者程序从 List 中读取一条消息后List 就不会再留存这条消息了。所以如果消费者程序在处理消息的过程出现了故障或宕机就会导致消息没有处理完成那么消费者程序再次启动后就没法再次从 List 中读取消息了。 为了留存消息List 类型提供了 BRPOPLPUSH 命令这个命令的作用是让消费者程序从一个 List 中读取消息同时Redis 会把这个消息再插入到另一个 List可以叫作备份 List留存。这样一来如果消费者程序读了消息但没能正常处理等它重启后就可以从备份 List 中重新读取消息并进行处理了。
这就要说到 Redis 从 5.0 版本开始提供的 Streams 数据类型了。和 List 相比Streams 同样能够满足消息队列的三大需求。而且它还支持消费组形式的消息读取。
其实关于 Redis 是否适合做消息队列业界一直是有争论的。很多人认为要使用消息队列就应该采用 Kafka、RabbitMQ 这些专门面向消息队列场景的软件而 Redis 更加适合做缓存。
我的看法是Redis 是一个非常轻量级的键值数据库部署一个 Redis 实例就是启动一个进程部署 Redis 集群也就是部署多个 Redis 实例。而 Kafka、RabbitMQ 部署时涉及额外的组件例如 Kafka 的运行就需要再部署 ZooKeeper。相比 Redis 来说Kafka 和 RabbitMQ 一般被认为是重量级的消息队列。