精华内容
下载资源
问答
  • 前面发布了父亲和母亲的计算,再补充感恩,感恩为每年的11月的第四星期四,下面会展示代码。...公历11月第4周四是感恩(Thanksgiving Day)。是美国和加拿大共有的节日,由美国人民

    前面发布了父亲节和母亲节的计算,再补充下感恩节,感恩节为每年的11月的第四个星期四,下面会展示代码。

    原文链接:免费天气API,全国天气 JSON API接口,可以获取五天的天气预报

    2017年感恩节的日期:

    2017年11月23日 星期四

    (丁酉年(鸡年)十月初六)

    公历11月第4个周四是感恩节(Thanksgiving Day)。是美国和加拿大共有的节日,由美国人民独创,原意是为了感谢上天赐予的好收成、感谢印第安人的帮助。详情>>

    万年历——Java计算母亲节是哪天?母亲节是哪一天?

    万年历——Java计算父亲节是哪天?父亲节是哪一天?

    Java计算感恩节代码

        /**
         * 
         * 开发公司:SOJSON在线工具 <p>
         * 版权所有:© www.sojson.com<p>
         * 博客地址:http://www.sojson.com/blog/  <p>
         * <p>
         * 
         * Java 感恩节计算
         * 
         * <p>
         * 
         * 区分 责任人 日期    说明<br/>
         * 创建 周柏成 2017年5月20日  <br/>
         *
         * @author zhou-baicheng
         * @email  i@itboy.net
         * @version 1.0,2017年5月20日 <br/>
         * 
         */
        public static void main(String[] args) {
        	//计算2017年的感恩节
        	int year = 2017;
        	
        	//计算感恩节,感恩节为每年的11月的第四个星期四
        	Calendar cal = Calendar.getInstance();
        	cal.set(Calendar.YEAR, year);
        	//因为从0开始,所以减1
        	cal.set(Calendar.MONTH, 11-1);
        	int maxDate = cal.getActualMaximum(Calendar.DATE);
        	int sundays = 0;
            for(int i = 1; i <= maxDate; i ++) {
                cal.set(Calendar.DATE, i);
                //判断是周四,
                if(cal.get(Calendar.DAY_OF_WEEK) == 4+1) {
                    sundays ++;
                    //第4个周四,返回
                    if(sundays == 4) {
                        break;
                    }
                }
            }
            String date = new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime());
            System.out.printf("%s年的感恩节是:%s",year,date);
            //2017年的感恩节是:2017-11-23
        }


    展开全文
  • 在亚信实习的日子——第一个月

    千次阅读 2020-06-30 19:52:23
    想起2个月前在Boss上海投简历的自己,想起现在的自己,想起当初顶着压力选择有“亚信男”这个“名声在外”的一家上市公司……总是要说点什么,记录自己这一个月。(PS:毕竟第一次从学校步入社会,顺利成为社会底层...

    在亚信实习的日子——第一个月

    哔哔赖赖

    2020,注定是个糟糕的一年,疫情爆发,让我这大3准大4,不,现在应该也可以说大4的2021年毕业生属实是焦虑起来。

    想起2个月前在Boss上海投简历的自己,想起现在的自己,想起当初顶着压力选择有“亚信男”这个“名声在外”的一家上市公司……总是要说点什么,记录下自己这一个月。(PS:毕竟第一次从学校步入社会,顺利成为社会底层娃)。

    看到类似的亚信实习记录文章,可以概括为技术好的选择互联网公司吧,别选亚信这种外包……可能不直白,换一种说法吧,技术好的别考虑,技术中等的看运气(看你分到的项目组和你的导师),技术差的可以来,前提要过面试。那,谈谈自己吧,疫情当前,我在4.5月份陆续过了4家公司的实习生,北京的软通动力、南京的亚信、杭州的什么来着,还有一家不知名的企业,总体来说吧,自己的水平和自己过的公司还算匹配,因算法欠缺,阿里,腾讯这种大厂通过“曲线救国”过去吧,所以,我本人内心是不排斥外包的,虽然它可能不好,但是对于自己这种普通本科毕业的在校小有成就的“有志青年”来说,外包是走到阿里腾讯这种大厂“最好的选择”。

    进入正文吧。

    第一周

    5.26

    报道篇,见张小三,如果是男的,叫张哥,如果是女的叫小三姐,可能张小三这个名字起的甚不合适,不过,别在意这些细节,早上报道,见hr,见部门经理,见导师,停停停停,这里夸一下,我导师是我见过最帅的,这我不接收反驳,然后接下来经理讲一讲部门的情况,业务,听到js用的比较多,完了,天塌了,自己最薄弱的就是js,本人一直觉得前端比后端要难,个人看法,请别喷我,因为在入职后2周的会议上,听到,前端是百进一的比例选的实习生,好吧,更坚信我的观点了,前端牛逼,而我只是个“门都没入的后端开发”。而我入的部门读者请原谅我,因我自己不确定是不是公司机密,我暂用A部门来表达,总之,是个不错的部门,虽然A部门还挺忙的,但是我过的也充实,实习生的样子不应该是划水划水摸鱼摸鱼再摸鱼吧,应该是加班加班再加班吧,可能大多数人都不喜欢加班,但是实习么?对我来说,实习能多学点还是我很喜欢的,加班?脱发?好吧,说到这,留下了我不争气的眼泪,摸摸自己的头发,怎么没有“毛发”了。

    今天是来的第一天,所以,就是搭建办公环境,初略的看一下部门业务,然后脑袋瓜现在或者以后都要记住的一句话,git操作你别乱,先pull再push,若你没pull就push,出了重大事故,要我小命。(PS:要我小命,我自己天添油加醋的,不过git这一点真的很重要)。那,客官,不知道你看累了没,我继续码字了,继续看接下来的日子。当然,不可能像开篇这样一天一天记录吧,一个月30天,写30段话,原谅我没有这个时间,不如让我去学习,呸,不如让我去休息会。

    5.27

    测试环境,灰度,生产……这些在校期间接触不到的名词,好吧,焦虑也兴奋。原谅我是个实习生,5.27真的算是划水一天吧……当然了,下文如果我哪天再划水,我就不会再点出来了,毕竟总说自己的不好,那岂不是自己看不起自己……

    5.28

    可能是A部门太忙吧,在5.28这天我还没意识到,现在意识到了,不过我可不是你想的那样,很难受,我更多的是开心,忙,说明是前线部门,忙,我才能学到东西,在需求中成长,好,需求二字,对,没错。在我实习的第3天,给我安排了一个需求,大概是更换一个字段的名字,在老员工看来很简单,在我现在看来也还可以,但是在第3天的我看来,我蒙了,我的眼前没有太阳了,只有这无尽的黑夜。在接下来的3天,问这个问那个,到处碰壁,也首次加班到晚上22:00之后,不过好在按时把需求搞定了,这个具体经历了多大压力,原谅我,文字总是不能来量化它,但是这是我成长的几天,确实成长了,这个下文再说。

    5.29-6.2

    ……//TODO 你是不是在好奇,这段话为什么这样,因为这个时间点,给我安排了单元测试工作,虽然我学会了Mock数据,学会了JAVA反射测试访问私有方法,但是,如大多数人一样,单元测试,不喜欢。我不管,反正我是不喜欢。生气,这段话到此为止。看接下来的日子。

    第二周

    6.3-6.5

    AJAX+前后端接口调用,如这所说,这几天就在反复训练这个,个人觉得自己ajax学的还不错,但是那也是自己觉得,来了公司才发现,自己曾经在学习拿的荣誉做的项目都算什么,哎呀,自嘲一下,其实自己也没有那么不堪,但是自己确实是差,要不然何必窝在这小地方受着鸟气……说归说,A部门绝对是个好部门,我还是很满意的,老员工都很好,自己能学的技术也很多,也会给自己分一点需求,好吧,ajax实在是没有什么好说的,就看下一段吧。

    第三周

    6.8-6.12

    第三周,自己终于入项了,有了自己的研发云,云桌面,还是比较开心的,这周呢,接触到了一些高级的东西,看公司z框架,x框架,c框架……看公司接口规范,当然会的多了,加班也就来了,每天晚上加班那也是家常便饭,不过我喜欢这种生活。

    第四周

    6.15-6.19

    本地造报文,CSF查问题,解决了2个需求,单元测试覆盖率……这些就别让我细聊了,实在是没有时间在此闲聊,让我们继续往下看好么。

    第五周

    6.22-6.28

    看到这一周,你可能看到我的文章标题质疑我了,但是第五周我确实做了很多事情,你敢想么?我解决了9个需求,系统了学了Git操作,负责某某省份联调业务,等等吧,这一周恰逢端午节,其他部门据我了解我的同学都没有粽子发,而我发了,所以我还是很喜欢我的部门的,包括今天码这篇文章,6.30号,我们部门给每个人发了套衣服,包括我这个卑微的实习生……

    那就这样吧,下篇文章,见。

    一个月后见。

    ——来自河南济源普通的实习生

    展开全文
  • 查询数据 redis的应用场景: 网站高并发的主页数据 网站数据的排名 消息订阅 1.3redis——数据结构和对象的使用介绍 redis官网 微软写的windows的redis 我们下载第一个 额案后基本一路默认就行了 安装后,服务...

    后端需要知道的关于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).

    要了解底层和这些优缺点

    展开全文
  • 火红的夏日,2018年苏宁818发烧购物自上周已经开始全面打响。这场由苏宁主导的全民购物消费狂欢,将历时27,搅动零售领域,尤其是在家电市场,其激战正酣。 就在近几日...
        

    640?wx_fmt=jpeg

    火红的夏日,2018年苏宁818发烧购物节自上周已经开始全面打响。这场由苏宁主导的全民购物消费狂欢,将历时27天,搅动零售领域,尤其是在家电市场,其激战正酣。

     

    就在近几日,华为消费者业务CEO余承东、联想集团执行副总裁兼中国区总裁刘军、海尔集团副总裁李华刚、创维-RGB董事长王志国等现身苏宁,可以说家电领域的品牌商大佬齐聚于此,要将今年苏宁818做出新高度。

     

    回顾整个家电零售业发展,渠道商和品牌商的对接是链条上的核心。电商玩家要玩转整个零售行业,最根本的就是撬动品牌商。所以,这也是苏宁818要拿下的制高点。

    8月1日开门红,苏宁家电全渠道销量同比增长121%,65英寸及以上彩电激增393%,空调单天销售突破27万套,家电第一主场强势爆发。

    最新的进展是,日前,国际知名数据调研公司GfK发布监测数据:2018上半年,主力传统家电市场(彩电、空调、冰箱、洗衣机)量额齐增,全渠道市场规模超2000亿元。其中,苏宁零售规模超450亿元,全渠道市场份额占比22.5%,同比增速远超市场,持续领跑全渠道第一。

    640?wx_fmt=jpeg

    就在今年731日,苏宁零售集团副总裁范志军也在北京表示,目前苏宁家电在全渠道份额已经占据第一,年底将冲刺全渠道30%的市场份额。整个零售的细分家电领域,苏宁无疑是老大,毕竟这也是苏宁的发家之根本,相当于看家利器。


    经过此番的818大会战,苏宁在家电的全渠道市场份额能冲高到多少,还说不好,但最起码苏宁家电第一的优势将进一步放大。而其具体的打法更值得剖析。


    1、用智慧零售新场景,犁一遍家电零售的土地640?wx_fmt=png

    818的发布会上,苏宁易购总裁侯恩龙表示,“这个夏天,彻底告别纯电商购物节,苏宁要用智慧零售新场景,犁一遍零售的土地。”

    640?wx_fmt=jpeg


    和以往的电商造节大促相比,或拼价格,或秀技术,而今年苏宁818则是讲场景,家电领域的角逐也是沿着这一路线。


    而要通过场景来再次引爆,就需要线上和线下的联动渠。特别是,如今线下渠道获得价值重估机会。

    根据GfK数据指出,2018年上半年,线下市场零售额比重达到70%,线下渠道依然是家电市场主力。苏宁双线优势在家电零售重构中有了更大的想象空间。

    公开资料显示,目前,苏宁在全国线下拥有各类门店数量已超5000家,苏宁在北京有各业态门店273家,年底这一数量将飙升至850家,是北京家电市场门店数量最多、分布最广、渠道结构最为完善,同时也拥有线上线下协同发展、销售规模最大的家电零售渠道。


    此外,苏宁已形成全国无盲区的四级家电智慧门店网络,各类型门店覆盖一二线城市、三四线城市、五六线城市、乡镇农村等市场。在线上,苏宁易购也正在以全网最高的增速在高速成长。伴随苏宁易购各业态的落地,这给苏宁家电带来了聚盆效应。

     

    这些也必将成为这次818,苏宁在家电领域再度出击的支撑。

    在我看来,苏宁818电商购物狂欢节已经搞了许多年了,其正在形成一个呼啸而来、试图裹挟新的商业势能。所以,身处行业内,我们不能简单的只看每年的交易金额,而是要看具体的新的动向,要看是否升华到更高层面博弈之上。这才是这次818苏宁新场景化的论调的深层次意义。


    2、服务之战、高端之战!640?wx_fmt=png

    今年818为期27天,伴随着818的打响,一场年度最大的家电市场份额争夺战也随之打响。

    640?wx_fmt=jpeg

    具体而言,除了最实惠的家电优惠、围绕总的场景战略外,服务战、高端战将是苏宁放大家电优势的重磅举措。

    首先来说服务,毕竟家电都是大件,服务至关重要。

    现实中,家电服务领域乱象丛生。7月初,被新华社、人民日报、央视先后报道的上海消保委做的空调维修体察报告中显示:在百度、淘宝、京东、360、58同城、大众点评等11家平台上联系的空调维修服务商中,有9家存在着虚构电脑板故障、虚构缺少制冷剂、虚构电子元件故障和高价兜售遥控器等行为,仅苏宁和百联能够提供正常服务并合理收费。

    所以,对此,苏宁将联合各家机构启动大家电售后服务标准化建设工程,在服务上推动家电行业升级,让原有的用户购买家电更放心,性价比更高。

    其次,在家电零售的博弈上,高端化是另一个着力点。GfK的统计数据表明,今年上半年,6000元以上高端家电产品线下零售额占比高达87%。

    眼下,我国消费者正在步入小康生活阶段,家电行业也从传统的基础型家电消费,向追求品质、健康的高端升级型家电时代迈进。

    伴随消费升级趋势持续,6000元以上高端家电产品零售额份额贡献占比提升至23%,苏宁高端产品贡献占比高达27%,高于行业占比。

    在今年818主场战役下,苏宁的高端家电优势将进一步凸显,也会将对手的份额进一步压缩。

     

    【结束语】

    总之, 2018年整个家电行业开始从原来的增量市场到存量市场的争夺。从今年上半年的市场情况来看,因为受到房地产调控滞后效应的深度影响,家电行业的增量市场、新增需求大幅度收缩。

    但作为家电领域最重要的电商购物节,广大消费者都会选择在苏宁“818发烧节”这段时间内“剁手”。对于很多家电厂商来说,接下来重点就是利用技术和产品的快速升级迭代,向存量用户要市场、要蛋糕。这时候,借助苏宁818的力量就成为不二之选。

     

    前方,818高潮即将到来,届时苏宁自然会搅动整个家电行业,不妨静待高潮来临。


    老胡说科技

    ______

    ID:laohushuokeji


    640?wx_fmt=jpeg

    长按图片,识别图中二维码,

    一起关注科技领域干货!

    你的分享和关注,是我前进的动力。

    运营人:老胡,知名互联网评论人士,观察家。沉积科技领域13年,历任《IT时代周刊》主笔,腾讯科技首席记者,迅雷、滴滴公关总监;写过大型书籍《黑马首富王传福》;

    他是中国科技自媒体top 10,2016自媒体人百强;【具备创造:10万+阅读,文章能力】;文章见今日头条、界面、钛媒体、新浪科技、搜狐、腾讯、网易、艾瑞等60余家顶级专栏,单篇文章曝光量百万,整体文章阅读量超过10亿;文章转载与商务合作请加个人微信号:taiyangfeixue。
     


    展开全文
  • 今天在看开源中国时看到别人写的一个demo很帅啊,是一个垂直方向展示的弹出菜单,链接地址为:IOS弹出式菜单KxMenu 同时文中也附上了github的地址,在此热泪感谢原作者,我们来试用一下。 因为学习了也有段日子...
  • 那么阿里云是一个什么样的企业,为什么能成为行业第一,以下相关介绍获取可以说明。 一:阿里云简介 阿里云创立于2009年,是全球领先的云计算及人工智能科技公司,致力于以在线公共服务的方式,提供安全、可靠的...
  • 当碰到休市的情况,股票日线数据就是缺失的,此时不能用每个月的1号作为当月交易的第一天。那该如何确定呢?答案是使用重采样函数resample,以dataframe对象df为例,df内容如下: 这个时候,如何获取当月交易的...
  • 从七初至今,一个半月我做了什么???

    千次阅读 热门讨论 2020-08-16 00:20:05
    第一段:7.1->7.13,把C++整体学了一遍,总共发了五篇博客。 中间经历了一些其它的事,所以SQL Server学了好久: 第二段:7.14->7.23,把SQL整体学了一遍,写了两篇博客。 第二.一段:7.16->7.21,由于学校...
  • 计算机网络自顶向方法 习题参考答案 第一

    万次阅读 多人点赞 2018-09-07 21:26:58
    没有什么不同。PC、服务器、手机等。Web服务器是端系统。 R2. Protocol is commonly described as a set of international courtesy rules. These well-established and time-honored rules have made it ...
  • 宅男也可变形男-我是如何在11个月零27减掉80斤的

    万次阅读 多人点赞 2016-06-16 12:39:57
    对我人生态度起到决定性的一个事件这事情说起来也是源于98年的次初中同学聚会,在我的“一切源于台笔记本”里我提到过,当时我是中专毕业。早早的工作了。96年11月份开始工作,而当时的初中同学们都还在读高中...
  • 一个普通专科生,拿什么拯救你的未来?(精简版)

    万次阅读 多人点赞 2021-03-09 17:05:00
    一个普通专科生,拿什么拯救你的未来?(精简版) 总有人要赢,为什么不能是我!————— 科比-布莱恩特 原文地址:www.dushunchang.top 此文为小Du博客原创出品 转载,复制请注明原文出处 近来看到则知乎头条,...
  • 这里是《齐姐聊大厂》系列的 11 篇 (前 10 篇见文末) 每周五早上 8 点,与你唠唠大厂的那些事 小齐说: 这篇文章来自去年一起刷题跳槽的小当家大佬,大佬手握 N offer,先是被西雅图的悠闲生活所打动,可去...
  • 爱情礼物的选择不仅仅是情侣恋人之间,还包括夫妻、情人之间以及即将恋爱者的第一次约会。其送礼的场合不外乎日常和庆。而庆,生日送礼和结婚纪念日送礼以及七夕、情人是必不可少之外。近年来网上流行的几种...
  • 所谓活着并不是单纯的呼吸,心脏跳动,也不是脑电波,而是在这个世界上留下痕迹。 要能看见自己一路走来的脚印,并确信那些都是自己留下的印记,这才叫活着。...上个月的今天我发了我在博客的第一篇...
  • 目录 python爬虫常用库之bs4 安装 安装 Requests pip install requests 获得源码 包安装: XPATH和元素节点获取...bs4全名BeautifulSoup,是编写python爬虫常用库之,主要用来解析html标签。   安装 ...
  • Linux系统&...(3+5) Linux系统  操作系统的发展简史(命令行交互--&gt;鼠标交互--&gt;触摸屏交互--&gt;语音交互)  内核   :7组件  shell  shell命令  通配符  ...
  • 跟放假休息的各位一样,元旦假期的营长着实也不想干活……想起前两天刚刚更新的《黑镜》第四季还没有跟,营长便决定在新年的第一天恶补一下科技和AI的黑暗面。第1集,《联邦星舰卡利斯特号》:“柯克船长”——咳,...
  • 1752年9什么少了11

    千次阅读 2019-02-16 00:26:44
    这是为什么呢,感觉挺有趣的,就查了, 究其原因:cal 指令源自美国 AT&amp;T 的 UNIX,也因此继承了美国的历史。时间回到 1752 年。1752年9大英帝国极其所属美洲殖民地的恺撒历法被格里高利教皇历法所取代...
  • 入学一月

    千次阅读 多人点赞 2016-02-08 06:32:20
    不觉间已经来美国快一个月了,感觉还像一场梦。从以前熟悉的生活圈子,每天起来朝九晚五地上班下班,回家吃饭看电视看书,到现在每天一睁眼从陌生的环境醒来,点点滴滴,要学习的很多。来了之后的第一大问题就是时差...
  • 贵州支教之(1111日)

    万次阅读 多人点赞 2011-12-15 10:02:19
    2011年11的7日至11日,对别人来说只是每年52星期中一个普通的星期。然而对于我来说,却是意义非凡的一个星期。  在这星期里,已经阔别学校快10年的我,又回到了校园里;  在这星期里,做了十几年学生的...
  • 贵州支教之(1110日)

    万次阅读 多人点赞 2011-12-13 09:50:36
    2011年11的7日至11日,对别人来说只是每年52星期中一个普通的星期。然而对于我来说,却是意义非凡的一个星期。  在这星期里,已经阔别学校快10年的我,又回到了校园里;  在这星期里,做了十几年学生的...
  • 今年,我这程序猿,最终还是一个人过情人 文章目录聊聊星座与生肖网站来首诗代码最后 貌似出生到现在,我都是一个人在过情人??? 聊聊星座与生肖 有粉丝问我今年怎么没写总结。去年的总结,过完年再写(我...
  • 题目的背后,它透露出这样种情绪,股对农村生活清心寡欲的期望,股对城市工作奋斗打拼的厌倦。
  • (10分) E 路径 (15分) 编程题 F 时间显示 (15分) G 砝码称重 (20分) H 杨辉三角形 (20分) I 双向排序 (25分) J 括号序列 (25分) 填空题 A 空间 (5分) 问题描述 小蓝准备用 256 256 256 MB 的内存空间开一个数组,...
  • 全球化背景的超20多国家地区共同实施的SAP项目概览
  • [期待岁月能温柔,陪伴母亲到白...因为放假的第一天是正清明,而我还在回家的高铁上,来不及去扫墓,放假的第二天我们还要送妈妈去医院复查(正好到了医院说复查的日期了),复查一天,然后再加上其它事情一天,第四...
  • 上海程序员的一天工作日志

    千次阅读 2012-11-25 12:09:22
    每天清晨,当地铁 2号线穿过黄浦江,在车厢里便随处可见这样一群男青年:背着黑色电脑包,穿着不知品牌的深色... 在这“白骨精”(白领、骨干、精英)女人鼓吹“好女愁嫁”的年代里,这群“园区男”却在“深闺”里过
  • 一天天看着好起来 能下床走 其实母亲还能走路 我心里已经很满足了 6的时候 这边雨天 爷爷出去结果磕了一脚 把腿骨折了 于是又 去了医院 拍片 医院非要给做手术 爸爸去找了医院的亲戚 他说年纪这么大了 没...
  • 1.确定公历节日信息实现了 2.第几周第几还需要继续实现,大致思路是参照网上的: ... "0150 世界麻风日", // 一的最后一星期日(倒数第一个星期日) "0520 国际母亲", "0530 全国助残日", "0630
  • 学习ARToolkit已经快一个月了,刚刚接触时只是按照官网教程中的步骤一步一步的学习,其中遇到了许多问题,如OpenGL在64位Win7的配置问题、ARtoolkit在64位Win7的配置问题、如何使用VS2010创建个自己的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 62,446
精华内容 24,978
关键字:

下个月的第一天是什么节