精华内容
下载资源
问答
  • 一个相反数是它本身这个数是
    千次阅读
    2020-09-17 01:55:22

    python实现:将一个四位数反向输出

    massage = '''
    将一个四位数,反向输出
    '''
    
    N = input()
    print(N[::-1])
    # 输入:	1245
    # 输出 :5421
    
    更多相关内容
  • 、题文计算机中常用的十六进制是种逢进的计数制,采用数字0到9和字母A到F共计数符号,这些符号与十进制的数字的对应关系如下表:十六进制01234567十进制01234567十六进制89ABCDEF十进制89101112131415例如,用...

    一、题文

    计算机中常用的十六进制是一种逢

    85bd933103b0ce6d5be60cf2f18c9c46.png

    0494b8b16ffc98249f3803182c790d9e.png的计数制,采用数字0到9和字母A到F共

    85bd933103b0ce6d5be60cf2f18c9c46.png个计数符号,这些符号与十进制的数字的对应关系如下表:

    十六进制01234567

    十进制01234567

    十六进制89ABCDEF

    十进制89101112131415例如,用十六进制表示

    b88d82425bc94358dcaf35c726233286.png,用十进制表示也就是13+14=1×16+11。则

    29d0fff920b46e9a2be683dc6279ae27.png用十六进制表示应为  

    A

    6ce07e1e3319a08830a221643c06c5ec.png       B 

    55bde23437efc110b8cf45f4e3f82e44.png      C 

    b7395b3da22082741399efbcb9d04d4c.png      D 

    49539f293a380b3a40d7313af5e843f3.png

    考点提示:有理数定义及分类,正数与负数,数轴,相反数

    二、答案

    A试题分析:根据题意,

    29d0fff920b46e9a2be683dc6279ae27.png用十进制表示=

    5ad6c50f855ad409998e86bcaf9dcd88.png;用十六进制表示应为  

    3ab7b9323cadb65de704746752bd1919.png

    点评:本题考查十六进制,对十六进制和十进制的概念的掌握是解本题的关键,属创新题

    三、考点梳理

    知名教师分析,《计算机中常用的十六进制是一种逢进的计数制,采用数字0到9和字母A到F共个计数符号,这些符号与十进制的数字的对应关系如下表:十六进制01234567十进制01234567十六进制89ABCD》这道题主要考你对

    关于这些知识点的“解析掌握知识”如下:

    知识点名称:有理数定义及分类,正数与负数,数轴,相反数

    考点名称:有理数定义及分类有理数的定义:

    有理数是整数和分数的统称,一切有理数都可以化成分数的形式。

    有理数的分类:(1)按有理数的定义:

    正整数

    整数{     零

    负整数

    有理数{

    正分数

    分数{

    负分数

    (2)按有理数的性质分类:

    正整数

    正数{

    正分数

    有理数{  零

    负整数

    负数{

    负分数考点名称:正数与负数正数:就是大于0的(实数)

    负数:

    就是小于0的(实数)

    0既不是正数也不是负数。

    非负数:正数与零的统称。

    非正数:负数与零的统称。

    正负数的认识:

    1.对于正数和负数的概念,不能简单的理解为:带“+”号的数是正数,带“-”号的数是负数。

    例如:-a一定是负数吗?

    答案是不一定,因为字母a可以表示任意的数。

    若a表示正数时,-a是负数;

    当a表示0时,-a就是在0的前面加一个负号,仍是0,0不分正负;

    当a表示负数时,-a就不是负数了,它是一个正数。

    2.引入负数后,数的范围扩大为有理数,奇数和偶数的外延也由自然数扩大为整数,整数也可以分为奇数和偶数两类,能被2整除的数是偶数,

    如…-6,-4,-2,0,2,4,6…,不能被2整除的数是奇数,如…-5,-4,-2,1,3,5…

    3.数细分有五类:正整数、正分数、0、负整数、负分数;

    但研究问题时,通常把有理数分为三类:正数、0、负数,进行讨论。

    4.通常把正数和0统称为非负数,负数和0统称为非正数,正整数和0称为非负整数;

    负整数和0统称为非正整数。考点名称:数轴数轴定义:

    规定了唯一的原点,正方向和单位长度的一条直线叫做数轴。

    数轴具有三要素:

    原点、正方向和单位长度,三者缺一不可。

    数轴是直线,可以向两方无限延伸,因此所有的有理数都可用数轴上的点来表示。

    用数轴上的点表示有理数:

    每一个有理数都可用数轴上的点来表示,表示正数的点在数轴原点的右边,表示负数的点在数轴原点的左边,原点表示数0。

    1.数轴上的点表示的数不一定都是有理数,还可能是无理数,但有理数都可用数轴上的点来表示。

    2.表示正数的点都在原点右边,表示负数的点都在原点左边。

    3.数轴上的点表示的数,右边的点表示的数总比左边的点表示的数大,因此,可借助数轴比较有理数的大小。

    数轴的画法:

    1.画一条直线(一般画成水平的直线);

    2.在直线上根据需要选取一点为原点(在原点下面标上“0”);

    3.确定正方向(一般规定向右为正,并用箭头表示出来);

    4.选取适当的长度为单位长度,

    从原点向右,每隔一个单位长度取一点,依次表示1,2,3,…;

    从原点向左,用类似的方法依次表示-1,-2,-3,…。

    数轴的应用范畴:符号相反的两个数互为相反数,零的相反数是零。(如2的相反—2)

    在数轴上离开原点的距离就叫做这个数的绝对值。一个正数的绝对值是它本身,一个负数的相反数是它的正数,0的绝对值是0。考点名称:相反数相反数的定义:

    像2和-2,5和-5这样,只有符号不同的两个数叫做互为相反数。

    相反数的几何意义:在数轴上到原点距离相等的两个点表示的两个数叫做互为相反数。

    相反数的代数意义:如果两个数的和为零,其中一个数是另一个数的相反数,这两个数称为互为相反数。

    相反数的特性:1、若a,b互为相反数,则a+b=0; 反之,若a+b=0,则a,b互为相反数;

    2、在数轴上,互为相反数(0除外)的两个点位于原点的两旁,并且关于原点对称;

    3、此时,b的相反数为﹣b=﹣(﹣a)=a,那么我们就说“相反数具有互称性”。

    4、相反数的规律:正数的相反数是负数,负数的相反数是正数,0的相反数是0。

    5、相反数的表示方法:a的相反数是-a,-a的相反数是a;a-b的相反数是b-a,b-a的相反数是a-b;a+b的相反数是-(a+b),即-a-b。

    (互为)相反数的代数意义:

    1、只有符号不同的两个数称互为相反数。a和-a是一对互为相反数,a叫做-a的相反数,-a叫做a的相反数。注意:-a不一定是负数。a不一定是正数。(a不等于0)

    2、若两个实数a和b满足b=﹣a。我们就说b是a的相反数。

    3、两个互为相反数的实数a和b必满足a+b=0。也可以说实数a和b满足a+b=0,则这两个实数a,b互为相反数。

    相反数的判别:

    我们在利用相反数的概念进行化简时,很多情况下,把括号里的部分看成一个整体(即想象成一个数a),问题就容易解决。因此要求一个数的相反数,只要在这个数前面叫上“-”,再化简即可。

    多重符号的化简:

    1、在一个数前面添加一个“+”好,所得的数与原数相同。

    2、在一个数前面添加一个“-”号,所得的数就成为原数的相反数。

    3、对于有三个火三个以上符号的数的化简,首先要注意,一个数前面不管有多少个“+”号,可以把正号去掉,其次要看“-”号的个数,当“-”号的个数为偶数个时,结果取正,当“-”号的个数为奇数个时,结果取“-”号。

    本文来自投稿,不代表本站立场,如若转载,请注明出处:https://www.planabc.net/shuxue/594977.html

    展开全文
  • 按订单ID拆分则正相反,优点是数据分布均匀,不会出现一个数据库数据极大或极小的情况,缺点是数据太分散,不利于做聚合查询。比如,按订单ID拆分后,一个商家的订单可能分布在不同的数据库中,查询一个商家的所有...

    大家好,我是冰河~~

    很多小伙伴留言说让我写一些工作过程中的真实案例,写些啥呢?想来想去,写一篇我在以前公司从零开始到用户超千万的数据库架构升级演变的过程吧。

    本文记录了我之前初到一家创业公司,从零开始到用户超千万,系统压力暴增的情况下是如何一步步优化MySQL数据库的,以及数据库架构升级的演变过程。升级的过程极具技术挑战性,也从中收获不少。希望能够为小伙伴们带来实质性的帮助。

    如果文章对大家有点帮助,小伙伴们点赞,收藏,评论,分享走起呀~~

    业务背景

    我之前呆过一家创业工作,是做商城业务的,商城这种业务,表面上看起来涉及的业务简单,包括:用户、商品、库存、订单、购物车、支付、物流等业务。但是,细分下来,还是比较复杂的。这其中往往会牵扯到很多提升用户体验的潜在需求。例如:为用户推荐商品,这就涉及到用户的行为分析和大数据的精准推荐。如果说具体的技术的话,那肯定就包含了:用户行为日志埋点、采集、上报,大数据实时统计分析,用户画像,商品推荐等大数据技术。

    公司的业务增长迅速,仅仅2年半不到的时间用户就从零积累到千万级别,每天的访问量几亿次,高峰QPS高达上万次每秒。数据的写压力来源于用户下单,支付等操作,尤其是赶上双十一大促期间,系统的写压力会成倍增长。然而,读业务的压力会远远大于写压力,据不完全统计,读业务的请求量是写业务的请求量的50倍左右。

    接下来,我们就一起来看看数据库是如何升级的。

    最初的技术选型

    作为创业公司,最重要的一点是敏捷,快速实现产品,对外提供服务,于是我们选择了公有云服务,保证快速实施和可扩展性,节省了自建机房等时间。整体后台采用的是Java语言进行开发,数据库使用的MySQL。整体如下图所示。
    在这里插入图片描述

    读写分离

    随着业务的发展,访问量的极速增长,上述的方案很快不能满足性能需求。每次请求的响应时间越来越长,比如用户在H5页面上不断刷新商品,响应时间从最初的500毫秒增加到了2秒以上。业务高峰期,系统甚至出现过宕机。在这生死存亡的关键时刻,通过监控,我们发现高期峰MySQL CPU使用率已接近80%,磁盘IO使用率接近90%,slow query(慢查询)从每天1百条上升到1万条,而且一天比一天严重。数据库俨然已成为瓶颈,我们必须得快速做架构升级。

    当Web应用服务出现性能瓶颈的时候,由于服务本身无状态,我们可以通过加机器的水平扩展方式来解决。 而数据库显然无法通过简单的添加机器来实现扩展,因此我们采取了MySQL主从同步和应用服务端读写分离的方案。

    MySQL支持主从同步,实时将主库的数据增量复制到从库,而且一个主库可以连接多个从库同步。利用此特性,我们在应用服务端对每次请求做读写判断,若是写请求,则把这次请求内的所有DB操作发向主库;若是读请求,则把这次请求内的所有DB操作发向从库,如下图所示。

    在这里插入图片描述

    实现读写分离后,数据库的压力减少了许多,CPU使用率和IO使用率都降到了5%以内,Slow Query(慢查询)也趋近于0。主从同步、读写分离给我们主要带来如下两个好处:

    • 减轻了主库(写)压力:商城业务主要来源于读操作,做读写分离后,读压力转移到了从库,主库的压力减小了数十倍。

    • 从库(读)可水平扩展(加从库机器):因系统压力主要是读请求,而从库又可水平扩展,当从库压力太时,可直接添加从库机器,缓解读请求压力。

    当然,没有一个方案是万能的。读写分离,暂时解决了MySQL压力问题,同时也带来了新的挑战。业务高峰期,用户提交完订单,在我的订单列表中却看不到自己提交的订单信息(典型的read after write问题);系统内部偶尔也会出现一些查询不到数据的异常。通过监控,我们发现,业务高峰期MySQL可能会出现主从复制延迟,极端情况,主从延迟高达数秒。这极大的影响了用户体验。

    那如何监控主从同步状态?在从库机器上,执行show slave status,查看Seconds_Behind_Master值,代表主从同步从库落后主库的时间,单位为秒,若主从同步无延迟,这个值为0。MySQL主从延迟一个重要的原因之一是主从复制是单线程串行执行(高版本MySQL支持并行复制)。

    那如何避免或解决主从延迟?我们做了如下一些优化:

    • 优化MySQL参数,比如增大innodb_buffer_pool_size,让更多操作在MySQL内存中完成,减少磁盘操作。
    • 使用高性能CPU主机。
    • 数据库使用物理主机,避免使用虚拟云主机,提升IO性能。
    • 使用SSD磁盘,提升IO性能。SSD的随机IO性能约是SATA硬盘的10倍甚至更高。
    • 业务代码优化,将实时性要求高的某些操作,强制使用主库做读操作。
    • 升级高版本MySQL,支持并行主从复制。

    垂直分库

    读写分离很好的解决了读压力问题,每次读压力增加,可以通过加从库的方式水平扩展。但是写操作的压力随着业务爆发式的增长没有得到有效的缓解,比如用户提交订单越来越慢。通过监控MySQL数据库,我们发现,数据库写操作越来越慢,一次普通的insert操作,甚至可能会执行1秒以上。

    另一方面,业务越来越复杂,多个应用系统使用同一个数据库,其中一个很小的非核心功能出现延迟,常常影响主库上的其它核心业务功能。这时,主库成为了性能瓶颈,我们意识到,必需得再一次做架构升级,将主库做拆分,一方面以提升性能,另一方面减少系统间的相互影响,以提升系统稳定性。这一次,我们将系统按业务进行了垂直拆分。如下图所示,将最初庞大的数据库按业务拆分成不同的业务数据库,每个系统仅访问对应业务的数据库,尽量避免或减少跨库访问。
    在这里插入图片描述

    垂直分库过程,我们也遇到不少挑战,最大的挑战是:不能跨库join,同时需要对现有代码重构。单库时,可以简单的使用join关联表查询;拆库后,拆分后的数据库在不同的实例上,就不能跨库使用join了。

    例如,通过商家名查询某个商家的所有订单,在垂直分库前,可以join商家和订单表做查询,也可以直接使用子查询,如下如示:

    select * from tb_order where supplier_id in (select id from supplier where name=’商家名称’)

    分库后,则要重构代码,先通过商家名查询商家id,再通过商家id查询订单表,如下所示:

    select id from supplier where name=’商家名称’
    select * from tb_order where supplier_id in (supplier_ids )
    

    垂直分库过程中的经验教训,使我们制定了SQL最佳实践,其中一条便是程序中禁用或少用join,而应该在程序中组装数据,让SQL更简单。一方面为以后进一步垂直拆分业务做准备,另一方面也避免了MySQL中join的性能低下的问题。

    经过近十天加班加点的底层架构调整,以及业务代码重构,终于完成了数据库的垂直拆分。拆分之后,每个应用程序只访问对应的数据库,一方面将单点数据库拆分成了多个,分摊了主库写压力;另一方面,拆分后的数据库各自独立,实现了业务隔离,不再互相影响。

    水平分库

    读写分离,通过从库水平扩展,解决了读压力;垂直分库通过按业务拆分主库,缓存了写压力,但系统依然存在以下隐患:

    • 单表数据量越来越大。如订单表,单表记录数很快就过亿,超出MySQL的极限,影响读写性能。
    • 核心业务库的写压力越来越大,已不能再进一次垂直拆分,此时的系统架构中,MySQL 主库不具备水平扩展的能力。

    此时,我们需要对MySQL进一步进行水平拆分。

    在这里插入图片描述

    水平分库面临的第一个问题是,按什么逻辑进行拆分。一种方案是按城市拆分,一个城市的所有数据在一个数据库中;另一种方案是按订单ID平均拆分数据。按城市拆分的优点是数据聚合度比较高,做聚合查询比较简单,实现也相对简单,缺点是数据分布不均匀,某些城市的数据量极大,产生热点,而这些热点以后可能还要被迫再次拆分。按订单ID拆分则正相反,优点是数据分布均匀,不会出现一个数据库数据极大或极小的情况,缺点是数据太分散,不利于做聚合查询。比如,按订单ID拆分后,一个商家的订单可能分布在不同的数据库中,查询一个商家的所有订单,可能需要查询多个数据库。针对这种情况,一种解决方案是将需要聚合查询的数据做冗余表,冗余的表不做拆分,同时在业务开发过程中,减少聚合查询。

    经过反复思考,我们最后决定按订单ID做水平分库。从架构上,将系统分为三层:

    • 应用层:即各类业务应用系统
    • 数据访问层:统一的数据访问接口,对上层应用层屏蔽读写分库、分表、缓存等技术细节。
    • 数据层:对DB数据进行分片,并可动态的添加shard分片。

    水平分库的技术关键点在于数据访问层的设计,数据访问层主要包含三部分:

    • 分布式缓存
    • 数据库中间件
    • 数据异构中间件

    而数据库中间件需要包含如下重要的功能:

    • ID生成器:生成每张表的主键
    • 数据源路由:将每次DB操作路由到不同的分片数据源上

    ID生成器

    ID生成器是整个水平分库的核心,它决定了如何拆分数据,以及查询存储-检索数据。ID需要跨库全局唯一,否则会引发业务层的冲突。此外,ID必须是数字且升序,这主要是考虑到升序的ID能保证MySQL的性能(若是UUID等随机字符串,在高并发和大数据量情况下,性能极差)。同时,ID生成器必须非常稳定,因为任何故障都会影响所有的数据库操作。

    我们系统中ID生成器的设计如下所示。

    在这里插入图片描述

    • 整个ID的二进制长度为64位
    • 前36位使用时间戳,以保证ID是升序增加
    • 中间13位是分库标识,用来标识当前这个ID对应的记录在哪个数据库中
    • 后15位为自增序列,以保证在同一秒内并发时,ID不会重复。每个分片库都有一个自增序列表,生成自增序列时,从自增序列表中获取当前自增序列值,并加1,做为当前ID的后15位
    • 下一秒时,后15位的自增序列再次从1开始。

    水平分库是一个极具挑战的项目,我们整个团队也在不断的迎接挑战中快速成长。

    为了适应公司业务的不断发展,除了在MySQL数据库上进行相应的架构升级外,我们还搭建了一套完整的大数据实时分析统计平台,在系统中对用户的行为进行实时分析。

    关于如何搭建大数据实时分析统计平台,对用户的行为进行实时分析,我们后面再详细介绍。

    写在最后

    如果你想进大厂,想升职加薪,或者对自己现有的工作比较迷茫,都可以私信我交流,希望我的一些经历能够帮助到大家~~

    推荐阅读:

    好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~~

    展开全文
  • 在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上面,完成之后释放空间并交换两个dictht的角色。 typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing ...

    后端需要知道的关于redis的事,基本都在这里了。

    此文后续会改为粉丝可见,所以喜欢的请提前关注。

    你的点赞和评论是我创作的最大动力,谢谢。

    1、入门

    Redis是一款基于键值对的NoSQL数据库,它的值支持多种数据结构:
    字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
    • Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人,用作数据库,缓存和消息代理。

    Redis具有内置的复制,Lua脚本,LRU逐出,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供了高可用性。
    • Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等

    1.1NoSql入门概述

    1)单机Mysql的美好时代

    瓶颈:

     

    • 数据库总大小一台机器硬盘内存放不下
    • 数据的索引(B + tree)一个机器的运行内存放不下
    • 访问量(读写混合)一个实例不能承受

     

    2)Memcached(缓存)+ MySql + 垂直拆分

    通过缓存来缓解数据库的压力,优化数据库的结构和索引

    垂直拆分指的是:分成多个数据库存储数据(如:卖家库与买家库)

     

    3)MySql主从复制读写分离

    1. 主从复制:主库来一条数据,从库立刻插入一条。
    2. 读写分离:读取(从库Master),写(主库Slave)

     

    4)分表分库+水平拆分+MySql集群

    1. 主库的写压力出现瓶颈(行锁InnoDB取代表锁MyISAM)
    2. 分库:根据业务相关紧耦合在同一个库,对不同的数据读写进行分库(如注册信息等不常改动的冷库与购物信息等热门库分开)
    3. 分表:切割表数据(例如90W条数据,id 1-30W的放在A库,30W-60W的放在B库,60W-90W的放在C库)

     

    MySql扩展的瓶颈

    1. 大数据下IO压力大
    2. 表结构更改困难

    常用的Nosql

    Redis
    memcache
    Mongdb
    以上几种Nosql 请到各自的官网上下载并参考使用

    Nosql 的核心功能点

    KV(存储)
    Cache(缓存)
    Persistence(持久化)
    ……

    1.2redis的介绍和特点:

           问题:
                   传统数据库:持久化存储数据。
                   solr索引库:大量的数据的检索。
                   在实际开发中,高并发环境下,不同的用户会需要相同的数据。因为每次请求,
                   在后台我们都会创建一个线程来处理,这样造成,同样的数据从数据库中查询了N次。
                   而数据库的查询本身是IO操作,效率低,频率高也不好。
                   总而言之,一个网站总归是有大量的数据是用户共享的,但是如果每个用户都去数据库查询
                   效率就太低了。
           解决:
                   将用户共享数据缓存到服务器的内存中。        
           特点:
                   1、基于键值对
                   2、非关系型(redis)
                           关系型数据库:存储了数据以及数据之间的关系,oracle,mysql
                           非关系型数据库:存储了数据,redis,mdb.
                   3、数据存储在内存中,服务器关闭后,持久化到硬盘中
                   4、支持主从同步
                   实现了缓存数据和项目的解耦。
           redis存储的数据特点:
                   大量数据
                   用户共享数据
                   数据不经常修改。
                   查询数据
           redis的应用场景:
                   网站高并发的主页数据
                   网站数据的排名
                   消息订阅


    1.3redis——数据结构和对象的使用介绍    
           

    redis官网

    微软写的windows下的redis

    我们下载第一个

    额案后基本一路默认就行了

    安装后,服务自动启动,以后也不用自动启动。

    出现这个表示我们连接上了。

     

    redis命令参考链接

    1.3.1String

    数据结构

    struct sdshdr{
        //记录buf数组中已使用字节的数量
        int len;
        //记录buf数组中未使用的数量
        int free;
        //字节数组,用于保存字符串
        char buf[];
    }

    常见操作

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> get hello
    "world"
    127.0.0.1:6379> del hello
    (integer) 1
    127.0.0.1:6379> get hello
    (nil)
    127.0.0.1:6379>

    应用场景

    String是最常用的一种数据类型,普通的key/value存储都可以归为此类,value其实不仅是String,也可以是数字:比如想知道什么时候封锁一个IP地址(访问超过几次)。INCRBY命令让这些变得很容易,通过原子递增保持计数。

    1.3.2LIST

    数据结构

    typedef struct listNode{
        //前置节点
        struct listNode *prev;
        //后置节点
        struct listNode *next;
        //节点的值
        struct value;
    }

    常见操作

    > lpush list-key item
    (integer) 1
    > lpush list-key item2
    (integer) 2
    > rpush list-key item3
    (integer) 3
    > rpush list-key item
    (integer) 4
    > lrange list-key 0 -1
    1) "item2"
    2) "item"
    3) "item3"
    4) "item"
    > lindex list-key 2
    "item3"
    > lpop list-key
    "item2"
    > lrange list-key 0 -1
    1) "item"
    2) "item3"
    3) "item"

    应用场景

    Redis list的应用场景非常多,也是Redis最重要的数据结构之一。
    我们可以轻松地实现最新消息排行等功能。
    Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。

    1.3.3HASH

    数据结构

    dictht是一个散列表结构,使用拉链法保存哈希冲突的dictEntry。

    typedef struct dictht{
        //哈希表数组
        dictEntry **table;
        //哈希表大小
        unsigned long size;
        //哈希表大小掩码,用于计算索引值
        unsigned long sizemask;
        //该哈希表已有节点的数量
        unsigned long used;
    }
    
    typedef struct dictEntry{
        //键
        void *key;
        //值
        union{
            void *val;
            uint64_tu64;
            int64_ts64;
        }
        struct dictEntry *next;
    }

    Redis的字典dict中包含两个哈希表dictht,这是为了方便进行rehash操作。在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上面,完成之后释放空间并交换两个dictht的角色。

    typedef struct dict {
        dictType *type;
        void *privdata;
        dictht ht[2];
        long rehashidx; /* rehashing not in progress if rehashidx == -1 */
        unsigned long iterators; /* number of iterators currently running */
    } dict;

    rehash操作并不是一次性完成、而是采用渐进式方式,目的是为了避免一次性执行过多的rehash操作给服务器带来负担。

    渐进式rehash通过记录dict的rehashidx完成,它从0开始,然后没执行一次rehash例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。

    在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。

    采用渐进式rehash会导致字典中的数据分散在两个dictht中,因此对字典的操作也会在两个哈希表上进行。
    例如查找时,先从ht[0]查找,没有再查找ht[1],添加时直接添加到ht[1]中。

    常见操作

    > hset hash-key sub-key1 value1
    (integer) 1
    > hset hash-key sub-key2 value2
    (integer) 1
    > hset hash-key sub-key1 value1
    (integer) 0
    > hgetall hash-key
    1) "sub-key1"
    2) "value1"
    3) "sub-key2"
    4) "value2"
    > hdel hash-key sub-key2
    (integer) 1
    > hdel hash-key sub-key2
    (integer) 0
    > hget hash-key sub-key1
    "value1"
    > hgetall hash-key
    1) "sub-key1"
    2) "value1"

    1.3.4SET

    常见操作

    > sadd set-key item
    (integer) 1
    > sadd set-key item2
    (integer) 1
    > sadd set-key item3
    (integer) 1
    > sadd set-key item
    (integer) 0
    > smembers set-key
    1) "item2"
    2) "item"
    3) "item3"
    > sismember set-key item4
    (integer) 0
    > sismember set-key item
    (integer) 1
    > srem set-key item
    (integer) 1
    > srem set-key item
    (integer) 0
    > smembers set-key
    1) "item2"
    2) "item3"

    应用场景

    Redis为集合提供了求交集、并集、差集等操作,故可以用来求共同好友等操作。

    1.3.5ZSET

    数据结构

    typedef struct zskiplistNode{
            //后退指针
            struct zskiplistNode *backward;
            //分值
            double score;
            //成员对象
            robj *obj;
            //层
            struct zskiplistLever{
                //前进指针
                struct zskiplistNode *forward;
                //跨度
                unsigned int span;
            }lever[];
        }
        
        typedef struct zskiplist{
            //表头节点跟表尾结点
            struct zskiplistNode *header, *tail;
            //表中节点的数量
            unsigned long length;
            //表中层数最大的节点的层数
            int lever;
        }

    跳跃表,基于多指针有序链实现,可以看作多个有序链表。

    与红黑树等平衡树相比,跳跃表具有以下优点:

    • 插入速度非常快速,因为不需要进行旋转等操作来维持平衡性。
    • 更容易实现。
    • 支持无锁操作。

    常见操作

    > zadd zset-key 728 member1
    (integer) 1
    > zadd zset-key 982 member0
    (integer) 1
    > zadd zset-key 982 member0
    (integer) 0
    > zrange zset-key 0 -1
    1) "member1"
    2) "member0"
    > zrange zset-key 0 -1 withscores
    1) "member1"
    2) "728"
    3) "member0"
    4) "982"
    > zrangebyscore zset-key 0 800 withscores
    1) "member1"
    2) "728"
    > zrem zset-key member1
    (integer) 1
    > zrem zset-key member1
    (integer) 0
    > zrange zset-key 0 -1 withscores
    1) "member0"
    2) "982"

    应用场景

    以某个条件为权重,比如按顶的次数排序
    ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
    Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。

     

    redis命令参考链接

    1.4Spring整合Redis

    引入依赖
    - spring-boot-starter-data-redis

    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-redis</artifactId>
    		</dependency>


    配置Redis
    - 配置数据库参数

    # RedisProperties
    spring.redis.database=11#第11个库,这个随便
    spring.redis.host=localhost
    spring.redis.port=6379#端口


    - 编写配置类,构造RedisTemplate

    这个springboot已经帮我们配了,但是默认object,我想改成string

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;
    
    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);
    
            // 设置key的序列化方式
            template.setKeySerializer(RedisSerializer.string());
            // 设置value的序列化方式
            template.setValueSerializer(RedisSerializer.json());
            // 设置hash的key的序列化方式
            template.setHashKeySerializer(RedisSerializer.string());
            // 设置hash的value的序列化方式
            template.setHashValueSerializer(RedisSerializer.json());
    
            template.afterPropertiesSet();
            return template;
        }
    
    }


    访问Redis
    - redisTemplate.opsForValue()
    - redisTemplate.opsForHash()
    - redisTemplate.opsForList()
    - redisTemplate.opsForSet()
    - redisTemplate.opsForZSet()

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @ContextConfiguration(classes = CommunityApplication.class)
    public class RedisTests {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Test
        public void testStrings() {
            String redisKey = "test:count";
    
            redisTemplate.opsForValue().set(redisKey, 1);
    
            System.out.println(redisTemplate.opsForValue().get(redisKey));
            System.out.println(redisTemplate.opsForValue().increment(redisKey));
            System.out.println(redisTemplate.opsForValue().decrement(redisKey));
        }
    
        @Test
        public void testHashes() {
            String redisKey = "test:user";
    
            redisTemplate.opsForHash().put(redisKey, "id", 1);
            redisTemplate.opsForHash().put(redisKey, "username", "zhangsan");
    
            System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
            System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
        }
    
        @Test
        public void testLists() {
            String redisKey = "test:ids";
    
            redisTemplate.opsForList().leftPush(redisKey, 101);
            redisTemplate.opsForList().leftPush(redisKey, 102);
            redisTemplate.opsForList().leftPush(redisKey, 103);
    
            System.out.println(redisTemplate.opsForList().size(redisKey));
            System.out.println(redisTemplate.opsForList().index(redisKey, 0));
            System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));
    
            System.out.println(redisTemplate.opsForList().leftPop(redisKey));
            System.out.println(redisTemplate.opsForList().leftPop(redisKey));
            System.out.println(redisTemplate.opsForList().leftPop(redisKey));
        }
    
        @Test
        public void testSets() {
            String redisKey = "test:teachers";
    
            redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");
    
            System.out.println(redisTemplate.opsForSet().size(redisKey));
            System.out.println(redisTemplate.opsForSet().pop(redisKey));
            System.out.println(redisTemplate.opsForSet().members(redisKey));
        }
    
        @Test
        public void testSortedSets() {
            String redisKey = "test:students";
    
            redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
            redisTemplate.opsForZSet().add(redisKey, "悟空", 90);
            redisTemplate.opsForZSet().add(redisKey, "八戒", 50);
            redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);
            redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);
    
            System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
            System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));
            System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
            System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
        }
    
        @Test
        public void testKeys() {
            redisTemplate.delete("test:user");
    
            System.out.println(redisTemplate.hasKey("test:user"));
    
            redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
        }
    }

    这样还是稍微有点麻烦,我们其实可以绑定key

        // 多次访问同一个key
        @Test
        public void testBoundOperations() {
            String redisKey = "test:count";
            BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
            operations.increment();
            operations.increment();
            operations.increment();
            operations.increment();
            operations.increment();
            System.out.println(operations.get());
        }

    2、数据结构原理总结

    这部分在我看来是最有意思的,我们有必要了解底层数据结构的实现,这也是我最感兴趣的。

    比如,你知道redis中的字符串怎么实现的吗?为什么这么实现?

    你知道redis压缩列表是什么算法吗?

    你知道redis为什么抛弃了红黑树反而采用了跳表这种新的数据结构吗?

    你知道hyperloglog为什么用如此小的空间就可以有这么好的统计性能和准确性吗?

    你知道布隆过滤器为什么这么有效吗?有没有数学证明过?

    你是否还能很快写出来快排?或者不断优化性能的排序?是不是只会调库了甚至库函数怎么实现的都不知道?真的就是快排?

    包括数据库,持久化,处理事件、客户端服务端、事务的实现、发布和订阅等功能的实现,也需要了解。


    2.1数据结构和对象的实现

    • 1) 字符串

    redis并未使用传统的c语言字符串表示,它自己构建了一种简单的动态字符串抽象类型。

    在redis里,c语言字符串只会作为字符串字面量出现,用在无需修改的地方。

    当需要一个可以被修改的字符串时,redis就会使用自己实现的SDS(simple dynamic string)。比如在redis数据库里,包含字符串的键值对底层都是SDS实现的,不止如此,SDS还被用作缓冲区(buffer):比如AOF模块中的AOF缓冲区以及客户端状态中的输入缓冲区。

    下面来具体看一下sds的实现:

    struct sdshdr
    {
        int len;//buf已使用字节数量(保存的字符串长度)
        int free;//未使用的字节数量
        char buf[];//用来保存字符串的字节数组
    };

    sds遵循c中字符串以'\0'结尾的惯例,这一字节的空间不算在len之内。

    这样的好处是,我们可以直接重用c中的一部分函数。比如printf;

        sds相对c的改进

        获取长度:c字符串并不记录自身长度,所以获取长度只能遍历一遍字符串,redis直接读取len即可。

        缓冲区安全:c字符串容易造成缓冲区溢出,比如:程序员没有分配足够的空间就执行拼接操作。而redis会先检查sds的空间是否满足所需要求,如果不满足会自动扩充。

        内存分配:由于c不记录字符串长度,对于包含了n个字符的字符串,底层总是一个长度n+1的数组,每一次长度变化,总是要对这个数组进行一次内存重新分配的操作。因为内存分配涉及复杂算法并且可能需要执行系统调用,所以它通常是比较耗时的操作。   

        redis内存分配:

    1、空间预分配:如果修改后大小小于1MB,程序分配和len大小一样的未使用空间,如果修改后大于1MB,程序分配  1MB的未使用空间。修改长度时检查,够的话就直接使用未使用空间,不用再分配。 

    2、惰性空间释放:字符串缩短时不需要释放空间,用free记录即可,留作以后使用。

        二进制安全

    c字符串除了末尾外,不能包含空字符,否则程序读到空字符会误以为是结尾,这就限制了c字符串只能保存文本,二进制文件就不能保存了。

    而redis字符串都是二进制安全的,因为有len来记录长度。

    • 2) 链表

    作为一种常用数据结构,链表内置在很多高级语言中,因为c并没有,所以redis实现了自己的链表。

    链表在redis也有一定的应用,比如列表键的底层实现之一就是链表。(当列表键包含大量元素或者元素都是很长的字符串时)

    发布与订阅、慢查询、监视器等功能也用到了链表。

    具体实现:

    //redis的节点使用了双向链表结构
    typedef struct listNode {
        // 前置节点
        struct listNode *prev;
        // 后置节点
        struct listNode *next;
        // 节点的值
        void *value;
    } listNode;
    //其实学过数据结构的应该都实现过
    typedef struct list {
        // 表头节点
        listNode *head;
        // 表尾节点
        listNode *tail;
        // 链表所包含的节点数量
        unsigned long len;
        // 节点值复制函数
        void *(*dup)(void *ptr);
        // 节点值释放函数
        void (*free)(void *ptr);
        // 节点值对比函数
        int (*match)(void *ptr, void *key);
    } list;

    总结一下redis链表特性:

    双端、无环、带长度记录、

    多态:使用 void* 指针来保存节点值, 可以通过 dup 、 free 、 match 为节点值设置类型特定函数, 可以保存不同类型的值。

    • 3)字典

    其实字典这种数据结构也内置在很多高级语言中,但是c语言没有,所以redis自己实现了。

    应用也比较广泛,比如redis的数据库就是字典实现的。不仅如此,当一个哈希键包含的键值对比较多,或者都是很长的字符串,redis就会用字典作为哈希键的底层实现。

    来看看具体是实现:

    //redis的字典使用哈希表作为底层实现
    typedef struct dictht {
        // 哈希表数组
        dictEntry **table;
        // 哈希表大小
        unsigned long size;
        // 哈希表大小掩码,用于计算索引值
        // 总是等于 size - 1
        unsigned long sizemask;
    
        // 该哈希表已有节点的数量
        unsigned long used;
    
    } dictht;

    table 是一个数组, 数组中的每个元素都是一个指向dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对

    图为一个大小为4的空哈希表。

    我们接着就来看dictEntry的实现:

    typedef struct dictEntry {
        // 键
        void *key;
        // 值
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
    
        // 指向下个哈希表节点,形成链表
        struct dictEntry *next;
    } dictEntry;

    (v可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。)

    next就是解决键冲突问题的,冲突了就挂后面,这个学过数据结构的应该都知道吧,不说了。

     

    下面我们来说字典是怎么实现的了。

    typedef struct dict {
        // 类型特定函数
        dictType *type;
        // 私有数据
        void *privdata;
        // 哈希表
        dictht ht[2];
        // rehash 索引
        int rehashidx; //* rehashing not in progress if rehashidx == -1 
    } dict;

    type 和 privdata 是对不同类型的键值对, 为创建多态字典而设置的:

    type 指向 dictType , 每个 dictType 保存了用于操作特定类型键值对的函数, 可以为用途不同的字典设置不同的类型特定函数。

    而 privdata 属性则保存了需要传给那些类型特定函数的可选参数。

    而dictType就暂时不展示了,不重要而且字有点多。。。还是讲有意思的东西吧

        rehash(重新散列)

    随着我们不断的操作,哈希表保存的键值可能会增多或者减少,为了让哈希表的负载因子维持在合理的范围内,有时需要对哈希表进行合理的扩展或者收缩。 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。

    redis字典哈希rehash的步骤如下:

    1)为ht[1]分配合理空间:如果是扩展操作,大小为第一个大于等于ht[0]*used*2的,2的n次幂。

                                               如果是收缩操作,大小为第一个大于等于ht[0]*used的,2的n次幂。

    2)将ht[0]中的数据rehash到ht[1]上。

    3)释放ht[0],将ht[1]设置为ht[0],ht[1]创建空表,为下次做准备。

        渐进rehash

    数据量特别大时,rehash可能对服务器造成影响。为了避免,服务器不是一次性rehash的,而是分多次。

    我们维持一个变量rehashidx,设置为0,代表rehash开始,然后开始rehash,在这期间,每个对字典的操作,程序都会把索引rehashidx上的数据移动到ht[1]。

    随着操作不断执行,最终我们会完成rehash,设置rehashidx为-1.

    需要注意:rehash过程中,每一次增删改查也是在两个表进行的。

    • 4)整数集合

    整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 可以保存 int16_t 、 int32_t 、 int64_t 的整数值, 并且保证集合中不会出现重复元素。

    实现较为简单:

    typedef struct intset {
        // 编码方式
        uint32_t encoding;
        // 集合包含的元素数量
        uint32_t length;
        // 保存元素的数组
        int8_t contents[];
    } intset;

    各个项在数组中从小到大有序地排列, 并且数组中不包含任何重复项。

    虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组并不保存任何 int8_t 类型的值 —— contents 数组的真正类型取决于 encoding 属性的值:

    如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t 类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )。

    如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t 类型的整数值 (最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。

    如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t 类型的整数值 (最小值为 -9,223,372,036,854,775,808 ,最大值为 9,223,372,036,854,775,807 )。

        升级

    c语言是静态类型语言,不允许不同类型保存在一个数组。这样第一,灵活性较差,第二,有时会用掉不必要的内存

    比如用long long储存1

    为了提高整数集合的灵活性和节约内存,我们引入升级策略。

    当我们要将一个新元素添加到集合里, 并且新元素类型比集合现有元素的类型都要长时, 集合需要先进行升级。

    分为三步进行:

    1. 根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
    2. 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上
    3. 将新元素添加到底层数组里面。

    因为每次添加新元素都可能会引起升级, 每次升级都要对已有元素类型转换, 所以添加新元素的时间复杂度为 O(N) 。

    因为引发升级的新元素比原数据都长,所以要么他是最大的,要么他是最小的。我们把它放在开头或结尾即可。

     

        降级

    略略略,不管你们信不信,整数集合不支持降级操作。。我也不知道为啥

    • 5)压缩列表

    压缩列表是列表键和哈希键的底层实现之一。

    当一个列表键只包含少量列表项,并且列表项都是小整数或者短字符串,redis就会用压缩列表做列表键底层实现。

    压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。

    一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。

    具体实现:

    具体说一下entry:

    由三个部分组成:

    1、previous_entry_length:记录上一个节点的长度,这样我们就可以从最后一路遍历到开头。

    2、encoding:记录了content所保存的数据类型和长度。(具体编码不写了,不重要)

    3、content:保存节点值,可以是字节数组或整数。(具体怎么压缩的等我搞明白再补)

        连锁更新

    前面说过, 每个节点的 previous_entry_length 属性都记录了前一个节点的长度:

    • 如果前一节点的长度< 254 KB, 那么 previous_entry_length 需要用 1 字节长的空间
    • 如果前一节点的长度>=254 KB, 那么 previous_entry_length 需要用 5 字节长的空间

    现在, 考虑这样一种情况: 在一个压缩列表中, 有多个连续的、长度介于 250 字节到 253 字节之间的节点 ,这时, 如果我们将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的表头节点。。。。

    然后脑补一下,就会导致连锁扩大每个节点的空间对吧?e(i)因为e(i-1)的扩大而扩大,i+1也是如此,以此类推。。。

     

    删除节点同样会导致连锁更新。

    这个事情只是想说明一个问题:插入删除操作的最坏时间复杂度其实是o(n*n),因为每更新一个节点都要o(n)。

    但是,也不用太过担心,因为这种特殊情况并不多见,这些命令的平均复杂度依旧是o(n)。

     

    2.2 跳表专栏

    2.2.1跳表是啥

    为什么选择了跳表而不是红黑树?

    跳表是个啥东西请看这个文章。

    我们知道,节点插入时随机出一个层数,仅仅依靠一个简单的随机数操作而构建出来的多层链表结构,能保证它有一个良好的查找性能吗?为了回答这个疑问,我们需要分析skiplist的统计性能。

    在分析之前,我们还需要着重指出的是,执行插入操作时计算随机数的过程,是一个很关键的过程,它对skiplist的统计特性有着很重要的影响。这并不是一个普通的服从均匀分布的随机数,它的计算过程如下:

    • 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
    • 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。
    • 节点最大的层数不允许超过一个最大值,记为MaxLevel。

    这个计算随机层数的伪码如下所示:

    randomLevel()
    level := 1
    // random()返回一个[0...1)的随机数
    while random() < p and level < MaxLevel do
    level := level + 1
    return level
    

    randomLevel()的伪码中包含两个参数,一个是p,一个是MaxLevel。在Redis的skiplist实现中,这两个参数的取值为:

    p = 1/4
    MaxLevel = 32

    2.2.2skiplist的算法性能分析

    在这一部分,我们来简单分析一下skiplist的时间复杂度和空间复杂度,以便对于skiplist的性能有一个直观的了解。如果你不是特别偏执于算法的性能分析,那么可以暂时跳过这一小节的内容。

    我们先来计算一下每个节点所包含的平均指针数目(概率期望)。节点包含的指针数目,相当于这个算法在空间上的额外开销(overhead),可以用来度量空间复杂度。

    根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下:

    • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
    • 节点层数恰好等于1的概率为1-p。
    • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
    • 节点层数大于等于3的概率为p^2,而节点层数恰好等于3的概率为p^2(1-p)。
    • 节点层数大于等于4的概率为p^3,而节点层数恰好等于4的概率为p^3(1-p)。
    • ......

    因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:

    现在很容易计算出:

    • 当p=1/2时,每个节点所包含的平均指针数目为2;
    • 当p=1/4时,每个节点所包含的平均指针数目为1.33。这也是Redis里的skiplist实现在空间上的开销。

    接下来,为了分析时间复杂度,我们计算一下skiplist的平均查找长度。查找长度指的是查找路径上跨越的跳数,而查找过程中的比较次数就等于查找长度加1。以前面图中标出的查找23的查找路径为例,从左上角的头结点开始,一直到结点22,查找长度为6。

    为了计算查找长度,这里我们需要利用一点小技巧。我们注意到,每个节点插入的时候,它的层数是由随机函数randomLevel()计算出来的,而且随机的计算不依赖于其它节点,每次插入过程都是完全独立的。所以,从统计上来说,一个skiplist结构的形成与节点的插入顺序无关。

    这样的话,为了计算查找长度,我们可以将查找过程倒过来看,从右下方第1层上最后到达的那个节点开始,沿着查找路径向左向上回溯,类似于爬楼梯的过程。我们假设当回溯到某个节点的时候,它才被插入,这虽然相当于改变了节点的插入顺序,但从统计上不影响整个skiplist的形成结构。

    现在假设我们从一个层数为i的节点x出发,需要向左向上攀爬k层。这时我们有两种可能:

    • 如果节点x有第(i+1)层指针,那么我们需要向上走。这种情况概率为p。
    • 如果节点x没有第(i+1)层指针,那么我们需要向左走。这种情况概率为(1-p)。

    preview

    用C(k)表示向上攀爬k个层级所需要走过的平均查找路径长度(概率期望),那么:

    C(0)=0
    C(k)=(1-p)×(上图中情况b的查找长度) + p×(上图中情况c的查找长度)

    代入,得到一个差分方程并化简:

    C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
    C(k)=1/p+C(k-1)
    C(k)=k/p

    这个结果的意思是,我们每爬升1个层级,需要在查找路径上走1/p步。而我们总共需要攀爬的层级数等于整个skiplist的总层数-1。

    那么接下来我们需要分析一下当skiplist中有n个节点的时候,它的总层数的概率均值是多少。这个问题直观上比较好理解。根据节点的层数随机算法,容易得出:

    • 第1层链表固定有n个节点;
    • 第2层链表平均有n*p个节点;
    • 第3层链表平均有n*p^2个节点;
    • ...

    所以,从第1层到最高层,各层链表的平均节点数是一个指数递减的等比数列。容易推算出,总层数的均值为log1/pn,而最高层的平均节点数为1/p。

    综上,粗略来计算的话,平均查找长度约等于:

    • C(log1/pn-1)=(log1/pn-1)/p

    即,平均时间复杂度为O(log n)。

    当然,这里的时间复杂度分析还是比较粗略的。比如,沿着查找路径向左向上回溯的时候,可能先到达左侧头结点,然后沿头结点一路向上;还可能先到达最高层的节点,然后沿着最高层链表一路向左。但这些细节不影响平均时间复杂度的最后结果。另外,这里给出的时间复杂度只是一个概率平均值,但实际上计算一个精细的概率分布也是有可能的。

    详情还请参见William Pugh的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

    2.2.3skiplist与平衡树、哈希表的比较

    • skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
    • 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
    • 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
    • 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
    • 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
    • 从算法实现难度上来比较,skiplist比平衡树要简单得多。

    2.2.4Redis中的skiplist和经典有何不同

    • 分数(score)允许重复,即skiplist的key允许重复。这在最开始介绍的经典skiplist中是不允许的。
    • 在比较时,不仅比较分数(相当于skiplist的key),还比较数据本身。在Redis的skiplist实现中,数据本身的内容唯一标识这份数据,而不是由key来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。
    • 第1层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。
    • 在skiplist中可以很方便地计算出每个元素的排名(rank)。

    2.2.5作者的话

    最后我们看看,对于这个问题,Redis的作者 @antirez 是怎么说的:

    There are a few reasons:

    1) They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

    2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

    3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

    有几个原因:

    1)它们的记忆力不是很强。基本上由你决定。更改有关节点具有给定数量级别的概率的参数将使内存密集度低于btree。

    2)排序集通常是许多Zrange或Zrevrange操作的目标,即作为链表遍历跳过列表。通过此操作,跳过列表的缓存区域性至少与其他类型的平衡树一样好。

    3)它们易于实现、调试等。例如,由于跳过列表的简单性,我收到了一个补丁(已经在redis master中),其中包含在o(log(n))中实现zrank的扩展跳过列表。它只需要对代码稍作修改。

    2.3HyperLogLog 专栏

    HyperLogLog 是一种概率数据结构,用来估算数据的基数。数据集可以是网站访客的 IP 地址,E-mail 邮箱或者用户 ID。

    基数就是指一个集合中不同值的数目,比如 a, b, c, d 的基数就是 4,a, b, c, d, a 的基数还是 4。虽然 a 出现两次,只会被计算一次。

    使用 Redis 统计集合的基数一般有三种方法,分别是使用 Redis 的 HashMap,BitMap 和 HyperLogLog。前两个数据结构在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 则不会。

    Redis 的 HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要不不高的场景。

    这是一个很惊人的结果,以如此小的内存来记录如此大数量级的数据基数。下面我们就带大家来深入了解一下 HyperLogLog 的使用,基础原理,源码实现和具体的试验数据分析。

    2.3.1HyperLogLog 在 Redis 中的使用

    Redis 提供了 PFADD 、 PFCOUNT 和 PFMERGE 三个命令来供用户使用 HyperLogLog。

    PFADD 用于向 HyperLogLog 添加元素。

    > PFADD visitors alice bob carol
    
    (integer) 1
    
    > PFCOUNT visitors
    
    (integer) 3

    如果 HyperLogLog 估计的近似基数在 PFADD 命令执行之后出现了变化, 那么命令返回 1 , 否则返回 0 。 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令。

    PFCOUNT 命令会给出 HyperLogLog 包含的近似基数。在计算出基数后, PFCOUNT 会将值存储在 HyperLogLog 中进行缓存,知道下次 PFADD 执行成功前,就都不需要再次进行基数的计算。

    PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的并集基数。

    > PFADD customers alice dan
    
    (integer) 1
    
    > PFMERGE everyone visitors customers
    
    OK
    
    > PFCOUNT everyone
    
    (integer) 4

    2.3.2内存消耗对比实验

    我们下面就来通过实验真实对比一下下面三种数据结构的内存消耗,HashMap、BitMap 和 HyperLogLog。

    我们首先使用 Lua 脚本向 Redis 对应的数据结构中插入一定数量的数,然后执行 bgsave 命令,最后使用 redis-rdb-tools 的 rdb 的命令查看各个键所占的内存大小。

    下面是 Lua 的脚本

    local key = KEYS[1]
    
    local size = tonumber(ARGV[1])
    
    local method = tonumber(ARGV[2])
    
    
    
    for i=1,size,1 do
    
    if (method == 0)
    
    then
    
    redis.call('hset',key,i,1)
    
    elseif (method == 1)
    
    then
    
    redis.call('pfadd',key, i)
    
    else
    
    redis.call('setbit', key, i, 1)
    
    end
    
    end

    我们在通过 redis-cli 的 script load 命令将 Lua 脚本加载到 Redis 中,然后使用 evalsha 命令分别向 HashMap、HyperLogLog 和 BitMap 三种数据结构中插入了一千万个数,然后使用 rdb 命令查看各个结构内存消耗。

    我们进行了两轮实验,分别插入一万数字和一千万数字,三种数据结构消耗的内存统计如下所示。

    从表中可以明显看出,一万数量级时 BitMap 消耗内存最小, 一千万数量级时 HyperLogLog 消耗内存最小,但是总体来看,HyperLogLog 消耗的内存都是 14392 字节,可见 HyperLogLog 在内存消耗方面有自己的独到之处。

    2.3.3基本原理

    HyperLogLog 是一种概率数据结构,它使用概率算法来统计集合的近似基数。而它算法的最本源则是伯努利过程。

    伯努利过程就是一个抛硬币实验的过程。抛一枚正常硬币,落地可能是正面,也可能是反面,二者的概率都是 1/2 。伯努利过程就是一直抛硬币,直到落地时出现正面位置,并记录下抛掷次数k。比如说,抛一次硬币就出现正面了,此时 k 为 1; 第一次抛硬币是反面,则继续抛,直到第三次才出现正面,此时 k 为 3。

    对于 n 次伯努利过程,我们会得到 n 个出现正面的投掷次数值 k1, k2 ... kn , 其中这里的最大值是k_max。

    根据一顿数学推导,我们可以得出一个结论: 2^{k_ max} 来作为n的估计值。也就是说你可以根据最大投掷次数近似的推算出进行了几次伯努利过程。

    下面,我们就来讲解一下 HyperLogLog 是如何模拟伯努利过程,并最终统计集合基数的。

    HyperLogLog 在添加元素时,会通过Hash函数,将元素转为64位比特串,例如输入5,便转为101(省略前面的0,下同)。这些比特串就类似于一次抛硬币的伯努利过程。比特串中,0 代表了抛硬币落地是反面,1 代表抛硬币落地是正面,如果一个数据最终被转化了 10010000,那么从低位往高位看,我们可以认为,这串比特串可以代表一次伯努利过程,首次出现 1 的位数为5,就是抛了5次才出现正面。

    所以 HyperLogLog 的基本思想是利用集合中数字的比特串第一个 1 出现位置的最大值来预估整体基数,但是这种预估方法存在较大误差,为了改善误差情况,HyperLogLog中引入分桶平均的概念,计算 m 个桶的调和平均值。

    Redis 中 HyperLogLog 一共分了 2^14 个桶,也就是 16384 个桶。每个桶中是一个 6 bit 的数组。

    HyperLogLog 将上文所说的 64 位比特串的低 14 位单独拿出,它的值就对应桶的序号,然后将剩下 50 位中第一次出现 1 的位置值设置到桶中。50位中出现1的位置值最大为50,所以每个桶中的 6 位数组正好可以表示该值。

    在设置前,要设置进桶的值是否大于桶中的旧值,如果大于才进行设置,否则不进行设置。

    此时为了性能考虑,是不会去统计当前的基数的,而是将 HyperLogLog 头的 card 属性中的标志位置为 1,表示下次进行 pfcount 操作的时候,当前的缓存值已经失效了,需要重新统计缓存值。在后面 pfcount 流程的时候,发现这个标记为失效,就会去重新统计新的基数,放入基数缓存。

    在计算近似基数时,就分别计算每个桶中的值,带入到上文的 DV 公式中,进行调和平均和结果修正,就能得到估算的基数值。

    2.3.4HyperLogLog 具体对象

    我们首先来看一下 HyperLogLog 对象的定义

    struct hllhdr {
    
    char magic[4]; /* 魔法值 "HYLL" */
    
    uint8_t encoding; /* 密集结构或者稀疏结构 HLL_DENSE or HLL_SPARSE. */
    
    uint8_t notused[3]; /* 保留位, 全为0. */
    
    uint8_t card[8]; /* 基数大小的缓存 */
    
    uint8_t registers[]; /* 数据字节数组 */
    
    };

    HyperLogLog 对象中的 registers 数组就是桶,它有两种存储结构,分别为密集存储结构和稀疏存储结构,两种结构只涉及存储和桶的表现形式,从中我们可以看到 Redis 对节省内存极致地追求。

    我们先看相对简单的密集存储结构,它也是十分的简单明了,既然要有 2^14 个 6 bit的桶,那么我就真使用足够多的 uint8_t 字节去表示,只是此时会涉及到字节位置和桶的转换,因为字节有 8 位,而桶只需要 6 位。

    所以我们需要将桶的序号转换成对应的字节偏移量 offsetbytes 和其内部的位数偏移量 offsetbits。需要注意的是小端字节序,高位在右侧,需要进行倒转。

    当 offset_bits 小于等于2时,说明一个桶就在该字节内,只需要进行倒转就能得到桶的值。

     offset_bits 大于 2 ,则说明一个桶分布在两个字节内,此时需要将两个字节的内容都进行倒置,然后再进行拼接得到桶的值。

    Redis 为了方便表达稀疏存储,它将上面三种字节表示形式分别赋予了一条指令。

    • ZERO : 一字节,表示连续多少个桶计数为0,前两位为标志00,后6位表示有多少个桶,最大为64。

    • XZERO : 两个字节,表示连续多少个桶计数为0,前两位为标志01,后14位表示有多少个桶,最大为16384。

    • VAL : 一字节,表示连续多少个桶的计数为多少,前一位为标志1,四位表示连桶内计数,所以最大表示桶的计数为32。后两位表示连续多少个桶。

     

    Redis从稀疏存储转换到密集存储的条件是:

    • 任意一个计数值从 32 变成 33,因为 VAL 指令已经无法容纳,它能表示的计数值最大为 32

    • 稀疏存储占用的总字节数超过 3000 字节,这个阈值可以通过 hllsparsemax_bytes 参数进行调整。

    2.4LRU专栏

    2.4.1LRU介绍和代码实现

    LRU全称是Least Recently Used,即最近最久未使用的意思。

    LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。(这一段是找的,让大家理解一下什么是LRU)。

     

    说一下我们什么时候见到过LRU:其实老师们肯定都给大家举过这么个例子:你在图书馆,你把书架子里的书拿到桌子上。。但是桌子是有限的,你有时候不得不把一些书放回去。这就相当于内存和硬盘。这个例子都说过吧?

    LRU就是记录你最长时间没看过的书,就把它放回去。在cache那里见过吧

     

    然后最近在研究redis,又看到了这个LRU,所以就想写一下吧。

    题目:设计一个结构,这个结构可以查询K-V,但是容量有限,当存不下的时候就要把用的年代最久远的那个东西扔掉。

    其实思路很简单,我们维护一个双向链表即可,get也就是使用了,我们就把把它提到最安全的位置。新来的KV就依次放即可。

    我们就先写这个双向链表结构

    先写节点结构:

    	public static class Node<V> {
    		public V value;
    		public Node<V> last;//前
    		public Node<V> next;//后
    
    		public Node(V value) {
    			this.value = value;
    		}
    	}

    然后写双向链表结构: 我们没必要把链表操作都写了,分析一下,我们只有三个操作:

    1、加节点

    2、使用了某个节点就把它调到尾,代表优先级最高

    3、把优先级最低的移除,也就是去头部

    (不会的,翻我之前的链表操作都有写)

    	public static class NodeDoubleLinkedList<V> {
    		private Node<V> head;//头
    		private Node<V> tail;//尾
    
    		public NodeDoubleLinkedList() {
    			this.head = null;
    			this.tail = null;
    		}
    
    		public void addNode(Node<V> newNode) {
    			if (newNode == null) {
    				return;
    			}
    			if (this.head == null) {//头空
    				this.head = newNode;
    				this.tail = newNode;
    			} else {//头不空
    				this.tail.next = newNode;
    				newNode.last = this.tail;//注意让本节点前指针指向旧尾
    				this.tail = newNode;//指向新尾
    			}
    		}
    /*某个点移到最后*/
    		public void moveNodeToTail(Node<V> node) {
    			if (this.tail == node) {//是尾
    				return;
    			}
    			if (this.head == node) {//是头
    				this.head = node.next;
    				this.head.last = null;
    			} else {//中间
    				node.last.next = node.next;
    				node.next.last = node.last;
    			}
    			node.last = this.tail;
    			node.next = null;
    			this.tail.next = node;
    			this.tail = node;
    		}
    /*删除第一个*/
    		public Node<V> removeHead() {
    			if (this.head == null) {
    				return null;
    			}
    			Node<V> res = this.head;
    			if (this.head == this.tail) {//就一个
    				this.head = null;
    				this.tail = null;
    			} else {
    				this.head = res.next;
    				res.next = null;
    				this.head.last = null;
    			}
    			return res;
    		}
    
    	}

    链表操作封装完了就要实现这个结构了。

    具体思路代码注释

    	public static class MyCache<K, V> {
    		//为了kv or vk都能查
    		private HashMap<K, Node<V>> keyNodeMap;
    		private HashMap<Node<V>, K> nodeKeyMap;
    		//用来做优先级
    		private NodeDoubleLinkedList<V> nodeList;
    		private int capacity;//容量
    
    		public MyCache(int capacity) {
    			if (capacity < 1) {//你容量连1都不给,捣乱呢
    				throw new RuntimeException("should be more than 0.");
    			}
    			this.keyNodeMap = new HashMap<K, Node<V>>();
    			this.nodeKeyMap = new HashMap<Node<V>, K>();
    			this.nodeList = new NodeDoubleLinkedList<V>();
    			this.capacity = capacity;
    		}
    
    		public V get(K key) {
    			if (this.keyNodeMap.containsKey(key)) {
    				Node<V> res = this.keyNodeMap.get(key);
    				this.nodeList.moveNodeToTail(res);//使用过了就放到尾部
    				return res.value;
    			}
    			return null;
    		}
    
    		public void set(K key, V value) {
    			if (this.keyNodeMap.containsKey(key)) {
    				Node<V> node = this.keyNodeMap.get(key);
    				node.value = value;//放新v
    				this.nodeList.moveNodeToTail(node);//我们认为放入旧key也是使用过
    			} else {
    				Node<V> newNode = new Node<V>(value);
    				this.keyNodeMap.put(key, newNode);
    				this.nodeKeyMap.put(newNode, key);
    				this.nodeList.addNode(newNode);//加进去
    				if (this.keyNodeMap.size() == this.capacity + 1) {
    					this.removeMostUnusedCache();//放不下就去掉优先级最低的
    				}
    			}
    		}
    
    		private void removeMostUnusedCache() {
    			//删除头
    			Node<V> removeNode = this.nodeList.removeHead();
    			K removeKey = this.nodeKeyMap.get(removeNode);
    			//删除掉两个map中的记录
    			this.nodeKeyMap.remove(removeNode);
    			this.keyNodeMap.remove(removeKey);
    		}
    	}

     

    2.4.2Redis中的LRU算法改进

    redis通常使用缓存,是使用一种固定最大内存的使用。当数据达到可使用的最大固定内存时,我们需要通过移除老数据来获取空间。redis作为缓存是否有效的重要标志是如何寻找一种好的策略:删除即将需要使用的数据是一种糟糕的策略,而删除那些很少再次请求的数据则是一种好的策略。
    在其他的缓存组件还有个命中率,仅仅表示读请求的比例。访问一个缓存中的keys通常不是分布式的。然而访问经常变化,这意味着不经常访问,相反,有些keys一旦不流行可能会转向最经常访问的keys。 因此,通常一个缓存系统应该尽可能保留那些未来最有可能被访问的keys。针对keys淘汰的策略是:那些未来极少可能被访问的数据应该被移除。
    但有一个问题:redis和其他缓存系统不能够预测未来。

    LRU算法

    缓存系统不能预测未来,原因是:那些很少再次被访问的key也很有可能最近访问相当频繁。如果经常被访问的模式不会突然改变,那么这是一种很有效的策略。然而,“最近经常被访问”似乎更隐晦地标明一种 理念。这种算法被称为LRU算法。最近访问频繁的key相比访问少的key有更高的可能性。
    举个例子,这里有4个不同访问周期的key,每一个“~”字符代表一秒,结尾的“|”表示当前时刻。

    ~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
    ~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
    ~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
    ~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

    A key每5秒请求一次,B周期是2秒,C、D都是10秒。
    访问频率最高的是B,因为它的空闲时间最短,这意味着B是4个key中未来最有可能被访问的key。
    同样的A和C目前的空闲时间是2s和6s也能很好地反映它们本身的周期。然而你可以看到不够严谨:D的访问周期是10秒,但它却是4个key中最近被访问的。
    当然,在一个很长的运行周期中,LRU算法能工作得很好。通常有一个更高访问频率的key当然有一个更低的空闲周期。LRU算法淘汰最少被访问key,那些有最大空闲周期的key。实现上也相当容易,只需要额外跟踪最近被访问的key即可,有时甚至都需要:把所有我们想要淘汰的对象放到一个链表中,当一个对象访问就移除链表头部元素,当我们要淘汰元素是就直接淘汰链表尾部开始。

    redis中的LRU:起因

    最初,redis不支持LRU算法。当内存有效性成为一个必须被解决的问题时,后来才加上了。通过修改redis对象结构,在每个key对象增加24bit的空间。没有额外的空间使用链表把所有对象放到一个链表中(大指针),因此需要实现得更加有效,不能因为key淘汰算法而让整个服务改动太大。
    24bits的对象已经足够去存储当前的unxi时间戳。这个表现,被称为“LRU 时钟”,key元数据经常被更新,所以它是一个有效的算法。
    然后,有另一个更加复杂的问题需要解决:如何选择访问间隔最长的key,然后淘汰它。
    redis内部采用一个庞大的hash table来保存,添加另外一个数据结构存储时间间隔显然不是一个好的选择。然而我们希望能达到一个LRU本身是一个近似的,通过LRU算法本身来实现。

    redis原始的淘汰算法简单实现:**当需要淘汰一个key时,随机选择3个key,淘汰其中间隔时间最长的key。**基本上,我们随机选择key,淘汰key效果很好。后来随机3个key改成一个配置项"N随机key"。但把默认值提高改成5个后效果大大提高。考虑到它的效果,你根本不用修改他。

    然而,你可能会想这个算法如何有效执行,你可以看到我们如何捣毁了很多有趣的数据。也许简单的N key,我们会遇到很多好的决策,但是当我们淘汰最好的,下一个周期又开始抓。

    验证规则第一条:用肉眼观察你的算法

    其中有一个观点已经应用到Redis 3.0正式版中了。在redis2.8中一个LRU缓存经常被使用在多个环境,用户关于淘汰的没有抱怨太多,但是很明显我可以提高它,通过不仅仅是增加额外的空间,还有额外的CPU时间。
    然而为了提高某项功能,你必须观察它。有多个不同的方式去观察LRU算法。你可以通过写工具观察,例如模拟不同的工作负载、校验命中率和失误率。
    程序非常简单:增加一些指定的keys,然后频繁地访问这些keys以至于每一个key都有一个下降的空闲时间。最终超过50%的keys被增加,一半的老key需要被淘汰。
    一个完美理想的LRU实现,应该是没有最新加的key被淘汰,而是淘汰最初的50%的老key。

    规则二:不要丢弃重要信息

    借助最新的可视化工具,我可以在尝试新的方法观察和测试几分钟。使用redis最明显有效的提高算法就是,积累对立的垃圾信息在一个淘汰池中。
    基本上,当N keys算法被采用时,通常会分配一个很大的线程pool(默认为16key),这个池按照空闲时间排序,所以只有当有一个大于池中的一个或者池为空的时候,最新的key只会进入到这个池中。
    同时,一个新的redis-cli模式去测量LRU算法也增加了(看这个-lru-test选项)。
    还有另外一个方式去检验LRU算法的好坏,通过一个幂等访问模式。这个工具通常校验用一个不同的测试,新算法工作工作效果好于真实世界负载。它也同样使用流水线和每秒打印访问日志,因此可以被使用不用为了基准不同的思想,至少可以校验和观察明显的速度回归。

    规则三、最少使用原则(LFU算法)


    一切源于一个开放性问题:但你有多个redis 3.2数据库时,而淘汰算法只能在本机选择。因此,假如你全部空闲小的key都是DB0号机器,空闲时间长的key都是1号机器,redis每台机器都会淘汰各自的key。一个更好的选择当然是先淘汰DB1,最后再淘汰DB0。
    当redis被当作缓存使用时很少有情况被分成不同的db上,这不是一个好的处理方式。然而这也是我为什么我再一次修改淘汰代码的原因。最终,我能够修改缓存池包括数据库id,使用单缓存池为多个db,代替多缓存池。这种实现很麻烦,但是通过优化和修改代码,最终它比普通实现要快到20%。
    然而这时候,我对这个redis缓存淘汰算法的好奇心又被点燃。我想要提升它。我花费了几天想要提高LRU算法实现:或许可以使用更大的缓存池?通过历史时间选择最合适被淘汰的key?
    经过一段时间,通过优化我的工具,我理解到:LRU算法受限于数据库中的数据样本,有时可能相反的场景效果非常好,因此要想提高非常非常难。实际上,能通过展示不同算法的图片上看这有点非常明显:每个周期10个keys几乎和理论的LRU算法表现一致。
    当原始算法很难提高时,我开始测试新的算法。 如果我们倒回到博客开始,我们说过LRU实际上有点严格。哪些key需要我们真正想要保留:将来有最大可能被访问,最频繁被访问,而不是最近被访问的key。
    淘汰最少被访问的key算法成为:LFU(Least Frequently Used),将来要被淘汰腾出新空间给新key。
    理论上LFU的思想相当简单,只需要给每个key加一个访问计数器。每次访问就自增1,所以也就很容易知道哪些key被访问更频繁。
    当然,LFU也会带起其他问题,不单单是针对redis,对于LFU实现:
    1、不能使用“移除顶部元素”的方式,keys必须要根据访问计数器进行排序。每访问一次就得遍历所有key找出访问次数最少的key。
    2、LFU不能仅仅是只增加每一访问的计数器。正如我们所讲的,访问模式改变随时变化,因此一个有高访问次数的key,后面很可能没有人继续访问它,因此我们的算法必须要适应超时的情况。
    在redis中,第一个问题很好解决:我们可以在LRU的方式一样:随机在缓存池中选举,淘汰其中某项。第二个问题redis还是存在,因此一般对于LFU的思想必须使用一些方式进行减少,或者定期把访问计数器减半。

    24位的LFU实现

    LFU有它本身的实现,在redis中我们使用自己的24bit来记录LRU。
    为了实现LFU仅仅需要在每个对象额外新增24bit:
    1、一部分用于保存访问计数器;
    2、足够用于决定什么时候将计数器减半的信息;

    我的解决方法是把24bit分成两列:

    16bits8bitslast decr timeLOG_C

    16位记录最后一次减半时间,那样redis知道上一次减半时间,另外8bit作为访问计数器。
    你可能会想8位的计数器很快就会溢出,是的,相对于简单计数器,我采用逻辑计数器。逻辑计数器的实现:

    uint8_t LFULogIncr(uint8_t counter) {
          if (counter == 255) return 255;
          double r = (double)rand()/RAND_MAX;
          double baseval = counter - LFU_INIT_VAL;
          if (baseval < 0) baseval = 0;
          double p = 1.0/(baseval*server.lfu_log_factor+1);
          if (r < p) counter++;
          return counter;
      }

    基本上计数器的较大者,更小的可能计数器会增加:上面的代码计算p位于0~1之间,但计数器增长时会越来越小,位于0-1的随机数r,只会但满足r<p时计数器才会加一。
    你可以配置计数器增长的速率,如果使用默认配置,会发生:

    • 100次访问后,计数器=10;
    • 1000次访问是是18;
    • 10万次访问是142;
    • 100万次访问后达到255,并不在继续增长;

    下面,让我们看看计数器如果进行衰减。16位的被储存为unix时间戳保留到分钟级别,redis会随机扫描key填充到缓存池中,如果最后一个下降的时间大于N分钟前(可配置化),如果计数器的值很大就减半,或者对于值小的就直接简单减半。
    这里又衍生出另外一个问题,就是新进来的key是需要有机会被保留的。由于LFU新增是得分都是0,非常容易被选举替换掉。在redis中,开始默认值为5。这个初始值是根据增长数据和减半算法来估算的。模拟显示得分小于5的key是首选。

    代码和性能

    上面描述的算法已经提交到一个非稳定版的redis分支上。我最初的测试显示:它在幂等模式下优于LRU算法,测试情况是每个key使用用相同数量的内存,然而真实世界的访问可能会有很大不同。时间和空间都可能改变得很不同,所以我会很开心去学习观察现实世界中LFU的性能如何,两种方式在redis实现中对性能的改变。
    因此,新增了一个OBJECT FREQ子命令,用于报告给定key的访问计数器,不仅仅能有效提观察一个计数器,而且还能调试LFU实现中的bug。
    注意运行中切换LRU和LFU,刚开始会随机淘汰一些key,随着24bit不能匹配上,然而慢慢会适应。 还有几种改进实现的可能。Ben Manes发给我这篇感兴趣的文章,描述了一种叫TinyLRU算法。链接

    这篇文章包含一个非常厉害的观点:相比于记录当前对象的访问频率,让我们(概率性地)记录全部对象的访问频率,看到了,这种方式我们甚至可以拒绝新key,同样,我们相信这些key很可能得到很少的访问,所以一点也不需要淘汰,如果淘汰一个key意味着降低命中/未命中率。
    我的感觉这种技术虽然很感兴趣GET/SET LFU缓存,但不适用与redis性质的数据服务器:用户期望keys被创建后至少存在几毫秒。拒绝key的创建似乎在redis上就是一种错误。
    然而,redis保留了LFU信息,当一个key被覆盖时,举个例子:

    SET oldkey some_new_value

    24位的LFU计数器会从老的key复制到新对象中。

    新的redis淘汰算法不稳定版本还有以下几个好消息:
    1、跨DB策略。在过去的redis只是基于本地的选举,现在修复为所有策略,不仅仅是LRU。
    2、易变ttl策略。基于key预期淘汰存活时间,如今就像其他策略中的使用缓存池。
    3、在缓存池中重用了sds对象,性能更好。

    这篇博客比我预期要长,但是我希望它反映出一个见解:在创新和对于已经存在的事物实现上,一种解决方案去解决一个特定问题,一个基础工具。由开发人员以正确的方式使用它。许多redis的用户把redis作为一个缓存的解决方案,因此提高淘汰策略这一块经常一次又一次被拿出来探讨。

    2.6对象

    刚写了redis主要的数据结构:

    动态字符串、双端链表、字典、压缩列表、整数集合、跳表等

    redis肯定不能直接使用这些数据结构来实现数据库,它用这些数据库建立了一个对象系统,包含:

    字符串对象、列表对象、哈希对象、集合对象、有序集合对象

    我们可以针对不同的使用场景,为对象设置多种分不同的数据结构实现,从而优化对象在不同场景下的效率。

    1)键值对

    对于redis的键值对来说:key只有字符串类型,而v可以是各种类型,

    我们习惯把“这个键所对应的值是一个列表”表达为这是一个“列表键。

    TYPE 命令的实现方式也与此类似, 当我们对一个数据库键执行 TYPE 命令时, 命令返回的结果为数据库键对应的值对象的类型, 而不是键对象的类型:

    # 键为字符串对象,值为列表对象
    
    redis> RPUSH numbers 1 3 5
    (integer) 6
    
    redis> TYPE numbers
    list

    2)对象

    我们看一下redis对象的组成:

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

    通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率, 因为 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。

    字符串对象

    字符串对象的编码可以是 int 、 raw 或者 embstr 。

    如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int 。

    如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw 。

    如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。

    embstr 编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject 结构和 sdshdr 结构来表示字符串对象,但 raw 编码会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构,而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构。

     embstr 编码有以下好处:

    1. embstr 编码创建删除字符串对象只需操作一次内存
    2. 因为数据都保存在一块连续的内存, 所以这种编码的字符串对象比 raw 编码字符串对象能更好地利用缓存带来的优势。

    3)列表对象

    列表对象的编码可以是 ziplist 或者 linkedlist 。

    当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:

    1. 列表对象保存的所有字符串元素的长度都小于 64 字节;
    2. 列表对象保存的元素数量小于 512 个;

    不能满足这两个条件的列表对象需要使用 linkedlist 编码。

    4)哈希对象

    哈希对象的编码可以是 ziplist 或者 hashtable 。

    当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

    1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
    2. 哈希对象保存的键值对数量小于 512 个;

    不能满足这两个条件的哈希对象需要使用 hashtable 编码。

    5)集合对象

    集合对象的编码可以是 intset 或者 hashtable 。

    当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

    1. 集合对象保存的所有元素都是整数值;
    2. 集合对象保存的元素数量不超过 512 个;

    不能满足这两个条件的集合对象需要使用 hashtable 编码。

    6)有序集合对象

    有序集合的编码可以是 ziplist 或者 skiplist 。

    当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:

    1. 有序集合保存的元素数量小于 128 个;
    2. 有序集合保存的所有元素成员的长度都小于 64 字节;

    不能满足以上两个条件的有序集合对象将使用 skiplist 编码。

    这里多说两句,各个语言的对象其实都差不多,底层实现也就那几个,比如java中的容器,c++的STL。java的hashset就是一个哈希而已,hashmap就是k带了一个v,而”有序的“Treemap使用了红黑树这种有平衡性的搜索二叉树。

    redis的有序集合并没有再采取hash+红黑树的操作,而是把平衡树换成了跳表,实际上性能真的没差多少,甚至有时比红黑树有优势,比如跳表的性能较为平均,红黑树攒了很多次不平衡要调整可能会带来资源需求的一个高峰,再加上跳表实现简单的优点,红黑树真的没什么优势。

    并且就算是真的想用一种带平衡性的搜索树,现在竞赛也是用的华人之光发明的SB树。

    有序集合的优点就是它的有序操作,比如拿最大最小值,红黑树时间o(logN),而哈希表只能一个一个遍历。缺点在于插入一个值的时间也是o(logN),跳表也是。而哈希表插入数是o(1).

    要了解底层和这些优缺点

    展开全文
  • 这一篇文章要探讨的是“如何用算法实现十进制转十六进制”并不涉及什么特别的知识点。属于C语言基础篇。 在翻找素材的时候,发现篇以前写的挺有意思的代码,篇代码里面涉及的知识点没有什么好讲的,也没有什么...
  • 证书与签名():数字签名是什么

    万次阅读 多人点赞 2017-04-21 18:47:26
      在签名的过程中,有一点很关键,收到数据的一方,需要自己保管好公钥,但是要知道每一个发送方都有一个公钥,那么接收数据的人需要保存非常多的公钥,根本就管理不过来。并且本地保存的公钥有可能被篡改替换,...
  • Redis是款基于键值对的NoSQL数据库,的值支持多种数据结构: 字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。 • Redis将所有的数据都存放在内存中,所以的读写性能十分...
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠

    万次阅读 多人点赞 2021-03-26 11:11:21
    注意事项: 声明一个引用,同时必须初始化,及声明代表哪一个变量。(作为函数参数时不需要初始化) 在声明一个引用后,不能再作为另一变量的引用。 3。不能建立引用数组。 1.2函数调用: 其实还是通过函数来理解...
  • Vertx学习玩意是到底是

    万次阅读 多人点赞 2019-06-22 08:56:13
    Vertx,融合Java、Ruby、Python等语言的高性能架构,架构师必读 原文链接: ... ... 、Vert.x简介 二、Vert.x原理解析 三、Vert牛刀小试 四、Vert应用实践 五、辅助工...
  • CDA数据分析师 出品 作者:Kayvon Tehranian 编译:Mika NFT是什么? 一副纯数字作品为什么...简单地说,NFT是区块链的一个条目,而区块链是类似于比特币等加密货币的去中心化数字账本技术。 因为NFT不可替代的特性.
  • 例如2157的千位是2,百位是1,十位是5,位是7,可以写成:2 x 1000 + 1 x 100 + 5 x 10 + 7 x 1注意,1000是10的立方(即3次幂),100是10的平方(即2次幂),10是10的1次幂,而且10(以及任意正数)的0次幂是1。...
  • 超硬核!小白读了篇文章,就能在算法圈混了

    万次阅读 多人点赞 2021-03-29 10:21:58
    作为只超级硬核的兔子,从来不给你说废话,只有最有用的干货!这些神级算法送给你 目录 第节 1.1bogo排序 1.2位运算 1.3打擂台 1.4morris遍历 第二节 2.1睡眠排序 2.2会死的兔子 2.3矩阵快速幂...
  • 编写一个猜数字游戏,数字范围0~100之间。用户输入,程序提示用户数字猜大了还是猜小了,直到用户才对数字程序才退出 <2>思路分析 用户输入的数据和随机值做对比,如果输入的数据大于随机值就提示猜大了,...
  • 云原生系列二:如何实现跨K8s集群的管理

    千次阅读 多人点赞 2022-07-14 10:28:25
    今天就由叶秋学长带领大家学习云原生专栏...那么他是如何做到,今天我们做一个简单的讲解。首先就是为什么Intuit公司要划分如此多的集群?他们希望在不同的业务部门之间实现隔离,并且各业务部门能够拥有自主权;其次
  • 论补和补码的关系

    千次阅读 多人点赞 2016-12-04 14:32:02
    我们都知道,补 引申到 计算机中 就是 补码的概念, 可是 为什么呢? 我们先来 看看 各自的 定义…… 补: 在日常生活中,常常会遇到补的概念。例如,时钟指示为6点,欲使指示3点,既可按顺时针方向将分针转...
  • 释了分布式数据存储技术的进步和市场激励的强大结合,正合力形成一个更安全、更高效的网络。 在不到三十年的时间里,互联网已成为人类历史上最伟大的成就之一,因为以前在科学、技术和商业领域从未出现过如此看得见...
  • 求两个数的平均值(三种方法)

    千次阅读 2019-08-17 15:36:14
    求两函数的平均值,很简单的想到可以用 (a+b)/2来实现。 代码块如下 方法 int Average_one(int a, int b){ return (a + b) / 2; } 但是如果a或b的值过大,会导致结果溢出。 方法二 把a比b多处的部分,分给b,...
  • 量子计算到底是什么鬼?

    万次阅读 2017-10-18 16:40:38
    几天,朋友圈被刷屏的非马云的云栖大会莫属,在此次大会上,阿里巴巴进行了系列重磅的发布,其中,阿里云联合中国科学院量子信息与量子科技创新研究院(上海)共同宣布“量子计算云平台”上线,应该算是众多发布...
  • 本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?
  • 新名词|什么是「电源」程序员?

    万次阅读 多人点赞 2020-03-27 13:08:07
    什么是计算机系统 计算机系统(A computer system) 是由硬件和软件组成的,它们协同工作运行程序。不同的系统可能会有不同实现,但是核心概念是一样的,通用的。...首先,问你一个问题,你想成为哪种程序员? 是我...
  • 向量和它本身 什么是点积? 两向量相乘,我们应该会想到如下场景: 但这个在现实生活中,用处不大。 但是其他乘法形式很有用。 最重要的是种向量运算方式是内积。也成为点积。 叫点积,是因为我们通常...
  • 科普文章-另一个视角解读计算机编码(修订版)

    万次阅读 多人点赞 2017-06-17 17:59:55
    我不知道本文该作为原创发布还是作为转载发布,因为本文是《另一个视角解读计算机编码-补码编码》的“排版后的版本”,内容几乎没有变,除了...为什么要整理篇文章时间过得太快,在我写下《另一个视角解读计算机编
  • 出现次的数字(找单身狗)

    千次阅读 2020-12-10 17:00:21
    这里我们首先了解二进制位异或的一些性质 1.相同数字异或为0.(0 ^ 0 = 0, 1 ^ 1 = 0) 2.相反数字异或为1.(1 ^ 0 = 1) 3.和 0 异或结果不变( 1 ^ 0 = 1 , 0 ^ 0 = 0) 4.和 1 异或结果相反( 1 ^ 1 = 0, 0 ^ ...
  • 大数据经过10年发展,现在已经到了一个重要的分水岭阶段:通用性和兼容性能力成为大数据发展主流,运行的稳定可靠和使用的简捷、易开发、易维护成为产品发展的驱动力,而正是 Hadoop/Spark 类积木式模块框架无法...
  • 机器数是计算机中参与运算且带有(+)、(-)属性的二进制数 –引用自王达老师的《深入理解计算机网络》 1、计算机中的数如何表示符号?(计算机的数也是存在正负之分的,那么如何区分它们的正负呢?) 我之前的博文介绍...
  • 在Java中,负数的绝对值竟然不一定是正数!!!

    千次阅读 多人点赞 2021-07-26 10:40:44
    绝对值是指一个数在数轴上所对应点到原点的距离,所以,在数学领域,正数的绝对值是这数本身,负数的绝对值应该是他的相反数。 这几乎是每个人都知道的。 在Java中,想要获得有个数字的绝对值,可以使用java.lang....
  • 首先,阅读篇文章的你,肯定是一个在网上已经纠结了很久的读者,因为你查阅了所有你能查到的资料,然后他们都会很耐心的告诉你,补码:就是按位取反,然后加一。准确无误,毫无破绽。但是,你搜遍了所有俯拾即是...
  • 必须知道的python专属骚技巧25例

    万次阅读 多人点赞 2021-03-16 11:17:43
    Python 提供了一个直观的在一行代码中赋值与交换(变量值)的方法 x, y = 10, 20 print(x, y) x, y = y, x print(x, y) #1 (10, 20) #2 (20, 10) 原理:赋值的右侧形成了一个新的元组,左侧立即解析...
  • 设计一个彩灯循环控制器

    万次阅读 多人点赞 2017-06-30 22:36:35
    设计一个彩灯循环控制器二、课程设计的要求与数据 设计要求包括: 1.10路彩灯分别用10个发光二极管L0、L1…..L9模拟,发光二极管L0、L1…..L9从左到右排列。 2.要求显示四种不同的花型:1)10路彩灯按照L0、L1…....
  • 由 于假想了一个小数点的位置,而且个位置是约定好就不变的,所以叫做定点 定点的表示:原码、反码、补码   原码  原码的存在是最自然而然的。把人类书写的按转换成二进制,正常情况下,转换成的二...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 202,781
精华内容 81,112
关键字:

一个相反数是它本身这个数是