久久精品国产精品国产精品污,男人扒开添女人下部免费视频,一级国产69式性姿势免费视频,夜鲁夜鲁很鲁在线视频 视频,欧美丰满少妇一区二区三区,国产偷国产偷亚洲高清人乐享,中文 在线 日韩 亚洲 欧美,熟妇人妻无乱码中文字幕真矢织江,一区二区三区人妻制服国产

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

《这是全网最硬核redis总结,谁赞成,谁反对?》六万字大合集

發(fā)布時間:2023/12/20 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《这是全网最硬核redis总结,谁赞成,谁反对?》六万字大合集 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

我攤牌了,這篇文章,值得99%的人收藏

此文后續(xù)會改為粉絲可見,所以喜歡的請?zhí)崆瓣P(guān)注和收藏,不迷路。

最近有五本我喜歡的redis實體新書,想要的去評論,我寫個隨機(jī)數(shù)抽獎包郵送給你。

?

?

那么,準(zhǔn)備好了嗎?我們開始吧。

《三天給你聊清楚redis》第1天先嘮嘮redis是個啥(18629字)

一、入門

Redis是一款基于鍵值對的NoSQL數(shù)據(jù)庫,它的值支持多種數(shù)據(jù)結(jié)構(gòu):
字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
? Redis將所有的數(shù)據(jù)都存放在內(nèi)存中,所以它的讀寫性能十分驚人,用作數(shù)據(jù)庫,緩存和消息代理。

Redis具有內(nèi)置的復(fù)制,Lua腳本,LRU逐出,事務(wù)和不同級別的磁盤持久性,并通過Redis Sentinel和Redis Cluster自動分區(qū)提供了高可用性。
? Redis典型的應(yīng)用場景包括:緩存、排行榜、計數(shù)器、社交網(wǎng)絡(luò)、消息隊列等

1.1NoSql入門概述

1)單機(jī)Mysql的美好時代

瓶頸:

?

  • 數(shù)據(jù)庫總大小一臺機(jī)器硬盤內(nèi)存放不下
  • 數(shù)據(jù)的索引(B + tree)一個機(jī)器的運(yùn)行內(nèi)存放不下
  • 訪問量(讀寫混合)一個實例不能承受

?

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

通過緩存來緩解數(shù)據(jù)庫的壓力,優(yōu)化數(shù)據(jù)庫的結(jié)構(gòu)和索引

垂直拆分指的是:分成多個數(shù)據(jù)庫存儲數(shù)據(jù)(如:賣家?guī)炫c買家?guī)?#xff09;

?

3)MySql主從復(fù)制讀寫分離

  • 主從復(fù)制:主庫來一條數(shù)據(jù),從庫立刻插入一條。
  • 讀寫分離:讀取(從庫Master),寫(主庫Slave)
  • ?

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

  • 主庫的寫壓力出現(xiàn)瓶頸(行鎖InnoDB取代表鎖MyISAM)
  • 分庫:根據(jù)業(yè)務(wù)相關(guān)緊耦合在同一個庫,對不同的數(shù)據(jù)讀寫進(jìn)行分庫(如注冊信息等不常改動的冷庫與購物信息等熱門庫分開)
  • 分表:切割表數(shù)據(jù)(例如90W條數(shù)據(jù),id 1-30W的放在A庫,30W-60W的放在B庫,60W-90W的放在C庫)
  • ?

    MySql擴(kuò)展的瓶頸

  • 大數(shù)據(jù)下IO壓力大
  • 表結(jié)構(gòu)更改困難
  • 常用的Nosql

    Redis
    memcache
    Mongdb
    以上幾種Nosql 請到各自的官網(wǎng)上下載并參考使用

    Nosql 的核心功能點

    KV(存儲)
    Cache(緩存)
    Persistence(持久化)
    ……

    1.2redis的介紹和特點:

    ? ? ? ?問題:
    ? ? ? ? ? ? ? ?傳統(tǒng)數(shù)據(jù)庫:持久化存儲數(shù)據(jù)。
    ? ? ? ? ? ? ? ?solr索引庫:大量的數(shù)據(jù)的檢索。
    ? ? ? ? ? ? ? ?在實際開發(fā)中,高并發(fā)環(huán)境下,不同的用戶會需要相同的數(shù)據(jù)。因為每次請求,
    ? ? ? ? ? ? ? ?在后臺我們都會創(chuàng)建一個線程來處理,這樣造成,同樣的數(shù)據(jù)從數(shù)據(jù)庫中查詢了N次。
    ? ? ? ? ? ? ? ?而數(shù)據(jù)庫的查詢本身是IO操作,效率低,頻率高也不好。
    ? ? ? ? ? ? ? ?總而言之,一個網(wǎng)站總歸是有大量的數(shù)據(jù)是用戶共享的,但是如果每個用戶都去數(shù)據(jù)庫查詢
    ? ? ? ? ? ? ? ?效率就太低了。
    ? ? ? ?解決:
    ? ? ? ? ? ? ? ?將用戶共享數(shù)據(jù)緩存到服務(wù)器的內(nèi)存中。 ? ? ? ?
    ? ? ? ?特點:
    ? ? ? ? ? ? ? ?1、基于鍵值對
    ? ? ? ? ? ? ? ?2、非關(guān)系型(redis)
    ? ? ? ? ? ? ? ? ? ? ? ?關(guān)系型數(shù)據(jù)庫:存儲了數(shù)據(jù)以及數(shù)據(jù)之間的關(guān)系,oracle,mysql
    ? ? ? ? ? ? ? ? ? ? ? ?非關(guān)系型數(shù)據(jù)庫:存儲了數(shù)據(jù),redis,mdb.
    ? ? ? ? ? ? ? ?3、數(shù)據(jù)存儲在內(nèi)存中,服務(wù)器關(guān)閉后,持久化到硬盤中
    ? ? ? ? ? ? ? ?4、支持主從同步
    ? ? ? ? ? ? ? ?實現(xiàn)了緩存數(shù)據(jù)和項目的解耦。
    ? ? ? ?redis存儲的數(shù)據(jù)特點:
    ? ? ? ? ? ? ? ?大量數(shù)據(jù)
    ? ? ? ? ? ? ? ?用戶共享數(shù)據(jù)
    ? ? ? ? ? ? ? ?數(shù)據(jù)不經(jīng)常修改。
    ? ? ? ? ? ? ? ?查詢數(shù)據(jù)
    ? ? ? ?redis的應(yīng)用場景:
    ? ? ? ? ? ? ? ?網(wǎng)站高并發(fā)的主頁數(shù)據(jù)
    ? ? ? ? ? ? ? ?網(wǎng)站數(shù)據(jù)的排名
    ? ? ? ? ? ? ? ?消息訂閱


    1.3redis——數(shù)據(jù)結(jié)構(gòu)和對象的使用介紹? ??
    ? ? ? ?

    redis官網(wǎng)

    微軟寫的windows下的redis

    我們下載第一個

    額案后基本一路默認(rèn)就行了

    安裝后,服務(wù)自動啟動,以后也不用自動啟動。

    出現(xiàn)這個表示我們連接上了。

    ?

    redis命令參考鏈接

    1.3.1String

    數(shù)據(jù)結(jié)構(gòu)

    struct sdshdr{//記錄buf數(shù)組中已使用字節(jié)的數(shù)量int len;//記錄buf數(shù)組中未使用的數(shù)量int free;//字節(jié)數(shù)組,用于保存字符串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>

    應(yīng)用場景

    String是最常用的一種數(shù)據(jù)類型,普通的key/value存儲都可以歸為此類,value其實不僅是String,也可以是數(shù)字:比如想知道什么時候封鎖一個IP地址(訪問超過幾次)。INCRBY命令讓這些變得很容易,通過原子遞增保持計數(shù)。

    1.3.2LIST

    數(shù)據(jù)結(jié)構(gòu)

    typedef struct listNode{//前置節(jié)點struct listNode *prev;//后置節(jié)點struct listNode *next;//節(jié)點的值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"

    應(yīng)用場景

    Redis list的應(yīng)用場景非常多,也是Redis最重要的數(shù)據(jù)結(jié)構(gòu)之一。
    我們可以輕松地實現(xiàn)最新消息排行等功能。
    Lists的另一個應(yīng)用就是消息隊列,可以利用Lists的PUSH操作,將任務(wù)存在Lists中,然后工作線程再用POP操作將任務(wù)取出進(jìn)行執(zhí)行。

    1.3.3HASH

    數(shù)據(jù)結(jié)構(gòu)

    dictht是一個散列表結(jié)構(gòu),使用拉鏈法保存哈希沖突的dictEntry。

    typedef struct dictht{//哈希表數(shù)組dictEntry **table;//哈希表大小unsigned long size;//哈希表大小掩碼,用于計算索引值unsigned long sizemask;//該哈希表已有節(jié)點的數(shù)量unsigned long used; }typedef struct dictEntry{//鍵void *key;//值union{void *val;uint64_tu64;int64_ts64;}struct dictEntry *next; }

    Redis的字典dict中包含兩個哈希表dictht,這是為了方便進(jìn)行rehash操作。在擴(kuò)容時,將其中一個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操作并不是一次性完成、而是采用漸進(jìn)式方式,目的是為了避免一次性執(zhí)行過多的rehash操作給服務(wù)器帶來負(fù)擔(dān)。

    漸進(jìn)式rehash通過記錄dict的rehashidx完成,它從0開始,然后沒執(zhí)行一次rehash例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],這一次會把 dict[0] 上 table[rehashidx] 的鍵值對 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。

    在 rehash 期間,每次對字典執(zhí)行添加、刪除、查找或者更新操作時,都會執(zhí)行一次漸進(jìn)式 rehash。

    采用漸進(jìn)式rehash會導(dǎo)致字典中的數(shù)據(jù)分散在兩個dictht中,因此對字典的操作也會在兩個哈希表上進(jìn)行。
    例如查找時,先從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"

    應(yīng)用場景

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

    1.3.5ZSET

    數(shù)據(jù)結(jié)構(gòu)

    typedef struct zskiplistNode{//后退指針struct zskiplistNode *backward;//分值double score;//成員對象robj *obj;//層struct zskiplistLever{//前進(jìn)指針struct zskiplistNode *forward;//跨度unsigned int span;}lever[];}typedef struct zskiplist{//表頭節(jié)點跟表尾結(jié)點struct zskiplistNode *header, *tail;//表中節(jié)點的數(shù)量unsigned long length;//表中層數(shù)最大的節(jié)點的層數(shù)int lever;}

    跳躍表,基于多指針有序鏈實現(xiàn),可以看作多個有序鏈表。

    與紅黑樹等平衡樹相比,跳躍表具有以下優(yōu)點:

    • 插入速度非常快速,因為不需要進(jìn)行旋轉(zhuǎn)等操作來維持平衡性。
    • 更容易實現(xiàn)。
    • 支持無鎖操作。

    常見操作

    > 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"

    應(yīng)用場景

    以某個條件為權(quán)重,比如按頂?shù)拇螖?shù)排序
    ZREVRANGE命令可以用來按照得分來獲取前100名的用戶,ZRANK可以用來獲取用戶排名,非常直接而且操作容易。
    Redis sorted set的使用場景與set類似,區(qū)別是set不是自動有序的,而sorted set可以通過用戶額外提供一個優(yōu)先級(score)的參數(shù)來為成員排序,并且是插入有序的,即自動排序。

    ?

    redis命令參考鏈接

    1.4Spring整合Redis

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

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


    配置Redis
    - 配置數(shù)據(jù)庫參數(shù)

    # RedisProperties spring.redis.database=11#第11個庫,這個隨便 spring.redis.host=localhost spring.redis.port=6379#端口


    - 編寫配置類,構(gòu)造RedisTemplate

    這個springboot已經(jīng)幫我們配了,但是默認(rèn)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 {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 設(shè)置key的序列化方式template.setKeySerializer(RedisSerializer.string());// 設(shè)置value的序列化方式template.setValueSerializer(RedisSerializer.json());// 設(shè)置hash的key的序列化方式template.setHashKeySerializer(RedisSerializer.string());// 設(shè)置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 {@Autowiredprivate RedisTemplate redisTemplate;@Testpublic 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));}@Testpublic 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"));}@Testpublic 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));}@Testpublic void testSets() {String redisKey = "test:teachers";redisTemplate.opsForSet().add(redisKey, "劉備", "關(guān)羽", "張飛", "趙云", "諸葛亮");System.out.println(redisTemplate.opsForSet().size(redisKey));System.out.println(redisTemplate.opsForSet().pop(redisKey));System.out.println(redisTemplate.opsForSet().members(redisKey));}@Testpublic 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));}@Testpublic void testKeys() {redisTemplate.delete("test:user");System.out.println(redisTemplate.hasKey("test:user"));redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);} }

    這樣還是稍微有點麻煩,我們其實可以綁定key

    // 多次訪問同一個key@Testpublic 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());}

    二、數(shù)據(jù)結(jié)構(gòu)原理總結(jié)

    這部分在我看來是最有意思的,我們有必要了解底層數(shù)據(jù)結(jié)構(gòu)的實現(xiàn),這也是我最感興趣的。

    比如,你知道redis中的字符串怎么實現(xiàn)的嗎?為什么這么實現(xiàn)?

    你知道redis壓縮列表是什么算法嗎?

    你知道redis為什么拋棄了紅黑樹反而采用了跳表這種新的數(shù)據(jù)結(jié)構(gòu)嗎?

    你知道hyperloglog為什么用如此小的空間就可以有這么好的統(tǒng)計性能和準(zhǔn)確性嗎?

    你知道布隆過濾器為什么這么有效嗎?有沒有數(shù)學(xué)證明過?

    你是否還能很快寫出來快排?或者不斷優(yōu)化性能的排序?是不是只會調(diào)庫了甚至庫函數(shù)怎么實現(xiàn)的都不知道?真的就是快排?

    包括數(shù)據(jù)庫,持久化,處理事件、客戶端服務(wù)端、事務(wù)的實現(xiàn)、發(fā)布和訂閱等功能的實現(xiàn),也需要了解。


    2.1數(shù)據(jù)結(jié)構(gòu)和對象的實現(xiàn)

    • 1) 字符串

    redis并未使用傳統(tǒng)的c語言字符串表示,它自己構(gòu)建了一種簡單的動態(tài)字符串抽象類型。

    在redis里,c語言字符串只會作為字符串字面量出現(xiàn),用在無需修改的地方。

    當(dāng)需要一個可以被修改的字符串時,redis就會使用自己實現(xiàn)的SDS(simple dynamic string)。比如在redis數(shù)據(jù)庫里,包含字符串的鍵值對底層都是SDS實現(xiàn)的,不止如此,SDS還被用作緩沖區(qū)(buffer):比如AOF模塊中的AOF緩沖區(qū)以及客戶端狀態(tài)中的輸入緩沖區(qū)。

    下面來具體看一下sds的實現(xiàn):

    struct sdshdr {int len;//buf已使用字節(jié)數(shù)量(保存的字符串長度)int free;//未使用的字節(jié)數(shù)量char buf[];//用來保存字符串的字節(jié)數(shù)組 };

    sds遵循c中字符串以'\0'結(jié)尾的慣例,這一字節(jié)的空間不算在len之內(nèi)。

    這樣的好處是,我們可以直接重用c中的一部分函數(shù)。比如printf;

    ? ? sds相對c的改進(jìn)

    ? ? 獲取長度:c字符串并不記錄自身長度,所以獲取長度只能遍歷一遍字符串,redis直接讀取len即可。

    ? ? 緩沖區(qū)安全:c字符串容易造成緩沖區(qū)溢出,比如:程序員沒有分配足夠的空間就執(zhí)行拼接操作。而redis會先檢查sds的空間是否滿足所需要求,如果不滿足會自動擴(kuò)充。

    ? ? 內(nèi)存分配:由于c不記錄字符串長度,對于包含了n個字符的字符串,底層總是一個長度n+1的數(shù)組,每一次長度變化,總是要對這個數(shù)組進(jìn)行一次內(nèi)存重新分配的操作。因為內(nèi)存分配涉及復(fù)雜算法并且可能需要執(zhí)行系統(tǒng)調(diào)用,所以它通常是比較耗時的操作。? ?

    ? ? redis內(nèi)存分配:

    1、空間預(yù)分配:如果修改后大小小于1MB,程序分配和len大小一樣的未使用空間,如果修改后大于1MB,程序分配? 1MB的未使用空間。修改長度時檢查,夠的話就直接使用未使用空間,不用再分配。?

    2、惰性空間釋放:字符串縮短時不需要釋放空間,用free記錄即可,留作以后使用。

    ? ? 二進(jìn)制安全

    c字符串除了末尾外,不能包含空字符,否則程序讀到空字符會誤以為是結(jié)尾,這就限制了c字符串只能保存文本,二進(jìn)制文件就不能保存了。

    而redis字符串都是二進(jìn)制安全的,因為有l(wèi)en來記錄長度。

    • 2) 鏈表

    作為一種常用數(shù)據(jù)結(jié)構(gòu),鏈表內(nèi)置在很多高級語言中,因為c并沒有,所以redis實現(xiàn)了自己的鏈表。

    鏈表在redis也有一定的應(yīng)用,比如列表鍵的底層實現(xiàn)之一就是鏈表。(當(dāng)列表鍵包含大量元素或者元素都是很長的字符串時)

    發(fā)布與訂閱、慢查詢、監(jiān)視器等功能也用到了鏈表。

    具體實現(xiàn):

    //redis的節(jié)點使用了雙向鏈表結(jié)構(gòu) typedef struct listNode {// 前置節(jié)點struct listNode *prev;// 后置節(jié)點struct listNode *next;// 節(jié)點的值void *value; } listNode; //其實學(xué)過數(shù)據(jù)結(jié)構(gòu)的應(yīng)該都實現(xiàn)過 typedef struct list {// 表頭節(jié)點listNode *head;// 表尾節(jié)點listNode *tail;// 鏈表所包含的節(jié)點數(shù)量unsigned long len;// 節(jié)點值復(fù)制函數(shù)void *(*dup)(void *ptr);// 節(jié)點值釋放函數(shù)void (*free)(void *ptr);// 節(jié)點值對比函數(shù)int (*match)(void *ptr, void *key); } list;

    總結(jié)一下redis鏈表特性:

    雙端、無環(huán)、帶長度記錄、

    多態(tài):使用?void*?指針來保存節(jié)點值, 可以通過?dup?、?free?、?match?為節(jié)點值設(shè)置類型特定函數(shù), 可以保存不同類型的值。

    • 3)字典

    其實字典這種數(shù)據(jù)結(jié)構(gòu)也內(nèi)置在很多高級語言中,但是c語言沒有,所以redis自己實現(xiàn)了。

    應(yīng)用也比較廣泛,比如redis的數(shù)據(jù)庫就是字典實現(xiàn)的。不僅如此,當(dāng)一個哈希鍵包含的鍵值對比較多,或者都是很長的字符串,redis就會用字典作為哈希鍵的底層實現(xiàn)。

    來看看具體是實現(xiàn):

    //redis的字典使用哈希表作為底層實現(xiàn) typedef struct dictht {// 哈希表數(shù)組dictEntry **table;// 哈希表大小unsigned long size;// 哈希表大小掩碼,用于計算索引值// 總是等于 size - 1unsigned long sizemask;// 該哈希表已有節(jié)點的數(shù)量unsigned long used;} dictht;

    table?是一個數(shù)組, 數(shù)組中的每個元素都是一個指向dictEntry?結(jié)構(gòu)的指針, 每個?dictEntry?結(jié)構(gòu)保存著一個鍵值對

    圖為一個大小為4的空哈希表。

    我們接著就來看dictEntry的實現(xiàn):

    typedef struct dictEntry {// 鍵void *key;// 值union {void *val;uint64_t u64;int64_t s64;} v;// 指向下個哈希表節(jié)點,形成鏈表struct dictEntry *next; } dictEntry;

    (v可以是一個指針, 或者是一個?uint64_t?整數(shù), 又或者是一個?int64_t?整數(shù)。)

    next就是解決鍵沖突問題的,沖突了就掛后面,這個學(xué)過數(shù)據(jù)結(jié)構(gòu)的應(yīng)該都知道吧,不說了。

    ?

    下面我們來說字典是怎么實現(xiàn)的了。

    typedef struct dict {// 類型特定函數(shù)dictType *type;// 私有數(shù)據(jù)void *privdata;// 哈希表dictht ht[2];// rehash 索引int rehashidx; //* rehashing not in progress if rehashidx == -1 } dict;

    type?和?privdata?是對不同類型的鍵值對, 為創(chuàng)建多態(tài)字典而設(shè)置的:

    type?指向?dictType?, 每個?dictType?保存了用于操作特定類型鍵值對的函數(shù), 可以為用途不同的字典設(shè)置不同的類型特定函數(shù)。

    而?privdata?屬性則保存了需要傳給那些類型特定函數(shù)的可選參數(shù)。

    而dictType就暫時不展示了,不重要而且字有點多。。。還是講有意思的東西吧

    ? ? rehash(重新散列)

    隨著我們不斷的操作,哈希表保存的鍵值可能會增多或者減少,為了讓哈希表的負(fù)載因子維持在合理的范圍內(nèi),有時需要對哈希表進(jìn)行合理的擴(kuò)展或者收縮。?一般情況下, 字典只使用?ht[0]?哈希表,?ht[1]?哈希表只會在對?ht[0]?哈希表進(jìn)行 rehash 時使用。

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

    1)為ht[1]分配合理空間:如果是擴(kuò)展操作,大小為第一個大于等于ht[0]*used*2的,2的n次冪。

    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?如果是收縮操作,大小為第一個大于等于ht[0]*used的,2的n次冪。

    2)將ht[0]中的數(shù)據(jù)rehash到ht[1]上。

    3)釋放ht[0],將ht[1]設(shè)置為ht[0],ht[1]創(chuàng)建空表,為下次做準(zhǔn)備。

    ? ? 漸進(jìn)rehash

    數(shù)據(jù)量特別大時,rehash可能對服務(wù)器造成影響。為了避免,服務(wù)器不是一次性rehash的,而是分多次。

    我們維持一個變量rehashidx,設(shè)置為0,代表rehash開始,然后開始rehash,在這期間,每個對字典的操作,程序都會把索引rehashidx上的數(shù)據(jù)移動到ht[1]。

    隨著操作不斷執(zhí)行,最終我們會完成rehash,設(shè)置rehashidx為-1.

    需要注意:rehash過程中,每一次增刪改查也是在兩個表進(jìn)行的。

    • 4)整數(shù)集合

    整數(shù)集合(intset)是 Redis 用于保存整數(shù)值的集合抽象數(shù)據(jù)結(jié)構(gòu), 可以保存?int16_t?、?int32_t?、?int64_t?的整數(shù)值, 并且保證集合中不會出現(xiàn)重復(fù)元素。

    實現(xiàn)較為簡單:

    typedef struct intset {// 編碼方式uint32_t encoding;// 集合包含的元素數(shù)量uint32_t length;// 保存元素的數(shù)組int8_t contents[]; } intset;

    各個項在數(shù)組中從小到大有序地排列, 并且數(shù)組中不包含任何重復(fù)項。

    雖然?intset?結(jié)構(gòu)將?contents?屬性聲明為?int8_t?類型的數(shù)組, 但實際上?contents?數(shù)組并不保存任何?int8_t?類型的值 ——?contents?數(shù)組的真正類型取決于?encoding?屬性的值:

    如果?encoding?屬性的值為?INTSET_ENC_INT16?, 那么?contents?就是一個?int16_t?類型的數(shù)組, 數(shù)組里的每個項都是一個?int16_t?類型的整數(shù)值 (最小值為?-32,768?,最大值為?32,767?)。

    如果?encoding?屬性的值為?INTSET_ENC_INT32?, 那么?contents?就是一個?int32_t?類型的數(shù)組, 數(shù)組里的每個項都是一個?int32_t?類型的整數(shù)值 (最小值為?-2,147,483,648?,最大值為?2,147,483,647?)。

    如果?encoding?屬性的值為?INTSET_ENC_INT64?, 那么?contents?就是一個?int64_t?類型的數(shù)組, 數(shù)組里的每個項都是一個?int64_t?類型的整數(shù)值 (最小值為?-9,223,372,036,854,775,808?,最大值為?9,223,372,036,854,775,807?)。

    ? ? 升級

    c語言是靜態(tài)類型語言,不允許不同類型保存在一個數(shù)組。這樣第一,靈活性較差,第二,有時會用掉不必要的內(nèi)存

    比如用long long儲存1

    為了提高整數(shù)集合的靈活性和節(jié)約內(nèi)存,我們引入升級策略。

    當(dāng)我們要將一個新元素添加到集合里, 并且新元素類型比集合現(xiàn)有元素的類型都要長時, 集合需要先進(jìn)行升級。

    分為三步進(jìn)行:

  • 根據(jù)新元素的類型, 擴(kuò)展整數(shù)集合底層數(shù)組的空間大小, 并為新元素分配空間。
  • 將底層數(shù)組現(xiàn)有的所有元素都轉(zhuǎn)換成與新元素相同的類型, 并將類型轉(zhuǎn)換后的元素放置到正確的位上
  • 將新元素添加到底層數(shù)組里面。
  • 因為每次添加新元素都可能會引起升級, 每次升級都要對已有元素類型轉(zhuǎn)換, 所以添加新元素的時間復(fù)雜度為?O(N)?。

    因為引發(fā)升級的新元素比原數(shù)據(jù)都長,所以要么他是最大的,要么他是最小的。我們把它放在開頭或結(jié)尾即可。

    ?

    ? ? 降級

    略略略,不管你們信不信,整數(shù)集合不支持降級操作。。我也不知道為啥

    • 5)壓縮列表

    壓縮列表是列表鍵和哈希鍵的底層實現(xiàn)之一。

    當(dāng)一個列表鍵只包含少量列表項,并且列表項都是小整數(shù)或者短字符串,redis就會用壓縮列表做列表鍵底層實現(xiàn)。

    壓縮列表是 Redis 為了節(jié)約內(nèi)存而開發(fā)的, 由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型(sequential)數(shù)據(jù)結(jié)構(gòu)。

    一個壓縮列表可以包含任意多個節(jié)點(entry), 每個節(jié)點可以保存一個字節(jié)數(shù)組或者一個整數(shù)值。

    具體實現(xiàn):

    具體說一下entry:

    由三個部分組成:

    1、previous_entry_length:記錄上一個節(jié)點的長度,這樣我們就可以從最后一路遍歷到開頭。

    2、encoding:記錄了content所保存的數(shù)據(jù)類型和長度。(具體編碼不寫了,不重要)

    3、content:保存節(jié)點值,可以是字節(jié)數(shù)組或整數(shù)。(具體怎么壓縮的等我搞明白再補(bǔ))

    ? ? 連鎖更新

    前面說過, 每個節(jié)點的?previous_entry_length?屬性都記錄了前一個節(jié)點的長度:

    • 如果前一節(jié)點的長度<?254?KB, 那么?previous_entry_length?需要用?1?字節(jié)長的空間
    • 如果前一節(jié)點的長度>=254?KB, 那么?previous_entry_length?需要用?5?字節(jié)長的空間

    現(xiàn)在, 考慮這樣一種情況: 在一個壓縮列表中, 有多個連續(xù)的、長度介于?250?字節(jié)到?253?字節(jié)之間的節(jié)點 ,這時, 如果我們將一個長度大于等于?254?字節(jié)的新節(jié)點?new?設(shè)置為壓縮列表的表頭節(jié)點。。。。

    然后腦補(bǔ)一下,就會導(dǎo)致連鎖擴(kuò)大每個節(jié)點的空間對吧?e(i)因為e(i-1)的擴(kuò)大而擴(kuò)大,i+1也是如此,以此類推。。。

    ?

    刪除節(jié)點同樣會導(dǎo)致連鎖更新。

    這個事情只是想說明一個問題:插入刪除操作的最壞時間復(fù)雜度其實是o(n*n),因為每更新一個節(jié)點都要o(n)。

    但是,也不用太過擔(dān)心,因為這種特殊情況并不多見,這些命令的平均復(fù)雜度依舊是o(n)。

    ?

    2.2 跳表專欄

    2.2.1跳表是啥

    為什么選擇了跳表而不是紅黑樹?

    跳表是個啥東西請看這個文章。

    我們知道,節(jié)點插入時隨機(jī)出一個層數(shù),僅僅依靠一個簡單的隨機(jī)數(shù)操作而構(gòu)建出來的多層鏈表結(jié)構(gòu),能保證它有一個良好的查找性能嗎?為了回答這個疑問,我們需要分析skiplist的統(tǒng)計性能。

    在分析之前,我們還需要著重指出的是,執(zhí)行插入操作時計算隨機(jī)數(shù)的過程,是一個很關(guān)鍵的過程,它對skiplist的統(tǒng)計特性有著很重要的影響。這并不是一個普通的服從均勻分布的隨機(jī)數(shù),它的計算過程如下:

    • 首先,每個節(jié)點肯定都有第1層指針(每個節(jié)點都在第1層鏈表里)。
    • 如果一個節(jié)點有第i層(i>=1)指針(即節(jié)點已經(jīng)在第1層到第i層鏈表中),那么它有第(i+1)層指針的概率為p。
    • 節(jié)點最大的層數(shù)不允許超過一個最大值,記為MaxLevel。

    這個計算隨機(jī)層數(shù)的偽碼如下所示:

    randomLevel() level := 1 // random()返回一個[0...1)的隨機(jī)數(shù) while random() < p and level < MaxLevel do level := level + 1 return level

    randomLevel()的偽碼中包含兩個參數(shù),一個是p,一個是MaxLevel。在Redis的skiplist實現(xiàn)中,這兩個參數(shù)的取值為:

    p = 1/4 MaxLevel = 32

    2.2.2skiplist的算法性能分析

    在這一部分,我們來簡單分析一下skiplist的時間復(fù)雜度和空間復(fù)雜度,以便對于skiplist的性能有一個直觀的了解。如果你不是特別偏執(zhí)于算法的性能分析,那么可以暫時跳過這一小節(jié)的內(nèi)容。

    我們先來計算一下每個節(jié)點所包含的平均指針數(shù)目(概率期望)。節(jié)點包含的指針數(shù)目,相當(dāng)于這個算法在空間上的額外開銷(overhead),可以用來度量空間復(fù)雜度。

    根據(jù)前面randomLevel()的偽碼,我們很容易看出,產(chǎn)生越高的節(jié)點層數(shù),概率越低。定量的分析如下:

    • 節(jié)點層數(shù)至少為1。而大于1的節(jié)點層數(shù),滿足一個概率分布。
    • 節(jié)點層數(shù)恰好等于1的概率為1-p。
    • 節(jié)點層數(shù)大于等于2的概率為p,而節(jié)點層數(shù)恰好等于2的概率為p(1-p)。
    • 節(jié)點層數(shù)大于等于3的概率為p^2,而節(jié)點層數(shù)恰好等于3的概率為p^2(1-p)。
    • 節(jié)點層數(shù)大于等于4的概率為p^3,而節(jié)點層數(shù)恰好等于4的概率為p^3(1-p)。
    • ......

    因此,一個節(jié)點的平均層數(shù)(也即包含的平均指針數(shù)目),計算如下:

    現(xiàn)在很容易計算出:

    • 當(dāng)p=1/2時,每個節(jié)點所包含的平均指針數(shù)目為2;
    • 當(dāng)p=1/4時,每個節(jié)點所包含的平均指針數(shù)目為1.33。這也是Redis里的skiplist實現(xiàn)在空間上的開銷。

    接下來,為了分析時間復(fù)雜度,我們計算一下skiplist的平均查找長度。查找長度指的是查找路徑上跨越的跳數(shù),而查找過程中的比較次數(shù)就等于查找長度加1。以前面圖中標(biāo)出的查找23的查找路徑為例,從左上角的頭結(jié)點開始,一直到結(jié)點22,查找長度為6。

    為了計算查找長度,這里我們需要利用一點小技巧。我們注意到,每個節(jié)點插入的時候,它的層數(shù)是由隨機(jī)函數(shù)randomLevel()計算出來的,而且隨機(jī)的計算不依賴于其它節(jié)點,每次插入過程都是完全獨(dú)立的。所以,從統(tǒng)計上來說,一個skiplist結(jié)構(gòu)的形成與節(jié)點的插入順序無關(guān)。

    這樣的話,為了計算查找長度,我們可以將查找過程倒過來看,從右下方第1層上最后到達(dá)的那個節(jié)點開始,沿著查找路徑向左向上回溯,類似于爬樓梯的過程。我們假設(shè)當(dāng)回溯到某個節(jié)點的時候,它才被插入,這雖然相當(dāng)于改變了節(jié)點的插入順序,但從統(tǒng)計上不影響整個skiplist的形成結(jié)構(gòu)。

    現(xiàn)在假設(shè)我們從一個層數(shù)為i的節(jié)點x出發(fā),需要向左向上攀爬k層。這時我們有兩種可能:

    • 如果節(jié)點x有第(i+1)層指針,那么我們需要向上走。這種情況概率為p。
    • 如果節(jié)點x沒有第(i+1)層指針,那么我們需要向左走。這種情況概率為(1-p)。

    用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

    這個結(jié)果的意思是,我們每爬升1個層級,需要在查找路徑上走1/p步。而我們總共需要攀爬的層級數(shù)等于整個skiplist的總層數(shù)-1。

    那么接下來我們需要分析一下當(dāng)skiplist中有n個節(jié)點的時候,它的總層數(shù)的概率均值是多少。這個問題直觀上比較好理解。根據(jù)節(jié)點的層數(shù)隨機(jī)算法,容易得出:

    • 第1層鏈表固定有n個節(jié)點;
    • 第2層鏈表平均有n*p個節(jié)點;
    • 第3層鏈表平均有n*p^2個節(jié)點;
    • ...

    所以,從第1層到最高層,各層鏈表的平均節(jié)點數(shù)是一個指數(shù)遞減的等比數(shù)列。容易推算出,總層數(shù)的均值為log1/pn,而最高層的平均節(jié)點數(shù)為1/p。

    綜上,粗略來計算的話,平均查找長度約等于:

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

    即,平均時間復(fù)雜度為O(log n)。

    當(dāng)然,這里的時間復(fù)雜度分析還是比較粗略的。比如,沿著查找路徑向左向上回溯的時候,可能先到達(dá)左側(cè)頭結(jié)點,然后沿頭結(jié)點一路向上;還可能先到達(dá)最高層的節(jié)點,然后沿著最高層鏈表一路向左。但這些細(xì)節(jié)不影響平均時間復(fù)雜度的最后結(jié)果。另外,這里給出的時間復(fù)雜度只是一個概率平均值,但實際上計算一個精細(xì)的概率分布也是有可能的。

    詳情還請參見William Pugh的論文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

    2.2.3skiplist與平衡樹、哈希表的比較

    • skiplist和各種平衡樹(如AVL、紅黑樹等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做單個key的查找,不適宜做范圍查找。所謂范圍查找,指的是查找那些大小在指定的兩個值之間的所有節(jié)點。
    • 在做范圍查找的時候,平衡樹比skiplist操作要復(fù)雜。在平衡樹上,我們找到指定范圍的小值之后,還需要以中序遍歷的順序繼續(xù)尋找其它不超過大值的節(jié)點。如果不對平衡樹進(jìn)行一定的改造,這里的中序遍歷并不容易實現(xiàn)。而在skiplist上進(jìn)行范圍查找就非常簡單,只需要在找到小值之后,對第1層鏈表進(jìn)行若干步的遍歷就可以實現(xiàn)。
    • 平衡樹的插入和刪除操作可能引發(fā)子樹的調(diào)整,邏輯復(fù)雜,而skiplist的插入和刪除只需要修改相鄰節(jié)點的指針,操作簡單又快速。
    • 從內(nèi)存占用上來說,skiplist比平衡樹更靈活一些。一般來說,平衡樹每個節(jié)點包含2個指針(分別指向左右子樹),而skiplist每個節(jié)點包含的指針數(shù)目平均為1/(1-p),具體取決于參數(shù)p的大小。如果像Redis里的實現(xiàn)一樣,取p=1/4,那么平均每個節(jié)點包含1.33個指針,比平衡樹更有優(yōu)勢。
    • 查找單個key,skiplist和平衡樹的時間復(fù)雜度都為O(log n),大體相當(dāng);而哈希表在保持較低的哈希值沖突概率的前提下,查找時間復(fù)雜度接近O(1),性能更高一些。所以我們平常使用的各種Map或dictionary結(jié)構(gòu),大都是基于哈希表實現(xiàn)的。
    • 從算法實現(xiàn)難度上來比較,skiplist比平衡樹要簡單得多。

    2.2.4Redis中的skiplist和經(jīng)典有何不同

    • 分?jǐn)?shù)(score)允許重復(fù),即skiplist的key允許重復(fù)。這在最開始介紹的經(jīng)典skiplist中是不允許的。
    • 在比較時,不僅比較分?jǐn)?shù)(相當(dāng)于skiplist的key),還比較數(shù)據(jù)本身。在Redis的skiplist實現(xiàn)中,數(shù)據(jù)本身的內(nèi)容唯一標(biāo)識這份數(shù)據(jù),而不是由key來唯一標(biāo)識。另外,當(dāng)多個元素分?jǐn)?shù)相同的時候,還需要根據(jù)數(shù)據(jù)內(nèi)容來進(jìn)字典排序。
    • 第1層鏈表不是一個單向鏈表,而是一個雙向鏈表。這是為了方便以倒序方式獲取一個范圍內(nèi)的元素。
    • 在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)它們的記憶力不是很強(qiáng)。基本上由你決定。更改有關(guān)節(jié)點具有給定數(shù)量級別的概率的參數(shù)將使內(nèi)存密集度低于btree。

    2)排序集通常是許多Zrange或Zrevrange操作的目標(biāo),即作為鏈表遍歷跳過列表。通過此操作,跳過列表的緩存區(qū)域性至少與其他類型的平衡樹一樣好。

    3)它們易于實現(xiàn)、調(diào)試等。例如,由于跳過列表的簡單性,我收到了一個補(bǔ)丁(已經(jīng)在redis master中),其中包含在o(log(n))中實現(xiàn)zrank的擴(kuò)展跳過列表。它只需要對代碼稍作修改。

    2.3HyperLogLog 專欄

    HyperLogLog 是一種概率數(shù)據(jù)結(jié)構(gòu),用來估算數(shù)據(jù)的基數(shù)。數(shù)據(jù)集可以是網(wǎng)站訪客的 IP 地址,E-mail 郵箱或者用戶 ID。

    基數(shù)就是指一個集合中不同值的數(shù)目,比如 a, b, c, d 的基數(shù)就是 4,a, b, c, d, a 的基數(shù)還是 4。雖然 a 出現(xiàn)兩次,只會被計算一次。

    使用 Redis 統(tǒng)計集合的基數(shù)一般有三種方法,分別是使用 Redis 的 HashMap,BitMap 和 HyperLogLog。前兩個數(shù)據(jù)結(jié)構(gòu)在集合的數(shù)量級增長時,所消耗的內(nèi)存會大大增加,但是 HyperLogLog 則不會。

    Redis 的 HyperLogLog 通過犧牲準(zhǔn)確率來減少內(nèi)存空間的消耗,只需要12K內(nèi)存,在標(biāo)準(zhǔn)誤差0.81%的前提下,能夠統(tǒng)計2^64個數(shù)據(jù)。所以 HyperLogLog 是否適合在比如統(tǒng)計日活月活此類的對精度要不不高的場景。

    這是一個很驚人的結(jié)果,以如此小的內(nèi)存來記錄如此大數(shù)量級的數(shù)據(jù)基數(shù)。下面我們就帶大家來深入了解一下 HyperLogLog 的使用,基礎(chǔ)原理,源碼實現(xiàn)和具體的試驗數(shù)據(jù)分析。

    2.3.1HyperLogLog 在 Redis 中的使用

    Redis 提供了?PFADD?、?PFCOUNT?和?PFMERGE?三個命令來供用戶使用 HyperLogLog。

    PFADD?用于向 HyperLogLog 添加元素。

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

    如果 HyperLogLog 估計的近似基數(shù)在?PFADD?命令執(zhí)行之后出現(xiàn)了變化, 那么命令返回 1 , 否則返回 0 。 如果命令執(zhí)行時給定的鍵不存在, 那么程序?qū)⑾葎?chuàng)建一個空的 HyperLogLog 結(jié)構(gòu), 然后再執(zhí)行命令。

    PFCOUNT?命令會給出 HyperLogLog 包含的近似基數(shù)。在計算出基數(shù)后,?PFCOUNT?會將值存儲在 HyperLogLog 中進(jìn)行緩存,知道下次?PFADD?執(zhí)行成功前,就都不需要再次進(jìn)行基數(shù)的計算。

    PFMERGE?將多個 HyperLogLog 合并為一個 HyperLogLog , 合并后的 HyperLogLog 的基數(shù)接近于所有輸入 HyperLogLog 的并集基數(shù)。

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

    2.3.2內(nèi)存消耗對比實驗

    我們下面就來通過實驗真實對比一下下面三種數(shù)據(jù)結(jié)構(gòu)的內(nèi)存消耗,HashMap、BitMap 和 HyperLogLog。

    我們首先使用 Lua 腳本向 Redis 對應(yīng)的數(shù)據(jù)結(jié)構(gòu)中插入一定數(shù)量的數(shù),然后執(zhí)行 bgsave 命令,最后使用 redis-rdb-tools 的 rdb 的命令查看各個鍵所占的內(nèi)存大小。

    下面是 Lua 的腳本

    local key = KEYS[1]local size = tonumber(ARGV[1])local method = tonumber(ARGV[2])for i=1,size,1 doif (method == 0)thenredis.call('hset',key,i,1)elseif (method == 1)thenredis.call('pfadd',key, i)elseredis.call('setbit', key, i, 1)endend

    我們在通過 redis-cli 的?script load?命令將 Lua 腳本加載到 Redis 中,然后使用?evalsha?命令分別向 HashMap、HyperLogLog 和 BitMap 三種數(shù)據(jù)結(jié)構(gòu)中插入了一千萬個數(shù),然后使用?rdb?命令查看各個結(jié)構(gòu)內(nèi)存消耗。

    我們進(jìn)行了兩輪實驗,分別插入一萬數(shù)字和一千萬數(shù)字,三種數(shù)據(jù)結(jié)構(gòu)消耗的內(nèi)存統(tǒng)計如下所示。

    從表中可以明顯看出,一萬數(shù)量級時 BitMap 消耗內(nèi)存最小, 一千萬數(shù)量級時 HyperLogLog 消耗內(nèi)存最小,但是總體來看,HyperLogLog 消耗的內(nèi)存都是 14392 字節(jié),可見 HyperLogLog 在內(nèi)存消耗方面有自己的獨(dú)到之處。

    2.3.3基本原理

    HyperLogLog 是一種概率數(shù)據(jù)結(jié)構(gòu),它使用概率算法來統(tǒng)計集合的近似基數(shù)。而它算法的最本源則是伯努利過程。

    伯努利過程就是一個拋硬幣實驗的過程。拋一枚正常硬幣,落地可能是正面,也可能是反面,二者的概率都是 1/2 。伯努利過程就是一直拋硬幣,直到落地時出現(xiàn)正面位置,并記錄下拋擲次數(shù)k。比如說,拋一次硬幣就出現(xiàn)正面了,此時 k 為 1; 第一次拋硬幣是反面,則繼續(xù)拋,直到第三次才出現(xiàn)正面,此時 k 為 3。

    對于 n 次伯努利過程,我們會得到 n 個出現(xiàn)正面的投擲次數(shù)值 k1, k2 ... kn , 其中這里的最大值是k_max。

    根據(jù)一頓數(shù)學(xué)推導(dǎo),我們可以得出一個結(jié)論: 2^{k_ max} 來作為n的估計值。也就是說你可以根據(jù)最大投擲次數(shù)近似的推算出進(jìn)行了幾次伯努利過程。

    下面,我們就來講解一下 HyperLogLog 是如何模擬伯努利過程,并最終統(tǒng)計集合基數(shù)的。

    HyperLogLog 在添加元素時,會通過Hash函數(shù),將元素轉(zhuǎn)為64位比特串,例如輸入5,便轉(zhuǎn)為101(省略前面的0,下同)。這些比特串就類似于一次拋硬幣的伯努利過程。比特串中,0 代表了拋硬幣落地是反面,1 代表拋硬幣落地是正面,如果一個數(shù)據(jù)最終被轉(zhuǎn)化了 10010000,那么從低位往高位看,我們可以認(rèn)為,這串比特串可以代表一次伯努利過程,首次出現(xiàn) 1 的位數(shù)為5,就是拋了5次才出現(xiàn)正面。

    所以 HyperLogLog 的基本思想是利用集合中數(shù)字的比特串第一個 1 出現(xiàn)位置的最大值來預(yù)估整體基數(shù),但是這種預(yù)估方法存在較大誤差,為了改善誤差情況,HyperLogLog中引入分桶平均的概念,計算 m 個桶的調(diào)和平均值。

    Redis 中 HyperLogLog 一共分了 2^14 個桶,也就是 16384 個桶。每個桶中是一個 6 bit 的數(shù)組。

    HyperLogLog 將上文所說的 64 位比特串的低 14 位單獨(dú)拿出,它的值就對應(yīng)桶的序號,然后將剩下 50 位中第一次出現(xiàn) 1 的位置值設(shè)置到桶中。50位中出現(xiàn)1的位置值最大為50,所以每個桶中的 6 位數(shù)組正好可以表示該值。

    在設(shè)置前,要設(shè)置進(jìn)桶的值是否大于桶中的舊值,如果大于才進(jìn)行設(shè)置,否則不進(jìn)行設(shè)置。

    此時為了性能考慮,是不會去統(tǒng)計當(dāng)前的基數(shù)的,而是將 HyperLogLog 頭的 card 屬性中的標(biāo)志位置為 1,表示下次進(jìn)行 pfcount 操作的時候,當(dāng)前的緩存值已經(jīng)失效了,需要重新統(tǒng)計緩存值。在后面 pfcount 流程的時候,發(fā)現(xiàn)這個標(biāo)記為失效,就會去重新統(tǒng)計新的基數(shù),放入基數(shù)緩存。

    在計算近似基數(shù)時,就分別計算每個桶中的值,帶入到上文的 DV 公式中,進(jìn)行調(diào)和平均和結(jié)果修正,就能得到估算的基數(shù)值。

    2.3.4HyperLogLog 具體對象

    我們首先來看一下 HyperLogLog 對象的定義

    struct hllhdr {char magic[4]; /* 魔法值 "HYLL" */uint8_t encoding; /* 密集結(jié)構(gòu)或者稀疏結(jié)構(gòu) HLL_DENSE or HLL_SPARSE. */uint8_t notused[3]; /* 保留位, 全為0. */uint8_t card[8]; /* 基數(shù)大小的緩存 */uint8_t registers[]; /* 數(shù)據(jù)字節(jié)數(shù)組 */};

    HyperLogLog 對象中的?registers?數(shù)組就是桶,它有兩種存儲結(jié)構(gòu),分別為密集存儲結(jié)構(gòu)和稀疏存儲結(jié)構(gòu),兩種結(jié)構(gòu)只涉及存儲和桶的表現(xiàn)形式,從中我們可以看到 Redis 對節(jié)省內(nèi)存極致地追求。

    我們先看相對簡單的密集存儲結(jié)構(gòu),它也是十分的簡單明了,既然要有 2^14 個 6 bit的桶,那么我就真使用足夠多的?uint8_t?字節(jié)去表示,只是此時會涉及到字節(jié)位置和桶的轉(zhuǎn)換,因為字節(jié)有 8 位,而桶只需要 6 位。

    所以我們需要將桶的序號轉(zhuǎn)換成對應(yīng)的字節(jié)偏移量 offsetbytes 和其內(nèi)部的位數(shù)偏移量 offsetbits。需要注意的是小端字節(jié)序,高位在右側(cè),需要進(jìn)行倒轉(zhuǎn)。

    當(dāng) offset_bits 小于等于2時,說明一個桶就在該字節(jié)內(nèi),只需要進(jìn)行倒轉(zhuǎn)就能得到桶的值。

    ?offset_bits 大于 2 ,則說明一個桶分布在兩個字節(jié)內(nèi),此時需要將兩個字節(jié)的內(nèi)容都進(jìn)行倒置,然后再進(jìn)行拼接得到桶的值。

    Redis 為了方便表達(dá)稀疏存儲,它將上面三種字節(jié)表示形式分別賦予了一條指令。

    • ZERO : 一字節(jié),表示連續(xù)多少個桶計數(shù)為0,前兩位為標(biāo)志00,后6位表示有多少個桶,最大為64。

    • XZERO : 兩個字節(jié),表示連續(xù)多少個桶計數(shù)為0,前兩位為標(biāo)志01,后14位表示有多少個桶,最大為16384。

    • VAL : 一字節(jié),表示連續(xù)多少個桶的計數(shù)為多少,前一位為標(biāo)志1,四位表示連桶內(nèi)計數(shù),所以最大表示桶的計數(shù)為32。后兩位表示連續(xù)多少個桶。

    ?

    Redis從稀疏存儲轉(zhuǎn)換到密集存儲的條件是:

    • 任意一個計數(shù)值從 32 變成 33,因為 VAL 指令已經(jīng)無法容納,它能表示的計數(shù)值最大為 32

    • 稀疏存儲占用的總字節(jié)數(shù)超過 3000 字節(jié),這個閾值可以通過 hllsparsemax_bytes 參數(shù)進(jìn)行調(diào)整。

    2.4LRU專欄

    2.4.1LRU介紹和代碼實現(xiàn)

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

    LRU算法的設(shè)計原則是:如果一個數(shù)據(jù)在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當(dāng)限定的空間已存滿數(shù)據(jù)時,應(yīng)當(dāng)把最久沒有被訪問到的數(shù)據(jù)淘汰。(這一段是找的,讓大家理解一下什么是LRU)。

    ?

    說一下我們什么時候見到過LRU:其實老師們肯定都給大家舉過這么個例子:你在圖書館,你把書架子里的書拿到桌子上。。但是桌子是有限的,你有時候不得不把一些書放回去。這就相當(dāng)于內(nèi)存和硬盤。這個例子都說過吧?

    LRU就是記錄你最長時間沒看過的書,就把它放回去。在cache那里見過吧

    ?

    然后最近在研究redis,又看到了這個LRU,所以就想寫一下吧。

    題目:設(shè)計一個結(jié)構(gòu),這個結(jié)構(gòu)可以查詢K-V,但是容量有限,當(dāng)存不下的時候就要把用的年代最久遠(yuǎn)的那個東西扔掉。

    其實思路很簡單,我們維護(hù)一個雙向鏈表即可,get也就是使用了,我們就把把它提到最安全的位置。新來的KV就依次放即可。

    我們就先寫這個雙向鏈表結(jié)構(gòu)

    先寫節(jié)點結(jié)構(gòu):

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

    然后寫雙向鏈表結(jié)構(gòu): 我們沒必要把鏈表操作都寫了,分析一下,我們只有三個操作:

    1、加節(jié)點

    2、使用了某個節(jié)點就把它調(diào)到尾,代表優(yōu)先級最高

    3、把優(yōu)先級最低的移除,也就是去頭部

    (不會的,翻我之前的鏈表操作都有寫)

    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;//注意讓本節(jié)點前指針指向舊尾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;}}

    鏈表操作封裝完了就要實現(xiàn)這個結(jié)構(gòu)了。

    具體思路代碼注釋

    public static class MyCache<K, V> {//為了kv or vk都能查private HashMap<K, Node<V>> keyNodeMap;private HashMap<Node<V>, K> nodeKeyMap;//用來做優(yōu)先級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;//放新vthis.nodeList.moveNodeToTail(node);//我們認(rèn)為放入舊key也是使用過} else {Node<V> newNode = new Node<V>(value);this.keyNodeMap.put(key, newNode);this.nodeKeyMap.put(newNode, key);this.nodeList.addNode(newNode);//加進(jìn)去if (this.keyNodeMap.size() == this.capacity + 1) {this.removeMostUnusedCache();//放不下就去掉優(yōu)先級最低的}}}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算法改進(jìn)

    redis通常使用緩存,是使用一種固定最大內(nèi)存的使用。當(dāng)數(shù)據(jù)達(dá)到可使用的最大固定內(nèi)存時,我們需要通過移除老數(shù)據(jù)來獲取空間。redis作為緩存是否有效的重要標(biāo)志是如何尋找一種好的策略:刪除即將需要使用的數(shù)據(jù)是一種糟糕的策略,而刪除那些很少再次請求的數(shù)據(jù)則是一種好的策略。
    在其他的緩存組件還有個命中率,僅僅表示讀請求的比例。訪問一個緩存中的keys通常不是分布式的。然而訪問經(jīng)常變化,這意味著不經(jīng)常訪問,相反,有些keys一旦不流行可能會轉(zhuǎn)向最經(jīng)常訪問的keys。 因此,通常一個緩存系統(tǒng)應(yīng)該盡可能保留那些未來最有可能被訪問的keys。針對keys淘汰的策略是:那些未來極少可能被訪問的數(shù)據(jù)應(yīng)該被移除。
    但有一個問題:redis和其他緩存系統(tǒng)不能夠預(yù)測未來。

    LRU算法

    緩存系統(tǒng)不能預(yù)測未來,原因是:那些很少再次被訪問的key也很有可能最近訪問相當(dāng)頻繁。如果經(jīng)常被訪問的模式不會突然改變,那么這是一種很有效的策略。然而,“最近經(jīng)常被訪問”似乎更隱晦地標(biāo)明一種 理念。這種算法被稱為LRU算法。最近訪問頻繁的key相比訪問少的key有更高的可能性。
    舉個例子,這里有4個不同訪問周期的key,每一個“~”字符代表一秒,結(jié)尾的“|”表示當(dāng)前時刻。

    ~~~~~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也能很好地反映它們本身的周期。然而你可以看到不夠嚴(yán)謹(jǐn):D的訪問周期是10秒,但它卻是4個key中最近被訪問的。
    當(dāng)然,在一個很長的運(yùn)行周期中,LRU算法能工作得很好。通常有一個更高訪問頻率的key當(dāng)然有一個更低的空閑周期。LRU算法淘汰最少被訪問key,那些有最大空閑周期的key。實現(xiàn)上也相當(dāng)容易,只需要額外跟蹤最近被訪問的key即可,有時甚至都需要:把所有我們想要淘汰的對象放到一個鏈表中,當(dāng)一個對象訪問就移除鏈表頭部元素,當(dāng)我們要淘汰元素是就直接淘汰鏈表尾部開始。

    redis中的LRU:起因

    最初,redis不支持LRU算法。當(dāng)內(nèi)存有效性成為一個必須被解決的問題時,后來才加上了。通過修改redis對象結(jié)構(gòu),在每個key對象增加24bit的空間。沒有額外的空間使用鏈表把所有對象放到一個鏈表中(大指針),因此需要實現(xiàn)得更加有效,不能因為key淘汰算法而讓整個服務(wù)改動太大。
    24bits的對象已經(jīng)足夠去存儲當(dāng)前的unxi時間戳。這個表現(xiàn),被稱為“LRU 時鐘”,key元數(shù)據(jù)經(jīng)常被更新,所以它是一個有效的算法。
    然后,有另一個更加復(fù)雜的問題需要解決:如何選擇訪問間隔最長的key,然后淘汰它。
    redis內(nèi)部采用一個龐大的hash table來保存,添加另外一個數(shù)據(jù)結(jié)構(gòu)存儲時間間隔顯然不是一個好的選擇。然而我們希望能達(dá)到一個LRU本身是一個近似的,通過LRU算法本身來實現(xiàn)。

    redis原始的淘汰算法簡單實現(xiàn):**當(dāng)需要淘汰一個key時,隨機(jī)選擇3個key,淘汰其中間隔時間最長的key。**基本上,我們隨機(jī)選擇key,淘汰key效果很好。后來隨機(jī)3個key改成一個配置項"N隨機(jī)key"。但把默認(rèn)值提高改成5個后效果大大提高。考慮到它的效果,你根本不用修改他。

    然而,你可能會想這個算法如何有效執(zhí)行,你可以看到我們?nèi)绾螕v毀了很多有趣的數(shù)據(jù)。也許簡單的N key,我們會遇到很多好的決策,但是當(dāng)我們淘汰最好的,下一個周期又開始抓。

    驗證規(guī)則第一條:用肉眼觀察你的算法

    其中有一個觀點已經(jīng)應(yīng)用到Redis 3.0正式版中了。在redis2.8中一個LRU緩存經(jīng)常被使用在多個環(huán)境,用戶關(guān)于淘汰的沒有抱怨太多,但是很明顯我可以提高它,通過不僅僅是增加額外的空間,還有額外的CPU時間。
    然而為了提高某項功能,你必須觀察它。有多個不同的方式去觀察LRU算法。你可以通過寫工具觀察,例如模擬不同的工作負(fù)載、校驗命中率和失誤率。
    程序非常簡單:增加一些指定的keys,然后頻繁地訪問這些keys以至于每一個key都有一個下降的空閑時間。最終超過50%的keys被增加,一半的老key需要被淘汰。
    一個完美理想的LRU實現(xiàn),應(yīng)該是沒有最新加的key被淘汰,而是淘汰最初的50%的老key。

    規(guī)則二:不要丟棄重要信息

    借助最新的可視化工具,我可以在嘗試新的方法觀察和測試幾分鐘。使用redis最明顯有效的提高算法就是,積累對立的垃圾信息在一個淘汰池中。
    基本上,當(dāng)N keys算法被采用時,通常會分配一個很大的線程pool(默認(rèn)為16key),這個池按照空閑時間排序,所以只有當(dāng)有一個大于池中的一個或者池為空的時候,最新的key只會進(jìn)入到這個池中。
    同時,一個新的redis-cli模式去測量LRU算法也增加了(看這個-lru-test選項)。
    還有另外一個方式去檢驗LRU算法的好壞,通過一個冪等訪問模式。這個工具通常校驗用一個不同的測試,新算法工作工作效果好于真實世界負(fù)載。它也同樣使用流水線和每秒打印訪問日志,因此可以被使用不用為了基準(zhǔn)不同的思想,至少可以校驗和觀察明顯的速度回歸。

    規(guī)則三、最少使用原則(LFU算法)


    一切源于一個開放性問題:但你有多個redis 3.2數(shù)據(jù)庫時,而淘汰算法只能在本機(jī)選擇。因此,假如你全部空閑小的key都是DB0號機(jī)器,空閑時間長的key都是1號機(jī)器,redis每臺機(jī)器都會淘汰各自的key。一個更好的選擇當(dāng)然是先淘汰DB1,最后再淘汰DB0。
    當(dāng)redis被當(dāng)作緩存使用時很少有情況被分成不同的db上,這不是一個好的處理方式。然而這也是我為什么我再一次修改淘汰代碼的原因。最終,我能夠修改緩存池包括數(shù)據(jù)庫id,使用單緩存池為多個db,代替多緩存池。這種實現(xiàn)很麻煩,但是通過優(yōu)化和修改代碼,最終它比普通實現(xiàn)要快到20%。
    然而這時候,我對這個redis緩存淘汰算法的好奇心又被點燃。我想要提升它。我花費(fèi)了幾天想要提高LRU算法實現(xiàn):或許可以使用更大的緩存池?通過歷史時間選擇最合適被淘汰的key?
    經(jīng)過一段時間,通過優(yōu)化我的工具,我理解到:LRU算法受限于數(shù)據(jù)庫中的數(shù)據(jù)樣本,有時可能相反的場景效果非常好,因此要想提高非常非常難。實際上,能通過展示不同算法的圖片上看這有點非常明顯:每個周期10個keys幾乎和理論的LRU算法表現(xiàn)一致。
    當(dāng)原始算法很難提高時,我開始測試新的算法。 如果我們倒回到博客開始,我們說過LRU實際上有點嚴(yán)格。哪些key需要我們真正想要保留:將來有最大可能被訪問,最頻繁被訪問,而不是最近被訪問的key。
    淘汰最少被訪問的key算法成為:LFU(Least Frequently Used),將來要被淘汰騰出新空間給新key。
    理論上LFU的思想相當(dāng)簡單,只需要給每個key加一個訪問計數(shù)器。每次訪問就自增1,所以也就很容易知道哪些key被訪問更頻繁。
    當(dāng)然,LFU也會帶起其他問題,不單單是針對redis,對于LFU實現(xiàn):
    1、不能使用“移除頂部元素”的方式,keys必須要根據(jù)訪問計數(shù)器進(jìn)行排序。每訪問一次就得遍歷所有key找出訪問次數(shù)最少的key。
    2、LFU不能僅僅是只增加每一訪問的計數(shù)器。正如我們所講的,訪問模式改變隨時變化,因此一個有高訪問次數(shù)的key,后面很可能沒有人繼續(xù)訪問它,因此我們的算法必須要適應(yīng)超時的情況。
    在redis中,第一個問題很好解決:我們可以在LRU的方式一樣:隨機(jī)在緩存池中選舉,淘汰其中某項。第二個問題redis還是存在,因此一般對于LFU的思想必須使用一些方式進(jìn)行減少,或者定期把訪問計數(shù)器減半。

    24位的LFU實現(xiàn)

    LFU有它本身的實現(xiàn),在redis中我們使用自己的24bit來記錄LRU。
    為了實現(xiàn)LFU僅僅需要在每個對象額外新增24bit:
    1、一部分用于保存訪問計數(shù)器;
    2、足夠用于決定什么時候?qū)⒂嫈?shù)器減半的信息;

    我的解決方法是把24bit分成兩列:

    16bits8bitslast decr timeLOG_C

    16位記錄最后一次減半時間,那樣redis知道上一次減半時間,另外8bit作為訪問計數(shù)器。
    你可能會想8位的計數(shù)器很快就會溢出,是的,相對于簡單計數(shù)器,我采用邏輯計數(shù)器。邏輯計數(shù)器的實現(xiàn):

    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;}

    基本上計數(shù)器的較大者,更小的可能計數(shù)器會增加:上面的代碼計算p位于0~1之間,但計數(shù)器增長時會越來越小,位于0-1的隨機(jī)數(shù)r,只會但滿足r<p時計數(shù)器才會加一。
    你可以配置計數(shù)器增長的速率,如果使用默認(rèn)配置,會發(fā)生:

    • 100次訪問后,計數(shù)器=10;
    • 1000次訪問是是18;
    • 10萬次訪問是142;
    • 100萬次訪問后達(dá)到255,并不在繼續(xù)增長;

    下面,讓我們看看計數(shù)器如果進(jìn)行衰減。16位的被儲存為unix時間戳保留到分鐘級別,redis會隨機(jī)掃描key填充到緩存池中,如果最后一個下降的時間大于N分鐘前(可配置化),如果計數(shù)器的值很大就減半,或者對于值小的就直接簡單減半。
    這里又衍生出另外一個問題,就是新進(jìn)來的key是需要有機(jī)會被保留的。由于LFU新增是得分都是0,非常容易被選舉替換掉。在redis中,開始默認(rèn)值為5。這個初始值是根據(jù)增長數(shù)據(jù)和減半算法來估算的。模擬顯示得分小于5的key是首選。

    代碼和性能

    上面描述的算法已經(jīng)提交到一個非穩(wěn)定版的redis分支上。我最初的測試顯示:它在冪等模式下優(yōu)于LRU算法,測試情況是每個key使用用相同數(shù)量的內(nèi)存,然而真實世界的訪問可能會有很大不同。時間和空間都可能改變得很不同,所以我會很開心去學(xué)習(xí)觀察現(xiàn)實世界中LFU的性能如何,兩種方式在redis實現(xiàn)中對性能的改變。
    因此,新增了一個OBJECT FREQ子命令,用于報告給定key的訪問計數(shù)器,不僅僅能有效提觀察一個計數(shù)器,而且還能調(diào)試LFU實現(xiàn)中的bug。
    注意運(yùn)行中切換LRU和LFU,剛開始會隨機(jī)淘汰一些key,隨著24bit不能匹配上,然而慢慢會適應(yīng)。 還有幾種改進(jìn)實現(xiàn)的可能。Ben Manes發(fā)給我這篇感興趣的文章,描述了一種叫TinyLRU算法。鏈接

    這篇文章包含一個非常厲害的觀點:相比于記錄當(dāng)前對象的訪問頻率,讓我們(概率性地)記錄全部對象的訪問頻率,看到了,這種方式我們甚至可以拒絕新key,同樣,我們相信這些key很可能得到很少的訪問,所以一點也不需要淘汰,如果淘汰一個key意味著降低命中/未命中率。
    我的感覺這種技術(shù)雖然很感興趣GET/SET LFU緩存,但不適用與redis性質(zhì)的數(shù)據(jù)服務(wù)器:用戶期望keys被創(chuàng)建后至少存在幾毫秒。拒絕key的創(chuàng)建似乎在redis上就是一種錯誤。
    然而,redis保留了LFU信息,當(dāng)一個key被覆蓋時,舉個例子:

    SET oldkey some_new_value

    24位的LFU計數(shù)器會從老的key復(fù)制到新對象中。

    新的redis淘汰算法不穩(wěn)定版本還有以下幾個好消息:
    1、跨DB策略。在過去的redis只是基于本地的選舉,現(xiàn)在修復(fù)為所有策略,不僅僅是LRU。
    2、易變ttl策略。基于key預(yù)期淘汰存活時間,如今就像其他策略中的使用緩存池。
    3、在緩存池中重用了sds對象,性能更好。

    這篇博客比我預(yù)期要長,但是我希望它反映出一個見解:在創(chuàng)新和對于已經(jīng)存在的事物實現(xiàn)上,一種解決方案去解決一個特定問題,一個基礎(chǔ)工具。由開發(fā)人員以正確的方式使用它。許多redis的用戶把redis作為一個緩存的解決方案,因此提高淘汰策略這一塊經(jīng)常一次又一次被拿出來探討。

    2.6對象

    剛寫了redis主要的數(shù)據(jù)結(jié)構(gòu):

    動態(tài)字符串、雙端鏈表、字典、壓縮列表、整數(shù)集合、跳表等

    redis肯定不能直接使用這些數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)數(shù)據(jù)庫,它用這些數(shù)據(jù)庫建立了一個對象系統(tǒng),包含:

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

    我們可以針對不同的使用場景,為對象設(shè)置多種分不同的數(shù)據(jù)結(jié)構(gòu)實現(xiàn),從而優(yōu)化對象在不同場景下的效率。

    1)鍵值對

    對于redis的鍵值對來說:key只有字符串類型,而v可以是各種類型,

    我們習(xí)慣把“這個鍵所對應(yīng)的值是一個列表”表達(dá)為這是一個“列表鍵。

    TYPE?命令的實現(xiàn)方式也與此類似, 當(dāng)我們對一個數(shù)據(jù)庫鍵執(zhí)行?TYPE?命令時, 命令返回的結(jié)果為數(shù)據(jù)庫鍵對應(yīng)的值對象的類型, 而不是鍵對象的類型:

    # 鍵為字符串對象,值為列表對象redis> RPUSH numbers 1 3 5 (integer) 6redis> TYPE numbers list

    2)對象

    我們看一下redis對象的組成:

    typedef struct redisObject {// 類型unsigned type:4;// 編碼unsigned encoding:4;// 指向底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針void *ptr;// ... } robj;

    通過?encoding?屬性來設(shè)定對象所使用的編碼, 而不是為特定類型的對象關(guān)聯(lián)一種固定的編碼, 極大地提升了 Redis 的靈活性和效率, 因為 Redis 可以根據(jù)不同的使用場景來為一個對象設(shè)置不同的編碼, 從而優(yōu)化對象在某一場景下的效率。

    字符串對象

    字符串對象的編碼可以是?int?、?raw?或者?embstr?。

    如果一個字符串對象保存的是整數(shù)值, 并且這個整數(shù)值可以用?long?類型來表示, 那么字符串對象會將整數(shù)值保存在字符串對象結(jié)構(gòu)的?ptr屬性里面(將?void*?轉(zhuǎn)換成?long?), 并將字符串對象的編碼設(shè)置為?int?。

    如果字符串對象保存的是一個字符串值, 并且這個字符串值的長度大于?39?字節(jié), 那么字符串對象將使用一個簡單動態(tài)字符串(SDS)來保存這個字符串值, 并將對象的編碼設(shè)置為?raw?。

    如果字符串對象保存的是一個字符串值, 并且這個字符串值的長度小于等于?39?字節(jié), 那么字符串對象將使用?embstr?編碼的方式來保存這個字符串值。

    embstr?編碼是專門用于保存短字符串的一種優(yōu)化編碼方式, 這種編碼和?raw?編碼一樣, 都使用?redisObject?結(jié)構(gòu)和?sdshdr?結(jié)構(gòu)來表示字符串對象,但?raw?編碼會調(diào)用兩次內(nèi)存分配函數(shù)來分別創(chuàng)建?redisObject?結(jié)構(gòu)和?sdshdr?結(jié)構(gòu),而?embstr?編碼則通過調(diào)用一次內(nèi)存分配函數(shù)來分配一塊連續(xù)的空間, 空間中依次包含?redisObject?和?sdshdr?兩個結(jié)構(gòu)。

    ?embstr?編碼有以下好處:

  • embstr?編碼創(chuàng)建刪除字符串對象只需操作一次內(nèi)存
  • 因為數(shù)據(jù)都保存在一塊連續(xù)的內(nèi)存, 所以這種編碼的字符串對象比?raw?編碼字符串對象能更好地利用緩存帶來的優(yōu)勢。
  • 3)列表對象

    列表對象的編碼可以是?ziplist?或者?linkedlist?。

    當(dāng)列表對象可以同時滿足以下兩個條件時, 列表對象使用?ziplist?編碼:

  • 列表對象保存的所有字符串元素的長度都小于?64?字節(jié);
  • 列表對象保存的元素數(shù)量小于?512?個;
  • 不能滿足這兩個條件的列表對象需要使用?linkedlist?編碼。

    4)哈希對象

    哈希對象的編碼可以是?ziplist?或者?hashtable?。

    當(dāng)哈希對象可以同時滿足以下兩個條件時, 哈希對象使用?ziplist?編碼:

  • 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于?64?字節(jié);
  • 哈希對象保存的鍵值對數(shù)量小于?512?個;
  • 不能滿足這兩個條件的哈希對象需要使用?hashtable?編碼。

    5)集合對象

    集合對象的編碼可以是?intset?或者?hashtable?。

    當(dāng)集合對象可以同時滿足以下兩個條件時, 對象使用?intset?編碼:

  • 集合對象保存的所有元素都是整數(shù)值;
  • 集合對象保存的元素數(shù)量不超過?512?個;
  • 不能滿足這兩個條件的集合對象需要使用?hashtable?編碼。

    6)有序集合對象

    有序集合的編碼可以是?ziplist?或者?skiplist?。

    當(dāng)有序集合對象可以同時滿足以下兩個條件時, 對象使用?ziplist?編碼:

  • 有序集合保存的元素數(shù)量小于?128?個;
  • 有序集合保存的所有元素成員的長度都小于?64?字節(jié);
  • 不能滿足以上兩個條件的有序集合對象將使用?skiplist?編碼。

    這里多說兩句,各個語言的對象其實都差不多,底層實現(xiàn)也就那幾個,比如java中的容器,c++的STL。java的hashset就是一個哈希而已,hashmap就是k帶了一個v,而”有序的“Treemap使用了紅黑樹這種有平衡性的搜索二叉樹。

    redis的有序集合并沒有再采取hash+紅黑樹的操作,而是把平衡樹換成了跳表,實際上性能真的沒差多少,甚至有時比紅黑樹有優(yōu)勢,比如跳表的性能較為平均,紅黑樹攢了很多次不平衡要調(diào)整可能會帶來資源需求的一個高峰,再加上跳表實現(xiàn)簡單的優(yōu)點,紅黑樹真的沒什么優(yōu)勢。

    并且就算是真的想用一種帶平衡性的搜索樹,現(xiàn)在競賽也是用的華人之光發(fā)明的SB樹。

    有序集合的優(yōu)點就是它的有序操作,比如拿最大最小值,紅黑樹時間o(logN),而哈希表只能一個一個遍歷。缺點在于插入一個值的時間也是o(logN),跳表也是。而哈希表插入數(shù)是o(1).

    要了解底層和這些優(yōu)缺點

    ?

    ?

    《三天給你聊清楚redis》第2天看看redis怎么被搞出來的(22036字)

    三、單機(jī)實現(xiàn)

    3.1、數(shù)據(jù)庫概述

    redis服務(wù)器將所有數(shù)據(jù)庫都保存在redis/redisServer中,數(shù)組db存放所有數(shù)據(jù)庫,每一項是一個redisdb結(jié)構(gòu)。dbnum代表數(shù)據(jù)庫數(shù)量。

    客戶端有一個指針指向當(dāng)前數(shù)據(jù)庫,可以切換,也就是移動指針。

    3.1.1鍵空間

    現(xiàn)在稍微介紹一下redisdb結(jié)構(gòu),它的字典保存了所有鍵值對

    鍵空間的鍵也就是數(shù)據(jù)庫的鍵, 每個鍵都是一個字符串對象。

    鍵空間的值也就是數(shù)據(jù)庫的值, 每個值可以是字符串對象、列表對象、哈希表對象、集合對象、有序集合對象

    所有數(shù)據(jù)庫的操作,添加一個鍵值對, 刪除一個鍵值對, 獲取某個鍵值對, 等等,都是通過對鍵空間字典進(jìn)行操作來實現(xiàn)的。

    3.1.2維護(hù)

    讀寫鍵空間的時候,服務(wù)器會執(zhí)行一些額外操作,比如:

    • 讀一個鍵后(讀操作寫操作都要對鍵讀取),?會根據(jù)鍵是否存在, 更新鍵空間命中(hit)次數(shù)或不命中(miss)次數(shù)。
    • 讀取一個鍵后, 服務(wù)器會更新鍵的 LRU (最后一次使用)時間, 這個值可以用于計算鍵的閑置時間。
    • 如果服務(wù)器在讀一個鍵時, 該鍵已經(jīng)過期, 服務(wù)器會刪除這個鍵, 然后執(zhí)行其他操作。
    • 如果客戶使用?WATCH?監(jiān)視某個鍵,在對這個鍵進(jìn)行修改之后, 會將這個鍵記為臟(dirty),讓事務(wù)程序知到這個鍵被修改
    • 服務(wù)器每次修改一個鍵之后, 都會對臟(dirty)鍵計數(shù)器的值增一, 這個計數(shù)器會觸發(fā)服務(wù)器的持久化以及復(fù)制操作執(zhí)行
    • 如果服務(wù)器開啟了數(shù)據(jù)庫通知功能, 那么在對鍵進(jìn)行修改之后, 服務(wù)器將按配置發(fā)送相應(yīng)的數(shù)據(jù)庫通知。

    3.1.3時間

    用戶可以給某個鍵設(shè)置生存時間,過期時間是一個UNIX時間戳,到時間自動刪除這個鍵。

    redisdb結(jié)構(gòu)的expires字典保存了所有的鍵的過期時間,我們稱這個字典為過期字典。

    3.1.4三種過期鍵刪除策略

    1)定時刪除:創(chuàng)建一個定時器,到時間立即執(zhí)行刪除操作(對內(nèi)存友好,因為能保證過期了立馬刪除,但是對cpu不友好)

    2)惰性刪除:鍵過期不管,每次獲取鍵時檢查是否過期,過期就刪除(對cpu友好,但是只有在使用的時候才可能刪除,對內(nèi)存不友好)

    3)定期刪除:隔一段時間檢查一次(具體算法決定檢查多少刪多少,需要合理設(shè)置)

    3.1.5淘汰策略

    當(dāng)Redis占用內(nèi)存超出最大限制 (maxmemory) 時,可采用如下策略 (maxmemory-policy) ,讓Redis淘汰一些數(shù)據(jù),以騰出空間繼續(xù)提供讀寫服務(wù) :

    noeviction: 對可能導(dǎo)致增大內(nèi)存的命令返回錯誤 (大多數(shù)寫命令,DEL除外) ;

    volatile-ttl: 在設(shè)置了過期時間的key中,選擇剩余壽命 (TTL) 最短的key,將其淘汰;

    volatile-lru: 在設(shè)置了過期時間的key中,選擇最少使用的key (RU) ,將其淘汰;

    volatile-random: 在設(shè)置了過期時間的key中,隨機(jī)選擇一些key,將其淘汰;

    allkeys-1Lru: 在所有的key中,選擇最少使用的key (LRU) ,將其淘汰;

    allkeys-random: 在所有的key中,隨機(jī)選擇一些key,將其淘汰;

    ?

    3.2、持久化

    因為redis是內(nèi)存數(shù)據(jù)庫,他把數(shù)據(jù)都存在內(nèi)存里,所以要想辦法實現(xiàn)持久化功能。

    3.2.1、RDB

    RDB持久化可以手動執(zhí)行,也可以配置定期執(zhí)行,可以把某個時間的數(shù)據(jù)狀態(tài)保存到RDB文件中,反之,我們可以用RDB文件還原數(shù)據(jù)庫狀態(tài)。

    ? ? 生成

    有兩個命令可以生成RDB文件:

    • SAVE?命令由服務(wù)器進(jìn)程直接執(zhí)行保存操作,所以該命令會阻塞服務(wù)器,服務(wù)器不能接受其他指令。
    • BGSAVE?命令由子進(jìn)程執(zhí)行保存操作,所以該命令不會阻塞服務(wù)器,服務(wù)器可以接受其他指令。。

    禁止BGSAVE和SAVE同時執(zhí)行,也就是說執(zhí)行其中一個就會拒絕另一個,這是為了避免父進(jìn)程和子進(jìn)程同時執(zhí)行兩個rdbsave,防止產(chǎn)生競爭條件。

    ? ? 載入

    ? ? RDB載入工作是服務(wù)器啟動時自動執(zhí)行的。

    ? ? 自動保存

    用戶可以通過save選項設(shè)置多個保存條件,服務(wù)器狀態(tài)中會保存所有用?save?選項設(shè)置的保存條件,當(dāng)任意一個保存條件被滿足時,服務(wù)器會自動執(zhí)行?BGSAVE?命令。

    比如

    save 900 1

    save 300 10

    滿足:服務(wù)器在900秒之內(nèi)被修改至少一次或者300秒內(nèi)修改至少十次。就會執(zhí)行BGSAVE。

    ?

    當(dāng)服務(wù)器啟動時,用戶可以通過指定配置文件或者傳入啟動參數(shù)來設(shè)置save選項,服務(wù)器會把條件放到一個結(jié)構(gòu)體里,結(jié)構(gòu)體有一個數(shù)組,保存了所有條件。

    ?

    serverCron函數(shù)默認(rèn)100毫秒檢查一次,他會遍歷數(shù)組依次檢查,符合條件就會執(zhí)行BGSAVE。

    ? ? RDB文件結(jié)構(gòu)

    一個完整 RDB 文件所包含的各個部分:

    REDIS,長度5字節(jié), 保存著?"REDIS"?五個字符。 通過這五個字符, 可以在載入文件時, 快速檢查載入文件是否 RDB 文件。

    db_version?,長度?4?字節(jié), 它的值是一個字符串表示的整數(shù), 這個整數(shù)記錄了 RDB 文件的版本號

    databases?部分包含著零個或任意多個數(shù)據(jù)庫, 以及各個數(shù)據(jù)庫中的鍵值對數(shù)據(jù)

    EOF?常量的長度為?1?字節(jié), 這個常量標(biāo)志著 RDB 文件正文內(nèi)容的結(jié)束

    check_sum?是一個?8?字節(jié)長的無符號整數(shù), 保存著一個校驗和,以此來檢查 RDB 文件是否出錯或損壞

    我并不想深入探究databases的組成。就是知道

    • RDB 文件是一個經(jīng)過壓縮的二進(jìn)制文件,由多個部分組成。
    • 對于不同類型的鍵值對, RDB 文件會使用不同的方式來保存它們即可。

    3.2.2、AOF

    AOF持久化是通過保存服務(wù)器執(zhí)行的命令來記錄狀態(tài)的。還原的時候再執(zhí)行一遍即可。

    功能的實現(xiàn)可以分為命令追加、文件寫入、文件同步三個步驟。

    ?

    當(dāng) AOF 持久化功能處于打開狀態(tài)時, 服務(wù)器在執(zhí)行完一個寫命令之后, 會以協(xié)議格式將被執(zhí)行的寫命令追加到服務(wù)器狀態(tài)的?aof_buf?緩沖區(qū)的末尾:

    struct redisServer {// ...// AOF 緩沖區(qū)sds aof_buf;// ... };

    Redis 服務(wù)器進(jìn)程就是一個事件循環(huán)

    循環(huán)中的文件事件負(fù)責(zé)接收客戶端的命令請求, 以及向客戶端發(fā)送命令回復(fù),

    而時間事件則負(fù)責(zé)執(zhí)行像?serverCron?函數(shù)這樣需要定時運(yùn)行的函數(shù)。

    因為服務(wù)器在處理文件事件時可能會執(zhí)行寫命令, 使得一些內(nèi)容被追加到?aof_buf?緩沖區(qū)里面, 所以在服務(wù)器每次結(jié)束一個事件循環(huán)之前, 它都會調(diào)用?flushAppendOnlyFile?函數(shù), 考慮是否需要將?aof_buf?緩沖區(qū)中的內(nèi)容寫入和保存到 AOF 文件里面, 這個過程可以用偽代碼表示:

    def eventLoop():while True:# 處理文件事件,接收命令請求以及發(fā)送命令回復(fù)# 處理命令請求時可能會有新內(nèi)容被追加到 aof_buf 緩沖區(qū)中processFileEvents()# 處理時間事件processTimeEvents()# 考慮是否要將 aof_buf 中的內(nèi)容寫入和保存到 AOF 文件里面flushAppendOnlyFile()

    flushAppendOnlyFile?函數(shù)的行為由服務(wù)器配置的?appendfsync?選項的值來決定

    值為?always?時, 服務(wù)器在每個事件循環(huán)都要將?aof_buf?緩沖區(qū)中的所有內(nèi)容寫入到 AOF 文件并且同步 AOF 文件, 所以?always?的效率最慢的一個, 但從安全性來說,?always?是最安全的, 因為即使出現(xiàn)故障停機(jī), AOF 持久化也只會丟失一個事件循環(huán)中所產(chǎn)生的命令數(shù)據(jù)。

    值為?everysec?時, 服務(wù)器在每個事件循環(huán)都要將?aof_buf?緩沖區(qū)中的所有內(nèi)容寫入到 AOF 文件, 每隔超過一秒就要在子線程中對 AOF 文件進(jìn)行一次同步: 從效率上來講,?everysec?模式足夠快, 并且就算出現(xiàn)故障停機(jī), 數(shù)據(jù)庫也只丟失一秒鐘的命令數(shù)據(jù)。

    值為?no?時, 服務(wù)器在每個事件循環(huán)都要將?aof_buf?緩沖區(qū)中的所有內(nèi)容寫入到 AOF 文件, 至于何時對 AOF 文件進(jìn)行同步, 則由操作系統(tǒng)控制。

    因為處于?no?模式下的?flushAppendOnlyFile?調(diào)用無須執(zhí)行同步操作, 所以該模式下的 AOF 文件寫入速度總是最快的, 不過因為這種模式會在系統(tǒng)緩存中積累一段時間的寫入數(shù)據(jù), 所以該模式的單次同步時長通常是三種模式中時間最長的: 從平攤操作的角度來看,no?模式和?everysec?模式的效率類似, 當(dāng)出現(xiàn)故障停機(jī)時, 使用?no?模式的服務(wù)器將丟失上次同步 AOF 文件之后的所有寫命令數(shù)據(jù)。

    ? ? 重寫

    AOF持久化是保存了一堆命令來恢復(fù)數(shù)據(jù)庫,隨著時間流逝,存的會越來越多,如果不加以控制,文件過大可能影響服務(wù)器甚至計算機(jī)。而且文件過大,恢復(fù)時需要時間也太長。

    所以redis提供了重寫功能,寫出的新文件不會包含任何浪費(fèi)時間的冗余命令。

    接下來,我們就介紹重寫的原理。

    其實重寫不會對現(xiàn)有的AOF文件進(jìn)行讀取分析等操作,而是通過當(dāng)前服務(wù)器的狀態(tài)來實現(xiàn)。

    # 假設(shè)服務(wù)器對鍵list執(zhí)行了以下命令s; 127.0.0.1:6379> RPUSH list "A" "B" (integer) 2 127.0.0.1:6379> RPUSH list "C" (integer) 3 127.0.0.1:6379> RPUSH list "D" "E" (integer) 5 127.0.0.1:6379> LPOP list "A" 127.0.0.1:6379> LPOP list "B" 127.0.0.1:6379> RPUSH list "F" "G" (integer) 5 127.0.0.1:6379> LRANGE list 0 -1 1) "C" 2) "D" 3) "E" 4) "F" 5) "G" 127.0.0.1:6379>

    當(dāng)前列表鍵list在數(shù)據(jù)庫中的值就為["C", "D", "E", "F", "G"]。要使用盡量少的命令來記錄list鍵的狀態(tài),最簡單的方式不是去讀取和分析現(xiàn)有AOF文件的內(nèi)容,,而是直接讀取list鍵在數(shù)據(jù)庫中的當(dāng)前值,然后用一條RPUSH list "C" "D" "E" "F" "G"代替前面的6條命令。

    • 偽代碼表示如下
    def AOF_REWRITE(tmp_tile_name):f = create(tmp_tile_name)# 遍歷所有數(shù)據(jù)庫for db in redisServer.db:# 如果數(shù)據(jù)庫為空,那么跳過這個數(shù)據(jù)庫if db.is_empty(): continue# 寫入 SELECT 命令,用于切換數(shù)據(jù)庫f.write_command("SELECT " + db.number)# 遍歷所有鍵for key in db:# 如果鍵帶有過期時間,并且已經(jīng)過期,那么跳過這個鍵if key.have_expire_time() and key.is_expired(): continueif key.type == String:# 用 SET key value 命令來保存字符串鍵value = get_value_from_string(key)f.write_command("SET " + key + value)elif key.type == List:# 用 RPUSH key item1 item2 ... itemN 命令來保存列表鍵item1, item2, ..., itemN = get_item_from_list(key)f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)elif key.type == Set:# 用 SADD key member1 member2 ... memberN 命令來保存集合鍵member1, member2, ..., memberN = get_member_from_set(key)f.write_command("SADD " + key + member1 + member2 + ... + memberN)elif key.type == Hash:# 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令來保存哈希鍵field1, value1, field2, value2, ..., fieldN, valueN =\get_field_and_value_from_hash(key)f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\... + fieldN + valueN)elif key.type == SortedSet:# 用 ZADD key score1 member1 score2 member2 ... scoreN memberN# 命令來保存有序集鍵score1, member1, score2, member2, ..., scoreN, memberN = \get_score_and_member_from_sorted_set(key)f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\... + scoreN + memberN)else:raise_type_error()# 如果鍵帶有過期時間,那么用 EXPIREAT key time 命令來保存鍵的過期時間if key.have_expire_time():f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())# 關(guān)閉文件f.close()

    ? ? AOF后臺重寫


    aof_rewrite函數(shù)可以創(chuàng)建新的AOF文件,但是這個函數(shù)會進(jìn)行大量的寫入操作,所以調(diào)用這個函數(shù)的線程被長時間的阻塞,因為服務(wù)器使用單線程來處理命令請求;所以如果直接是服務(wù)器進(jìn)程調(diào)用AOF_REWRITE函數(shù)的話,那么重寫AOF期間,服務(wù)器將無法處理客戶端發(fā)送來的命令請求;


    Redis不希望AOF重寫會造成服務(wù)器無法處理請求,所以將AOF重寫程序放到子進(jìn)程(后臺)里執(zhí)行。這樣處理的好處是:?
    1)子進(jìn)程進(jìn)行AOF重寫期間,主進(jìn)程可以繼續(xù)處理命令請求;
    2)子進(jìn)程帶有主進(jìn)程的數(shù)據(jù)副本,使用子進(jìn)程而不是線程,可以避免在鎖的情況下,保證數(shù)據(jù)的安全性。

    還有一個問題,可能重寫的時候又有新的命令過來,造成信息不對等,所以redis設(shè)置了一個緩沖區(qū),重寫期間把命令放到重寫緩沖區(qū)。
    ?

    ? 總結(jié)
    AOF重寫的目的是為了解決AOF文件體積膨脹的問題,使用更小的體積來保存數(shù)據(jù)庫狀態(tài),整個重寫過程基本上不影響Redis主進(jìn)程處理命令請求;
    AOF重寫其實是一個有歧義的名字,實際上重寫工作是針對數(shù)據(jù)庫的當(dāng)前狀態(tài)來進(jìn)行的,重寫過程中不會讀寫、也不適用原來的AOF文件;
    AOF可以由用戶手動觸發(fā),也可以由服務(wù)器自動觸發(fā)。

    ?

    3.3、事件

    redis服務(wù)器是一個事件驅(qū)動程序。

    需要處理兩類事件:

    1)文件事件:redis是通過套接字與客戶端或者其他服務(wù)器連接的,而文件事件就是服務(wù)器對套接字操作的抽象。

    2)時間事件:服務(wù)器對一些定時操作的抽象。

    3.3.1、文件事件

    redis基于reactor模式開發(fā)了自己的網(wǎng)絡(luò)事件處理器,這個處理器被稱作文件事件處理器,它使用IO多路復(fù)用程序來同時監(jiān)聽多個套接字, 并根據(jù)套接字目前執(zhí)行的任務(wù)來為套接字關(guān)聯(lián)不同的事件處理器,當(dāng)被監(jiān)聽的套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、讀取(read)、寫入(write)、關(guān)閉(close)等操作時, 與操作相對應(yīng)的文件事件就會產(chǎn)生, 這時文件事件處理器就會調(diào)用套接字之前關(guān)聯(lián)好的事件處理器來處理這些事件。

    雖然文件事件處理器以單線程方式運(yùn)行, 但通過使用 I/O 多路復(fù)用程序來監(jiān)聽多個套接字, 文件事件處理器既實現(xiàn)了高性能的網(wǎng)絡(luò)通信模型, 又可以很好地與 Redis 服務(wù)器中其他同樣以單線程方式運(yùn)行的模塊進(jìn)行對接, 這保持了 Redis 內(nèi)部單線程設(shè)計的簡單性。

    文件事件處理器的構(gòu)成:

    I/O 多路復(fù)用程序負(fù)責(zé)監(jiān)聽多個套接字, 并向文件事件分派器傳送那些產(chǎn)生了事件的套接字。

    ?I/O 多路復(fù)用程序會把所有產(chǎn)生事件的套接字放到一個隊列, 以有序(sequentially)、同步(synchronously)、每次一個套接字的方式,向文件事件分派器傳送套接字。

    I/O 多路復(fù)用程序可以監(jiān)聽多個套接字的?ae.h/AE_READABLE?事件和?ae.h/AE_WRITABLE?事件

    1)當(dāng)套接字變得可讀時(客戶端對套接字執(zhí)行?write?操作,或者執(zhí)行?close?操作), 或者有新的可應(yīng)答(acceptable)套接字出現(xiàn)時(客戶端對服務(wù)器的監(jiān)聽套接字執(zhí)行?connect?操作), 套接字產(chǎn)生?AE_READABLE?事件。

    2)當(dāng)套接字變得可寫時(客戶端對套接字執(zhí)行?read?操作), 套接字產(chǎn)生?AE_WRITABLE?事件。

    如果一個套接字又可讀又可寫的話, 那么服務(wù)器將先讀套接字, 后寫套接字。

    下面介紹各種處理器:

    1)連接應(yīng)答處理器:服務(wù)器進(jìn)行初始化時, 程序會將連接應(yīng)答處理器和服務(wù)器監(jiān)聽套接字的?AE_READABLE?事件關(guān)聯(lián), 當(dāng)有客戶端連接(connect)服務(wù)器監(jiān)聽套接字的時候, 套接字就會產(chǎn)生?AE_READABLE?事件, 引發(fā)連接應(yīng)答處理器執(zhí)行, 并執(zhí)行相應(yīng)的套接字應(yīng)答操作。

    2)命令請求處理器:客戶端連接到服務(wù)器后, 服務(wù)器會將客戶端套接字的?AE_READABLE?事件和命令請求處理器關(guān)聯(lián)起來, 當(dāng)客戶端發(fā)送命令請求時, 套接字就會產(chǎn)生?AE_READABLE?事件, 引發(fā)命令請求處理器執(zhí)行, 并執(zhí)行相應(yīng)的套接字讀入操作

    3)命令回復(fù)處理器:服務(wù)器有命令回復(fù)需要傳送給客戶端, 服務(wù)器會將客戶端套接字的?AE_WRITABLE?事件和命令回復(fù)處理器關(guān)聯(lián)起來, 當(dāng)客戶端準(zhǔn)備好接收服務(wù)器傳回的命令回復(fù)時, 就會產(chǎn)生?AE_WRITABLE?事件, 引發(fā)命令回復(fù)處理器執(zhí)行, 并執(zhí)行相應(yīng)的套接字寫入操作。

    一次完整的連接事件實例:

    3.3.2、時間事件

    redis時間事件可以分為兩類:定時事件、周期性事件,他們的特點就像他們的名字一樣。

    而一個時間事件主要有三部分:

    id:服務(wù)器為時間事件創(chuàng)建的全局唯一id,按時間遞增,越新的越大

    when:unix時間戳,記錄到達(dá)時間

    timeProc:時間事件處理器,是一個函數(shù),時間事件到達(dá)時,服務(wù)器就會調(diào)用處理器來處理事件。

    目前版本的redis只使用周期性事件

    來看看實現(xiàn):

    服務(wù)器把所有時間事件放在一個鏈表中,每當(dāng)時間事件執(zhí)行器執(zhí)行時,它就遍歷鏈表,調(diào)用相應(yīng)的事件處理器。

    但是注意:鏈表是無序的,不按when屬性來排序,當(dāng)時間事件執(zhí)行器運(yùn)行時,必須遍歷整個鏈表。但是,無序鏈表并不影響時間事件處理器的性能,因為在目前版本中,redis服務(wù)器只使用serverCron一個時間事件,就算在benchmark模式下也只有兩個事件,服務(wù)器幾乎是把鏈表退化成指針使用了。

    ?

    3.3.3、事件的調(diào)度和執(zhí)行

    ?

    文件事件和時間事件之間是合作關(guān)系, 服務(wù)器會輪流處理這兩種事件,對兩種事件的處理都是同步、有序、原子地進(jìn)行的,處理事件的過程中也不會進(jìn)行搶占,所以時間事件的實際處理時間通常會比設(shè)定的到達(dá)時間晚一些

    大概流程為:

    是否關(guān)閉服務(wù)器?---->等待文件事件產(chǎn)生---->處理已經(jīng)產(chǎn)生的文件事件---->處理已經(jīng)達(dá)到的時間事件---->是否關(guān)閉服務(wù)器?........

    ?

    3.4、客戶端

    redis服務(wù)器是典型的一對多服務(wù)器,通過使用由IO多路復(fù)用技術(shù)實現(xiàn)的文件事件處理器,redis服務(wù)器使用了單線程單進(jìn)程的方式來處理請求。

    3.4.1客戶端的屬性

    • 描述符

    客戶端狀態(tài)的?fd?屬性記錄了客戶端正在使用的套接字描述符:

    typedef struct redisClient {// ...int fd;// ... } redisClient;
    • 偽客戶端fd?值為?-1?: 偽客戶端處理的命令請求來源于 AOF 文件或者 Lua 腳本, 而不是網(wǎng)絡(luò), 所以這種客戶端不需要套接字連接。
    • 普通客戶端?fd?值為大于?-1?的整數(shù): 普通客戶端使用套接字來與服務(wù)器進(jìn)行通訊, 所以服務(wù)器會用?fd?屬性來記錄客戶端套接字的描述符。?

    ?

    • 標(biāo)志

    客戶端的標(biāo)志屬性?flags?記錄了客戶端的角色(role), 以及客戶端目前所處的狀態(tài):

    typedef struct redisClient {// ...int flags;// ...} redisClient;

    flags?屬性的值可以是單個標(biāo)志:

    flags = <flag>

    也可以是多個標(biāo)志的二進(jìn)制或, 比如:

    flags = <flag1> | <flag2> | ...

    每個標(biāo)志使用一個常量表示, 一部分標(biāo)志記錄了客戶端的角色:

    • 在主從服務(wù)器進(jìn)行復(fù)制操作時, 主服務(wù)器會成為從服務(wù)器的客戶端, 而從服務(wù)器也會成為主服務(wù)器的客戶端。?REDIS_MASTER?標(biāo)志表示客戶端代表的是一個主服務(wù)器,?REDIS_SLAVE?標(biāo)志表示客戶端代表的是一個從服務(wù)器。
    • REDIS_LUA_CLIENT?標(biāo)識表示客戶端是專門用于處理 Lua 腳本里面包含的 Redis 命令的偽客戶端。

    另一部分標(biāo)志記錄了客戶端目前所處的狀態(tài):

    以下內(nèi)容為摘抄 REDIS_MONITOR?標(biāo)志表示客戶端正在執(zhí)行?MONITOR?命令。REDIS_UNIX_SOCKET?標(biāo)志表示服務(wù)器使用 UNIX 套接字來連接客戶端。REDIS_BLOCKED?標(biāo)志表示客戶端正在被?BRPOP?、?BLPOP?等命令阻塞。REDIS_UNBLOCKED?標(biāo)志表示客戶端已經(jīng)從?REDIS_BLOCKED?標(biāo)志所表示的阻塞狀態(tài)中脫離出來, 不再阻塞。?REDIS_UNBLOCKED?標(biāo)志只能在?REDIS_BLOCKED?標(biāo)志已經(jīng)打開的情況下使用。REDIS_MULTI?標(biāo)志表示客戶端正在執(zhí)行事務(wù)。REDIS_DIRTY_CAS?標(biāo)志表示事務(wù)使用?WATCH?命令監(jiān)視的數(shù)據(jù)庫鍵已經(jīng)被修改,? REDIS_DIRTY_EXEC?標(biāo)志表示事務(wù)在命令入隊時出現(xiàn)了錯誤, 以上兩個標(biāo)志都表示事務(wù)的安全性已經(jīng)被破壞, 只要這兩個標(biāo)記中的任意一個被打開,? EXEC?命令必然會執(zhí)行失敗。 這兩個標(biāo)志只能在客戶端打開了?REDIS_MULTI?標(biāo)志的情況下使用。REDIS_CLOSE_ASAP?標(biāo)志表示客戶端的輸出緩沖區(qū)大小超出了服務(wù)器允許的范圍, 服務(wù)器會在下一次執(zhí)行?serverCron?函數(shù)時關(guān)閉這個客戶端, 以免服務(wù)器的穩(wěn)定性受到這個客戶端影響。 積存在輸出緩沖區(qū)中的所有內(nèi)容會直接被釋放, 不會返回給客戶端。REDIS_CLOSE_AFTER_REPLY?標(biāo)志表示有用戶對這個客戶端執(zhí)行了?CLIENT_KILL?命令, 或者客戶端發(fā)送給服務(wù)器的命令請求中包含了錯誤的協(xié)議內(nèi)容。 服務(wù)器會將客戶端積存在輸出緩沖區(qū)中的所有內(nèi)容發(fā)送給客戶端, 然后關(guān)閉客戶端。REDIS_ASKING?標(biāo)志表示客戶端向集群節(jié)點(運(yùn)行在集群模式下的服務(wù)器)發(fā)送了?ASKING?命令。REDIS_FORCE_AOF?標(biāo)志強(qiáng)制服務(wù)器將當(dāng)前執(zhí)行的命令寫入到 AOF 文件里面, REDIS_FORCE_REPL?標(biāo)志強(qiáng)制主服務(wù)器將當(dāng)前執(zhí)行的命令復(fù)制給所有從服務(wù)器。 執(zhí)行?PUBSUB?命令會使客戶端打開?REDIS_FORCE_AOF?標(biāo)志, 執(zhí)行?SCRIPT_LOAD?命令會使客戶端打開? REDIS_FORCE_AOF標(biāo)志和?REDIS_FORCE_REPL?標(biāo)志。在主從服務(wù)器進(jìn)行命令傳播期間, 從服務(wù)器需要向主服務(wù)器發(fā)送?REPLICATION ACK?命令, 在發(fā)送這個命令之前, 從服務(wù)器必須打開主服務(wù)器對應(yīng)的客戶端的? REDIS_MASTER_FORCE_REPLY?標(biāo)志, 否則發(fā)送操作會被拒絕執(zhí)行。

    以上提到的所有標(biāo)志都定義在?redis.h?文件里面。

    PUBSUB?命令和?SCRIPT?LOAD?命令的特殊性

    通常情況下, Redis 只會將那些對數(shù)據(jù)庫進(jìn)行了修改的命令寫入到 AOF 文件, 并復(fù)制到各個從服務(wù)器: 如果一個命令沒有對數(shù)據(jù)庫進(jìn)行任何修改, 那么它就會被認(rèn)為是只讀命令, 這個命令不會被寫入到 AOF 文件, 也不會被復(fù)制到從服務(wù)器。

    以上規(guī)則適用于絕大部分 Redis 命令, 但?PUBSUB?命令和?SCRIPT_LOAD?命令是其中的例外。

    PUBSUB?命令雖然沒有修改數(shù)據(jù)庫, 但?PUBSUB?命令向頻道的所有訂閱者發(fā)送消息這一行為帶有副作用, 接收到消息的所有客戶端的狀態(tài)都會因為這個命令而改變。 因此, 服務(wù)器需要使用?REDIS_FORCE_AOF?標(biāo)志, 強(qiáng)制將這個命令寫入 AOF 文件, 這樣在將來載入 AOF 文件時, 服務(wù)器就可以再次執(zhí)行相同的?PUBSUB?命令, 并產(chǎn)生相同的副作用。

    SCRIPT_LOAD?命令的與?PUBSUB?命令類似

    3.4.2輸入緩沖區(qū)

    客戶端狀態(tài)的輸入緩沖區(qū)用于保存客戶端發(fā)送的命令請求:

    typedef struct redisClient {// ...sds querybuf;// ...} redisClient;

    ?redisClient 實例:

    3.4.3命令相關(guān)

    在服務(wù)器將客戶端發(fā)送的命令請求保存到客戶端狀態(tài)的?querybuf?屬性之后, 服務(wù)器將對命令請求的內(nèi)容進(jìn)行分析, 并將得出的命令參數(shù)以及命令參數(shù)的個數(shù)分別保存到客戶端狀態(tài)的?argv?屬性和?argc?屬性:

    typedef struct redisClient {// ...robj **argv;int argc;// ...} redisClient;

    argv?屬性是一個數(shù)組, 數(shù)組中的每個項都是一個字符串對象: 其中?argv[0]?是要執(zhí)行的命令, 而之后的其他項則是傳給命令的參數(shù)。

    argc?屬性則負(fù)責(zé)記錄?argv?數(shù)組的長度。

    3.3.4實現(xiàn)函數(shù)

    ?

    當(dāng)服務(wù)器從協(xié)議內(nèi)容中分析并得出?argv?屬性和?argc?屬性的值之后, 服務(wù)器將根據(jù)項?argv[0]?的值, 在命令表中查找命令所對應(yīng)的命令實現(xiàn)函數(shù)。

    (命令表是一個字典,字典的鍵是一個 SDS 結(jié)構(gòu), 保存了命令的名字, 字典的值是命令所對應(yīng)的?redisCommand?結(jié)構(gòu), 這個結(jié)構(gòu)保存了命令的實現(xiàn)函數(shù)、 命令的標(biāo)志、 命令應(yīng)該給定的參數(shù)個數(shù)、 命令的總執(zhí)行次數(shù)和總消耗時長等統(tǒng)計信息。)

    3.3.5、輸出緩沖區(qū)

    執(zhí)行命令所得的命令回復(fù)會被保存在客戶端狀態(tài)的輸出緩沖區(qū)里面, 每個客戶端都有兩個輸出緩沖區(qū):

    • 固定大小的緩沖區(qū)用于保存那些長度比較小的回復(fù), 比如?OK?、簡短的字符串值、整數(shù)值、錯誤回復(fù),等等。
    • 可變大小的緩沖區(qū)用于保存那些長度比較大的回復(fù), 比如一個非常長的字符串值, 一個由很多項組成的列表, 一個包含了很多元素的集合, 等等。

    3.3.6、其它

    客戶端狀態(tài)的?authenticated?屬性用于記錄客戶端是否通過了身份驗證,還有幾個和時間有關(guān)的屬性,敘述是一件挺無聊的事情,不再寫。

    ?

    3.4、命令的執(zhí)行過程

    3.4.1發(fā)送命令請求

    當(dāng)用戶在客戶端中鍵入一個命令請求時, 客戶端會將這個命令請求轉(zhuǎn)換成協(xié)議格式, 然后通過連接到服務(wù)器的套接字, 將協(xié)議格式的命令請求發(fā)送給服務(wù)器。

    3.4.2讀取命令請求

    當(dāng)客戶端與服務(wù)器之間的連接套接字因為客戶端的寫入而變得可讀時, 服務(wù)器將調(diào)用命令請求處理器來執(zhí)行以下操作:

  • 讀取套接字中協(xié)議格式的命令請求, 并將其保存到客戶端狀態(tài)的輸入緩沖區(qū)里面。
  • 對輸入緩沖區(qū)中的命令請求進(jìn)行分析, 提取出命令請求中包含的命令參數(shù), 以及命令參數(shù)的個數(shù), 然后分別將參數(shù)和參數(shù)個數(shù)保存到客戶端狀態(tài)的?argv?屬性和?argc?屬性里面。
  • 調(diào)用命令執(zhí)行器, 執(zhí)行客戶端指定的命令。
  • 3.4.3命令執(zhí)行器:查找命令實現(xiàn)

    命令執(zhí)行器要做的第一件事就是根據(jù)客戶端狀態(tài)的?argv[0]?參數(shù), 在命令表(command table)中查找參數(shù)所指定的命令, 并將找到的命令保存到客戶端狀態(tài)的?cmd?屬性里面。

    命令表是一個字典, 字典的鍵是一個個命令名字,比如?"set"?、?"get"?、?"del"?,等等; 而字典的值是一個個?redisCommand?結(jié)構(gòu), 每個?redisCommand?結(jié)構(gòu)記錄了一個 Redis 命令的實現(xiàn)信息。

    命令名字的大小寫不影響命令表的查找結(jié)果

    因為命令表使用的是大小寫無關(guān)的查找算法, 無論輸入的命令名字是大寫、小寫或者混合大小寫, 只要命令的名字是正確的, 就能找到相應(yīng)的 redisCommand 結(jié)構(gòu)。

    比如說, 無論用戶輸入的命令名字是 "SET" 、 "set" 、 "SeT" 又或者 "sEt" , 命令表返回的都是同一個 redisCommand 結(jié)構(gòu)。

    redis> SET msg "hello world" OKredis> set msg "hello world" OKredis> SeT msg "hello world" OKredis> sEt msg "hello world" OK

    3.4.4命令執(zhí)行器:執(zhí)行預(yù)備操作

    到目前為止, 服務(wù)器已經(jīng)將執(zhí)行命令所需的命令實現(xiàn)函數(shù)(保存在客戶端狀態(tài)的?cmd?屬性)、參數(shù)(保存在客戶端狀態(tài)的?argv?屬性)、參數(shù)個數(shù)(保存在客戶端狀態(tài)的?argc?屬性)都收集齊了, 但是在真正執(zhí)行命令之前, 程序還需要進(jìn)行一些預(yù)備操作, 從而確保命令可以正確、順利地被執(zhí)行, 這些操作包括:

    • 檢查客戶端狀態(tài)的?cmd?指針是否指向?NULL?, 如果是的話, 那么說明用戶輸入的命令名字找不到相應(yīng)的命令實現(xiàn), 服務(wù)器不再執(zhí)行后續(xù)步驟, 并向客戶端返回一個錯誤。
    • 根據(jù)客戶端?cmd?屬性指向的?redisCommand?結(jié)構(gòu)的?arity?屬性, 檢查命令請求所給定的參數(shù)個數(shù)是否正確, 當(dāng)參數(shù)個數(shù)不正確時, 不再執(zhí)行后續(xù)步驟, 直接向客戶端返回一個錯誤。 比如說, 如果?redisCommand?結(jié)構(gòu)的?arity?屬性的值為?-3?, 那么用戶輸入的命令參數(shù)個數(shù)必須大于等于?3?個才行。
    • 檢查客戶端是否已經(jīng)通過了身份驗證, 未通過身份驗證的客戶端只能執(zhí)行?AUTH?命令, 如果未通過身份驗證的客戶端試圖執(zhí)行除?AUTH?命令之外的其他命令, 那么服務(wù)器將向客戶端返回一個錯誤。
    • 如果服務(wù)器打開了?maxmemory?功能, 那么在執(zhí)行命令之前, 先檢查服務(wù)器的內(nèi)存占用情況, 并在有需要時進(jìn)行內(nèi)存回收, 從而使得接下來的命令可以順利執(zhí)行。 如果內(nèi)存回收失敗, 那么不再執(zhí)行后續(xù)步驟, 向客戶端返回一個錯誤。
    • 如果服務(wù)器上一次執(zhí)行?BGSAVE?命令時出錯, 并且服務(wù)器打開了?stop-writes-on-bgsave-error?功能, 而且服務(wù)器即將要執(zhí)行的命令是一個寫命令, 那么服務(wù)器將拒絕執(zhí)行這個命令, 并向客戶端返回一個錯誤。
    • 如果客戶端當(dāng)前正在用?SUBSCRIBE?命令訂閱頻道, 或者正在用?PSUBSCRIBE?命令訂閱模式, 那么服務(wù)器只會執(zhí)行客戶端發(fā)來的?SUBSCRIBE?、?PSUBSCRIBE?、?UNSUBSCRIBE?、?PUNSUBSCRIBE?四個命令, 其他別的命令都會被服務(wù)器拒絕。
    • 如果服務(wù)器正在進(jìn)行數(shù)據(jù)載入, 那么客戶端發(fā)送的命令必須帶有?l?標(biāo)識(比如?INFO?、?SHUTDOWN?、?PUBLISH?,等等)才會被服務(wù)器執(zhí)行, 其他別的命令都會被服務(wù)器拒絕。
    • 如果服務(wù)器因為執(zhí)行 Lua 腳本而超時并進(jìn)入阻塞狀態(tài), 那么服務(wù)器只會執(zhí)行客戶端發(fā)來的?SHUTDOWN nosave?命令和?SCRIPT KILL?命令, 其他別的命令都會被服務(wù)器拒絕。
    • 如果客戶端正在執(zhí)行事務(wù), 那么服務(wù)器只會執(zhí)行客戶端發(fā)來的?EXEC?、?DISCARD?、?MULTI?、?WATCH?四個命令, 其他命令都會被放進(jìn)事務(wù)隊列中。
    • 如果服務(wù)器打開了監(jiān)視器功能, 那么服務(wù)器會將要執(zhí)行的命令和參數(shù)等信息發(fā)送給監(jiān)視器。

    當(dāng)完成了以上預(yù)備操作之后, 服務(wù)器就可以開始真正執(zhí)行命令了。

    3.4.5命令執(zhí)行器:調(diào)用命令的實現(xiàn)函數(shù)

    在前面的操作中, 服務(wù)器已經(jīng)將要執(zhí)行命令的實現(xiàn)保存到了客戶端狀態(tài)的?cmd?屬性里面, 并將命令的參數(shù)和參數(shù)個數(shù)分別保存到了客戶端狀態(tài)的?argv?屬性和?argc?屬性里面, 當(dāng)服務(wù)器決定要執(zhí)行命令時, 它只要執(zhí)行以下語句就可以了:

    // client 是指向客戶端狀態(tài)的指針client->cmd->proc(client);

    因為執(zhí)行命令所需的實際參數(shù)都已經(jīng)保存到客戶端狀態(tài)的?argv?屬性里面了, 所以命令的實現(xiàn)函數(shù)只需要一個指向客戶端狀態(tài)的指針作為參數(shù)即可。

    3.4.6命令執(zhí)行器:執(zhí)行后續(xù)工作

    在執(zhí)行完實現(xiàn)函數(shù)之后, 服務(wù)器還需要執(zhí)行一些后續(xù)工作:

    • 如果服務(wù)器開啟了慢查詢?nèi)罩竟δ?#xff0c; 那么慢查詢?nèi)罩灸K會檢查是否需要為剛剛執(zhí)行完的命令請求添加一條新的慢查詢?nèi)罩尽?/li>
    • 根據(jù)剛剛執(zhí)行命令所耗費(fèi)的時長, 更新被執(zhí)行命令的?redisCommand?結(jié)構(gòu)的?milliseconds?屬性, 并將命令的?redisCommand?結(jié)構(gòu)的?calls?計數(shù)器的值增一。
    • 如果服務(wù)器開啟了 AOF 持久化功能, 那么 AOF 持久化模塊會將剛剛執(zhí)行的命令請求寫入到 AOF 緩沖區(qū)里面。
    • 如果有其他從服務(wù)器正在復(fù)制當(dāng)前這個服務(wù)器, 那么服務(wù)器會將剛剛執(zhí)行的命令傳播給所有從服務(wù)器。

    當(dāng)以上操作都執(zhí)行完了之后, 服務(wù)器對于當(dāng)前命令的執(zhí)行到此就告一段落了, 之后服務(wù)器就可以繼續(xù)從文件事件處理器中取出并處理下一個命令請求了。

    3.4.7將命令回復(fù)發(fā)送給客戶端

    前面說過, 命令實現(xiàn)函數(shù)會將命令回復(fù)保存到客戶端的輸出緩沖區(qū)里面, 并為客戶端的套接字關(guān)聯(lián)命令回復(fù)處理器, 當(dāng)客戶端套接字變?yōu)榭蓪憼顟B(tài)時, 服務(wù)器就會執(zhí)行命令回復(fù)處理器, 將保存在客戶端輸出緩沖區(qū)中的命令回復(fù)發(fā)送給客戶端。

    當(dāng)命令回復(fù)發(fā)送完畢之后, 回復(fù)處理器會清空客戶端狀態(tài)的輸出緩沖區(qū), 為處理下一個命令請求做好準(zhǔn)備。

    3.4.8客戶端接收并打印命令回復(fù)

    當(dāng)客戶端接收到協(xié)議格式的命令回復(fù)之后, 它會將這些回復(fù)轉(zhuǎn)換成人類可讀的格式, 并打印給用戶觀看(假設(shè)使用的是 Redis 自帶的?客戶端)

    ?

    3.5、事務(wù)

    Redis 事務(wù)可以一次執(zhí)行多個命令, 并且?guī)в幸韵氯齻€重要的保證:

    • 批量操作在發(fā)送 EXEC 命令前被放入隊列緩存。
    • 收到 EXEC 命令后進(jìn)入事務(wù)執(zhí)行,事務(wù)中任意命令執(zhí)行失敗,其余的命令依然被執(zhí)行。
    • 在事務(wù)執(zhí)行過程,其他客戶端提交的命令請求不會插入到事務(wù)執(zhí)行命令序列中。

    一個事務(wù)從開始到執(zhí)行會經(jīng)歷以下三個階段:

    • 開始事務(wù)。
    • 命令入隊。
    • 執(zhí)行事務(wù)。

    以下是一個事務(wù)的例子, 它先以?MULTI?開始一個事務(wù), 然后將多個命令入隊到事務(wù)中, 最后由?EXEC?命令觸發(fā)事務(wù), 一并執(zhí)行事務(wù)中的所有命令:

    redis 127.0.0.1:6379> MULTI OKredis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days" QUEUEDredis 127.0.0.1:6379> GET book-name QUEUEDredis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series" QUEUEDredis 127.0.0.1:6379> SMEMBERS tag QUEUEDredis 127.0.0.1:6379> EXEC 1) OK 2) "Mastering C++ in 21 days" 3) (integer) 3 4) 1) "Mastering Series"2) "C++"3) "Programming"

    詳細(xì)介紹:

    3.5.1事務(wù)開始

    MULTI?命令的執(zhí)行標(biāo)志著事務(wù)的開始:

    redis> MULTI OK

    MULTI?命令可以將執(zhí)行該命令的客戶端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài), 這一切換是通過在客戶端狀態(tài)的?flags?屬性中打開?REDIS_MULTI?標(biāo)識來完成的,?MULTI?命令的實現(xiàn)可以用以下偽代碼來表示:

    def MULTI():# 打開事務(wù)標(biāo)識client.flags |= REDIS_MULTI# 返回 OK 回復(fù)replyOK()

    3.5.2命令入隊

    當(dāng)一個客戶端處于非事務(wù)狀態(tài)時, 這個客戶端發(fā)送的命令會立即被服務(wù)器執(zhí)行:

    redis> SET "name" "Practical Common Lisp" OKredis> GET "name" "Practical Common Lisp"redis> SET "author" "Peter Seibel" OKredis> GET "author" "Peter Seibel"

    與此不同的是, 當(dāng)一個客戶端切換到事務(wù)狀態(tài)之后, 服務(wù)器會根據(jù)這個客戶端發(fā)來的不同命令執(zhí)行不同的操作:

    • 如果客戶端發(fā)送的命令為?EXEC?、?DISCARD?、?WATCH?、?MULTI?四個命令的其中一個, 那么服務(wù)器立即執(zhí)行這個命令。
    • 與此相反, 如果客戶端發(fā)送的命令是?EXEC?、?DISCARD?、?WATCH?、?MULTI?四個命令以外的其他命令, 那么服務(wù)器并不立即執(zhí)行這個命令, 而是將這個命令放入一個事務(wù)隊列里面, 然后向客戶端返回?QUEUED?回復(fù)。

    3.5.3事務(wù)隊列

    每個 Redis 客戶端都有自己的事務(wù)狀態(tài), 這個事務(wù)狀態(tài)保存在客戶端狀態(tài)的?mstate?屬性里面:

    typedef struct redisClient {// ...// 事務(wù)狀態(tài)multiState mstate; /* MULTI/EXEC state */// ...} redisClient;

    事務(wù)狀態(tài)包含一個事務(wù)隊列, 以及一個已入隊命令的計數(shù)器 (也可以說是事務(wù)隊列的長度):

    typedef struct multiState {// 事務(wù)隊列,FIFO 順序multiCmd *commands;// 已入隊命令計數(shù)int count;} multiState;

    事務(wù)隊列是一個?multiCmd?類型的數(shù)組, 數(shù)組中的每個?multiCmd?結(jié)構(gòu)都保存了一個已入隊命令的相關(guān)信息, 包括指向命令實現(xiàn)函數(shù)的指針, 命令的參數(shù), 以及參數(shù)的數(shù)量:

    typedef struct multiCmd {// 參數(shù)robj **argv;// 參數(shù)數(shù)量int argc;// 命令指針struct redisCommand *cmd;} multiCmd;

    事務(wù)隊列以先進(jìn)先出(FIFO)的方式保存入隊的命令: 較先入隊的命令會被放到數(shù)組的前面, 而較后入隊的命令則會被放到數(shù)組的后面。

    舉個例子, 如果客戶端執(zhí)行以下命令:

    redis> MULTI OKredis> SET "name" "Practical Common Lisp" QUEUEDredis> GET "name" QUEUEDredis> SET "author" "Peter Seibel" QUEUEDredis> GET "author" QUEUED

    那么服務(wù)器將為客戶端創(chuàng)建事務(wù)狀態(tài):

    • 最先入隊的?SET?命令被放在了事務(wù)隊列的索引?0?位置上。
    • 第二入隊的?GET?命令被放在了事務(wù)隊列的索引?1?位置上。
    • 第三入隊的另一個?SET?命令被放在了事務(wù)隊列的索引?2?位置上。
    • 最后入隊的另一個?GET?命令被放在了事務(wù)隊列的索引?3?位置上。

    3.5.4執(zhí)行事務(wù)

    當(dāng)一個處于事務(wù)狀態(tài)的客戶端向服務(wù)器發(fā)送?EXEC?命令時, 這個?EXEC?命令將立即被服務(wù)器執(zhí)行: 服務(wù)器會遍歷這個客戶端的事務(wù)隊列, 執(zhí)行隊列中保存的所有命令, 最后將執(zhí)行命令所得的結(jié)果全部返回給客戶端。

    EXEC?命令的實現(xiàn)原理可以用以下偽代碼來描述:

    def EXEC():# 創(chuàng)建空白的回復(fù)隊列reply_queue = []# 遍歷事務(wù)隊列中的每個項# 讀取命令的參數(shù),參數(shù)的個數(shù),以及要執(zhí)行的命令for argv, argc, cmd in client.mstate.commands:# 執(zhí)行命令,并取得命令的返回值reply = execute_command(cmd, argv, argc)# 將返回值追加到回復(fù)隊列末尾reply_queue.append(reply)# 移除 REDIS_MULTI 標(biāo)識,讓客戶端回到非事務(wù)狀態(tài)client.flags &= ~REDIS_MULTI# 清空客戶端的事務(wù)狀態(tài),包括:# 1)清零入隊命令計數(shù)器# 2)釋放事務(wù)隊列client.mstate.count = 0release_transaction_queue(client.mstate.commands)# 將事務(wù)的執(zhí)行結(jié)果返回給客戶端send_reply_to_client(client, reply_queue)

    3.5.5WATCH命令的實現(xiàn)

    WATCH命令是一個樂觀鎖,它可以在EXEC命令執(zhí)行之前,監(jiān)視任意數(shù)量的數(shù)據(jù)庫鍵,并在EXEC執(zhí)行后,檢查被監(jiān)視的鍵是否至少有一個被修改,如果是,服務(wù)器拒絕執(zhí)行事務(wù),并向客戶端返回代表事務(wù)執(zhí)行失敗的回復(fù)。

    /* Redis database representation. There are multiple databases identified* by integers from 0 (the default database) up to the max configured* database. The database number is the 'id' field in the structure. */ typedef struct redisDb {dict *dict; /* The keyspace for this DB 數(shù)據(jù)庫鍵空間,保存數(shù)據(jù)庫中所有的鍵值對*/dict *expires; /* Timeout of keys with a timeout set 保存過期時間*/dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */dict *ready_keys; /* Blocked keys that received a PUSH 已經(jīng)準(zhǔn)備好數(shù)據(jù)的阻塞狀態(tài)的key*/dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS 事物模塊,用于保存被WATCH命令所監(jiān)控的鍵*/// 當(dāng)內(nèi)存不足時,Redis會根據(jù)LRU算法回收一部分鍵所占的空間,而該eviction_pool是一個長為16數(shù)組,保存可能被回收的鍵// eviction_pool中所有鍵按照idle空轉(zhuǎn)時間,從小到大排序,每次回收空轉(zhuǎn)時間最長的鍵struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */// 數(shù)據(jù)庫IDint id; /* Database ID */// 鍵的平均過期時間long long avg_ttl; /* Average TTL, just for stats */ } redisDb;

    在每個代表數(shù)據(jù)庫的 server.h/redisDb?結(jié)構(gòu)類型中, 都保存了一個?watched_keys?字典, 字典的鍵是這個數(shù)據(jù)庫被監(jiān)視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了所有監(jiān)視這個鍵的客戶端。比如說,以下字典就展示了一個?watched_keys?字典的例子:

    每個key后掛著監(jiān)視自己的客戶端。

    3.5.6監(jiān)控的觸發(fā)

    在任何對數(shù)據(jù)庫鍵空間(key space)進(jìn)行修改的命令成功執(zhí)行之后 (比如?FLUSHDB?、?SET?、?DEL?、?LPUSH?、?SADD?、?ZREM?,諸如此類),?multi.c/touchWatchedKey?函數(shù)都會被調(diào)用 (修改命令會調(diào)用signalModifiedKey()函數(shù)來處理數(shù)據(jù)庫中的鍵被修改的情況,該函數(shù)直接調(diào)用touchWatchedKey()函數(shù))—— 它檢查數(shù)據(jù)庫的?watched_keys?字典, 看是否有客戶端在監(jiān)視已經(jīng)被命令修改的鍵, 如果有的話, 程序?qū)⑺斜O(jiān)視這個/這些被修改鍵的客戶端的?REDIS_DIRTY_CAS?選項打開:
    ?

    /* "Touch" a key, so that if this key is being WATCHed by some client the* next EXEC will fail. */ // Touch 一個 key,如果該key正在被監(jiān)視,那么客戶端會執(zhí)行EXEC失敗 void touchWatchedKey(redisDb *db, robj *key) {list *clients;listIter li;listNode *ln;// 字典為空,沒有任何鍵被監(jiān)視if (dictSize(db->watched_keys) == 0) return;// 獲取所有監(jiān)視這個鍵的客戶端 clients = dictFetchValue(db->watched_keys, key);// 沒找到返回if (!clients) return;/* Mark all the clients watching this key as CLIENT_DIRTY_CAS *//* Check if we are already watching for this key */// 遍歷所有客戶端,打開他們的 REDIS_DIRTY_CAS 標(biāo)識listRewind(clients,&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);// 設(shè)置CLIENT_DIRTY_CAS標(biāo)識c->flags |= CLIENT_DIRTY_CAS;} }

    3.5.7事務(wù)的ACID性質(zhì)

    ?在傳統(tǒng)的關(guān)系式數(shù)據(jù)庫中,常常用?ACID 性質(zhì)來檢驗事務(wù)功能的安全性。

    redis事物總是具有前三個性質(zhì)。

    a)原子性atomicity:redis事務(wù)保證事務(wù)中的命令要么全部執(zhí)行要不全部不執(zhí)行。

    但是redis不同于傳統(tǒng)關(guān)系型數(shù)據(jù)庫,不支持回滾,即使出現(xiàn)了錯誤,事務(wù)也會繼續(xù)執(zhí)行下去。

    因為redis作者認(rèn)為,這種復(fù)雜的機(jī)制和redis追求的簡單高效不符。并且,redis事務(wù)錯誤通常是編程錯誤,只會出現(xiàn)在開發(fā)環(huán)境中,而不會出現(xiàn)在實際生產(chǎn)環(huán)境中,所以沒必要支持回滾。

    b)一致性consistency:redis事務(wù)可以保證命令失敗的情況下得以回滾,數(shù)據(jù)能恢復(fù)到?jīng)]有執(zhí)行之前的樣子,是保證一致性的,除非redis進(jìn)程意外終結(jié)。

    Redis 的一致性問題可以分為三部分來討論:入隊錯誤、執(zhí)行錯誤、Redis 進(jìn)程被終結(jié)。

    入隊錯誤

    在命令入隊的過程中,如果客戶端向服務(wù)器發(fā)送了錯誤的命令,比如命令的參數(shù)數(shù)量不對,等等, 那么服務(wù)器將向客戶端返回一個出錯信息, 并且將客戶端的事務(wù)狀態(tài)設(shè)為?REDIS_DIRTY_EXEC?。
    因此,帶有不正確入隊命令的事務(wù)不會被執(zhí)行,也不會影響數(shù)據(jù)庫的一致性。

    執(zhí)行錯誤

    如果命令在事務(wù)執(zhí)行的過程中發(fā)生錯誤,比如說,對一個不同類型的 key 執(zhí)行了錯誤的操作, 那么 Redis 只會將錯誤包含在事務(wù)的結(jié)果中, 這不會引起事務(wù)中斷或整個失敗,不會影響已執(zhí)行事務(wù)命令的結(jié)果,也不會影響后面要執(zhí)行的事務(wù)命令, 所以它對事務(wù)的一致性也沒有影響。

    Redis 進(jìn)程被終結(jié)

    如果 Redis 服務(wù)器進(jìn)程在執(zhí)行事務(wù)的過程中被其他進(jìn)程終結(jié),或者被管理員強(qiáng)制殺死,那么根據(jù) Redis 所使用的持久化模式,可能有以下情況出現(xiàn):

    內(nèi)存模式:如果 Redis 沒有采取任何持久化機(jī)制,那么重啟之后的數(shù)據(jù)庫總是空白的,所以數(shù)據(jù)總是一致的。

    RDB 模式:在執(zhí)行事務(wù)時,Redis 不會中斷事務(wù)去執(zhí)行保存 RDB 的工作,只有在事務(wù)執(zhí)行之后,保存 RDB 的工作才有可能開始。所以當(dāng) RDB 模式下的 Redis 服務(wù)器進(jìn)程在事務(wù)中途被殺死時,事務(wù)內(nèi)執(zhí)行的命令,不管成功了多少,都不會被保存到 RDB 文件里。恢復(fù)數(shù)據(jù)庫需要使用現(xiàn)有的 RDB 文件,而這個 RDB 文件的數(shù)據(jù)保存的是最近一次的數(shù)據(jù)庫快照(snapshot),所以它的數(shù)據(jù)可能不是最新的,但只要 RDB 文件本身沒有因為其他問題而出錯,那么還原后的數(shù)據(jù)庫就是一致的。

    AOF 模式:因為保存 AOF 文件的工作在后臺線程進(jìn)行,所以即使是在事務(wù)執(zhí)行的中途,保存 AOF 文件的工作也可以繼續(xù)進(jìn)行,因此,根據(jù)事務(wù)語句是否被寫入并保存到 AOF 文件,有以下兩種情況發(fā)生:

    1)如果事務(wù)語句未寫入到 AOF 文件,或 AOF 未被 SYNC 調(diào)用保存到磁盤,那么當(dāng)進(jìn)程被殺死之后,Redis 可以根據(jù)最近一次成功保存到磁盤的 AOF 文件來還原數(shù)據(jù)庫,只要 AOF 文件本身沒有因為其他問題而出錯,那么還原后的數(shù)據(jù)庫總是一致的,但其中的數(shù)據(jù)不一定是最新的。

    2)如果事務(wù)的部分語句被寫入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的事務(wù)執(zhí)行信息就會遺留在 AOF 文件里,當(dāng)重啟 Redis 時,程序會檢測到 AOF 文件并不完整,Redis 會退出,并報告錯誤。需要使用 redis-check-aof 工具將部分成功的事務(wù)命令移除之后,才能再次啟動服務(wù)器。還原之后的數(shù)據(jù)總是一致的,而且數(shù)據(jù)也是最新的(直到事務(wù)執(zhí)行之前為止)。
    ?

    c)隔離性Isolation:redis事務(wù)是嚴(yán)格遵守隔離性的,原因是redis是單進(jìn)程單線程模式,可以保證命令執(zhí)行過程中不會被其他客戶端命令打斷。

    因為redis使用單線程執(zhí)行事務(wù),并且保證不會中斷,所以肯定有隔離性。

    d)持久性Durability:持久性是指:當(dāng)一個事務(wù)執(zhí)行完畢,結(jié)果已經(jīng)保存在永久介質(zhì)里,比如硬盤,所以即使服務(wù)器后來停機(jī)了,結(jié)果也不會丟失

    redis事務(wù)是不保證持久性的,這是因為redis持久化策略中不管是RDB還是AOF都是異步執(zhí)行的,不保證持久性是出于對性能的考慮。


    3.5.8重點提煉

    • 事務(wù)提供了一種將多個命令打包, 然后一次性、有序地執(zhí)行的機(jī)制。
    • 多個命令會被入隊到事務(wù)隊列中, 然后按先進(jìn)先出(FIFO)的順序執(zhí)行。
    • 事務(wù)在執(zhí)行過程中不會被中斷, 當(dāng)事務(wù)隊列中的所有命令都被執(zhí)行完畢之后, 事務(wù)才會結(jié)束。
    • 帶有?WATCH?命令的事務(wù)會將客戶端和被監(jiān)視的鍵在數(shù)據(jù)庫的?watched_keys?字典中進(jìn)行關(guān)聯(lián), 當(dāng)鍵被修改時, 程序會將所有監(jiān)視被修改鍵的客戶端的?REDIS_DIRTY_CAS?標(biāo)志打開。
    • 只有在客戶端的?REDIS_DIRTY_CAS?標(biāo)志未被打開時, 服務(wù)器才會執(zhí)行客戶端提交的事務(wù), 否則的話, 服務(wù)器將拒絕執(zhí)行客戶端提交的事務(wù)。
    • Redis 的事務(wù)總是保證 ACID 中的原子性、一致性和隔離性, 當(dāng)服務(wù)器運(yùn)行在 AOF 持久化模式下, 并且?appendfsync?選項的值為?always?時, 事務(wù)也具有耐久性。

    ?

    以上就是 Redis 客戶端和服務(wù)器執(zhí)行命令請求的整個過程了。

    ?

    3.6、發(fā)布和訂閱

    3.6.1頻道的訂閱和退訂

    當(dāng)一個客戶端執(zhí)行?SUBSCRIBE?命令, 訂閱某個或某些頻道的時候, 這個客戶端與被訂閱頻道之間就建立起了一種訂閱關(guān)系。

    Redis 將所有頻道的訂閱關(guān)系都保存在服務(wù)器狀態(tài)的?pubsub_channels?字典里面, 這個字典的鍵是某個被訂閱的頻道, 而鍵的值則是一個鏈表, 鏈表里面記錄了所有訂閱這個頻道的客戶端:

    struct redisServer {// ...// 保存所有頻道的訂閱關(guān)系dict *pubsub_channels;// ...};

    每當(dāng)客戶端執(zhí)行?SUBSCRIBE?命令, 訂閱某個或某些頻道的時候, 服務(wù)器都會將客戶端與被訂閱的頻道在?pubsub_channels?字典中進(jìn)行關(guān)聯(lián)。

    根據(jù)頻道是否已經(jīng)有其他訂閱者, 關(guān)聯(lián)操作分為兩種情況執(zhí)行:

    • 如果頻道已經(jīng)有其他訂閱者, 那么它在?pubsub_channels?字典中必然有相應(yīng)的訂閱者鏈表, 程序唯一要做的就是將客戶端添加到訂閱者鏈表的末尾。
    • 如果頻道還未有任何訂閱者, 那么它必然不存在于?pubsub_channels?字典, 程序首先要在?pubsub_channels?字典中為頻道創(chuàng)建一個鍵, 并將這個鍵的值設(shè)置為空鏈表, 然后再將客戶端添加到鏈表, 成為鏈表的第一個元素。

    SUBSCRIBE?命令的實現(xiàn)可以用以下偽代碼來描述:

    def subscribe(*all_input_channels):# 遍歷輸入的所有頻道for channel in all_input_channels:# 如果 channel 不存在于 pubsub_channels 字典(沒有任何訂閱者)# 那么在字典中添加 channel 鍵,并設(shè)置它的值為空鏈表if channel not in server.pubsub_channels:server.pubsub_channels[channel] = []# 將訂閱者添加到頻道所對應(yīng)的鏈表的末尾server.pubsub_channels[channel].append(client)

    ?

    UNSUBSCRIBE?命令的行為和?SUBSCRIBE?命令的行為正好相反 —— 當(dāng)一個客戶端退訂某個或某些頻道的時候, 服務(wù)器將從?pubsub_channels?中解除客戶端與被退訂頻道之間的關(guān)聯(lián):

    • 程序會根據(jù)被退訂頻道的名字, 在?pubsub_channels?字典中找到頻道對應(yīng)的訂閱者鏈表, 然后從訂閱者鏈表中刪除退訂客戶端的信息。
    • 如果刪除退訂客戶端之后, 頻道的訂閱者鏈表變成了空鏈表, 那么說明這個頻道已經(jīng)沒有任何訂閱者了, 程序?qū)?pubsub_channels?字典中刪除頻道對應(yīng)的鍵。

    UNSUBSCRIBE?命令的實現(xiàn)可以用以下偽代碼來描述:

    def unsubscribe(*all_input_channels):# 遍歷要退訂的所有頻道for channel in all_input_channels:# 在訂閱者鏈表中刪除退訂的客戶端server.pubsub_channels[channel].remove(client)# 如果頻道已經(jīng)沒有任何訂閱者了(訂閱者鏈表為空)# 那么將頻道從字典中刪除if len(server.pubsub_channels[channel]) == 0:server.pubsub_channels.remove(channel)

    3.6.2模式的訂閱和退訂

    前面說過,服務(wù)器將所有頻道的訂閱關(guān)系保存起來,與此類似,服務(wù)器也將所有模式的訂閱關(guān)系存在了pubsub_Patterns屬性里。

    struct redisServer {// ...// 保存所有頻道的訂閱關(guān)系list *pubsub_patterns;// ...};

    pubsub_Patterns屬性是一個鏈表,每個結(jié)點是被訂閱的模式,節(jié)點內(nèi)記錄了模式,節(jié)點內(nèi)的client屬性記錄了訂閱模式的客戶端。

    typedef struct pubsubPattern{//訂閱模式的客戶端redisClient *client;//被訂閱的模式robj *pattern; }pubsubPattern;

    每當(dāng)客戶端執(zhí)行PSUBSCRIBE這個命令來訂閱某個或某些模式時,服務(wù)器會對每個被訂閱的模式執(zhí)行下面的操作:

    1)新建一個pubsubPattern結(jié)構(gòu),設(shè)置好兩個屬性

    2)將新節(jié)點加到pubsub_patterns尾部

    偽代碼實現(xiàn):

    def osubscribe(*all_input_patterns):#遍歷所有輸入的模式#記錄被訂閱的模式和對應(yīng)的客戶端pubsubPattern=create()pubsubPattern.client=clientpubsubPattern.pattern=pattern#插入鏈表末尾server.pub_patterns.append(pubsubPattern)

    模式退訂命令PUNSUBSCRIBE是PSUBSCRIBE的反操作

    服務(wù)器將找到并刪除那些被退訂的模式

    偽代碼如下:(我想吐槽一下這樣時間復(fù)雜度。。。沒有更好的辦法嗎?)

    def osubscribe(*all_input_patterns):#遍歷所有退訂的模式for pattern in all_input_patterns:#遍歷每一個節(jié)點for pubsubPattern in server.pubsub_patterns:#如果客戶端和模式都相同if client==pubsubPattern.client:if pattern==pubsubPattern.pattern:#刪除server.pub_patterns.remove(pubsubPattern)

    3.6.3、發(fā)送消息

    當(dāng)一個客戶端執(zhí)行PUBLISH<channel> <message>命令將消息發(fā)送給頻道時,服務(wù)器需要:

    1)把消息發(fā)送給所有本頻道的訂閱者

    具體做法就是去pubsub_channels字典找到本頻道的鏈表,也就是訂閱名單,然后發(fā)消息

    2)將消息發(fā)給,包含本頻道的所有模式中的所有訂閱者

    具體做法就是去pubsub_patterns查找包含本頻道的模式,并且把消息發(fā)送給訂閱它們的客戶端。

    3.6.4、查看訂閱信息

    redis2.8新增三個命令,用來查看頻道和模式的相關(guān)信息。

    PUBLISH CHANNELS[pattern]用于返回服務(wù)器當(dāng)前被訂閱的頻道,pattern可寫可不寫,不寫就查看所有,否則查看與pattern匹配的對應(yīng)頻道

    這個子命令是通過遍歷pubsub_channels字典實現(xiàn)的。

    PUBLISH NUMSUB[CHANNEL-1 CHANNEL-2.....]返回這些頻道的訂閱者數(shù)量

    這個子命令是通過遍歷pubsub_channels字典,查看對應(yīng)鏈表長度實現(xiàn)的。

    PUBLISH NUMPAT返回被訂閱模式數(shù)量

    這個子命令是通過返回pubsub_patterns的長度實現(xiàn)的。

    總而言之,PUBSUB?命令的三個子命令都是通過讀取?pubsub_channels?字典和?pubsub_patterns?鏈表中的信息來實現(xiàn)的。

    ?

    四、多機(jī)實現(xiàn)

    4.1、舊版復(fù)制

    Redis 的復(fù)制功能分為同步(sync)和命令傳播(command propagate)兩個操作:

    • 同步操作用于將從服務(wù)器的數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器當(dāng)前所處的數(shù)據(jù)庫狀態(tài)。
    • 命令傳播操作用于在主服務(wù)器的數(shù)據(jù)庫狀態(tài)被修改, 導(dǎo)致主從服務(wù)器的數(shù)據(jù)庫狀態(tài)出現(xiàn)不一致時, 讓主從服務(wù)器的數(shù)據(jù)庫重新回到一致狀態(tài)。

    同步

    當(dāng)客戶端向從服務(wù)器發(fā)送?SLAVEOF?命令, 要求從服務(wù)器復(fù)制主服務(wù)器時, 從服務(wù)器首先需要執(zhí)行同步操作, 也即是, 將從服務(wù)器的數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器當(dāng)前所處的數(shù)據(jù)庫狀態(tài)。

    從服務(wù)器對主服務(wù)器的同步操作需要通過向主服務(wù)器發(fā)送?SYNC?命令來完成, 以下是?SYNC?命令的執(zhí)行步驟:

  • 從服務(wù)器向主服務(wù)器發(fā)送?SYNC?命令。
  • 收到?SYNC?命令的主服務(wù)器執(zhí)行?BGSAVE?命令, 在后臺生成一個 RDB 文件, 并使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令。
  • 當(dāng)主服務(wù)器的?BGSAVE?命令執(zhí)行完畢時, 主服務(wù)器會將?BGSAVE?命令生成的 RDB 文件發(fā)送給從服務(wù)器, 從服務(wù)器接收并載入這個 RDB 文件, 將自己的數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器執(zhí)行?BGSAVE?命令時的數(shù)據(jù)庫狀態(tài)。
  • 主服務(wù)器將記錄在緩沖區(qū)里面的所有寫命令發(fā)送給從服務(wù)器, 從服務(wù)器執(zhí)行這些寫命令, 將自己的數(shù)據(jù)庫狀態(tài)更新至主服務(wù)器數(shù)據(jù)庫當(dāng)前所處的狀態(tài)。
  • 命令傳播

    在同步操作執(zhí)行完畢之后, 主從服務(wù)器兩者的數(shù)據(jù)庫將達(dá)到一致狀態(tài), 但這種一致并不是一成不變的 —— 每當(dāng)主服務(wù)器執(zhí)行客戶端發(fā)送的寫命令時, 主服務(wù)器的數(shù)據(jù)庫就有可能會被修改, 并導(dǎo)致主從服務(wù)器狀態(tài)不再一致。

    舉個例子, 假設(shè)一個主服務(wù)器和一個從服務(wù)器剛剛完成同步操作, 它們的數(shù)據(jù)庫都保存了相同的五個鍵?k1?至?k5

    如果這時, 客戶端向主服務(wù)器發(fā)送命令?DEL?k3?, 那么主服務(wù)器在執(zhí)行完這個?DEL?命令之后, 主從服務(wù)器的數(shù)據(jù)庫將出現(xiàn)不一致: 主服務(wù)器的數(shù)據(jù)庫已經(jīng)不再包含鍵?k3?, 但這個鍵卻仍然包含在從服務(wù)器的數(shù)據(jù)庫里面

    為了讓主從服務(wù)器再次回到一致狀態(tài), 主服務(wù)器需要對從服務(wù)器執(zhí)行命令傳播操作: 主服務(wù)器會將自己執(zhí)行的寫命令 —— 也即是造成主從服務(wù)器不一致的那條寫命令 —— 發(fā)送給從服務(wù)器執(zhí)行, 當(dāng)從服務(wù)器執(zhí)行了相同的寫命令之后, 主從服務(wù)器將再次回到一致狀態(tài)。

    缺陷

     其中可以明顯看出重新連接主服務(wù)器之后,SYNC命令創(chuàng)建包含k1-k10089的RDB文件。而事實上只需要再同步斷線后的k10087-k10089即可。SYNC的“全同步”對于從服務(wù)來說是不必要的。

    ? ? ? ? ? ?SYNC命令非常消耗資源,原因有三點:

        1)主服務(wù)器執(zhí)行BGSAVE命令生成RDB文件,這個生成過程會大量消耗主服務(wù)器資源(CPU、內(nèi)存和磁盤I/O資源)

        2)主服務(wù)器需要將自己生成的RBD文件發(fā)送給從從服務(wù)器,這個發(fā)送操作會消耗主從服務(wù)器大量的網(wǎng)絡(luò)資源(帶寬與流量)

        3)接收到RDB文件你的從服務(wù)器需要載入RDB文件,載入期間從服務(wù)器會因為阻塞而導(dǎo)致沒辦法處理命令請求。

    4.2新版復(fù)制

    sync雖然解決了數(shù)據(jù)同步問題,但是在數(shù)據(jù)量比較大情況下,從庫斷線從來依然采用全量復(fù)制機(jī)制,無論是從數(shù)據(jù)恢復(fù)、寬帶占用來說,sync所帶來的問題還是很多的。于是redis從2.8開始,引入新的命令psync。

    psync有兩種模式:完整重同步和部分重同步。

    部分重同步主要依賴三個方面來實現(xiàn),依次介紹。

    offset(復(fù)制偏移量):

    主庫和從庫分別各自維護(hù)一個復(fù)制偏移量(可以使用info replication查看),用于標(biāo)識自己復(fù)制的情況:

    在主庫中代表主節(jié)點向從節(jié)點傳遞的字節(jié)數(shù),在從庫中代表從庫同步的字節(jié)數(shù)。

    每當(dāng)主庫向從節(jié)點發(fā)送N個字節(jié)數(shù)據(jù)時,主節(jié)點的offset增加N

    從庫每收到主節(jié)點傳來的N個字節(jié)數(shù)據(jù)時,從庫的offset增加N。

    因此offset總是不斷增大,這也是判斷主從數(shù)據(jù)是否同步的標(biāo)志,若主從的offset相同則表示數(shù)據(jù)同步量,不通則表示數(shù)據(jù)不同步。

    replication backlog buffer(復(fù)制積壓緩沖區(qū)):

      復(fù)制積壓緩沖區(qū)是一個固定長度的FIFO隊列,大小由配置參數(shù)repl-backlog-size指定,默認(rèn)大小1MB。

    需要注意的是該緩沖區(qū)由master維護(hù)并且有且只有一個,所有slave共享此緩沖區(qū),其作用在于備份最近主庫發(fā)送給從庫的數(shù)據(jù)。

      在主從命令傳播階段,主節(jié)點除了將寫命令發(fā)送給從節(jié)點外,還會發(fā)送一份到復(fù)制積壓緩沖區(qū),作為寫命令的備份。

    ?

    除了存儲最近的寫命令,復(fù)制積壓緩沖區(qū)中還存儲了每個字節(jié)相應(yīng)的復(fù)制偏移量,由于復(fù)制積壓緩沖區(qū)固定大小先進(jìn)先出的隊列,所以它總是保存的是最近redis執(zhí)行的命令。

    所以,重連服務(wù)器后,從服務(wù)器會發(fā)送自己的復(fù)制偏移量offset給主服務(wù)器,

    如果offset偏移量之后的數(shù)據(jù)仍然存在于復(fù)制擠壓緩沖區(qū),就執(zhí)行部分重同步操作。

    相反,執(zhí)行完整重同步操作。

    run_id(服務(wù)器運(yùn)行的唯一ID)?

      每個redis實例在啟動時候,都會隨機(jī)生成一個長度為40的唯一字符串來標(biāo)識當(dāng)前運(yùn)行的redis節(jié)點,查看此id可通過命令info server查看。

      當(dāng)主從復(fù)制在初次復(fù)制時,主節(jié)點將自己的runid發(fā)送給從節(jié)點,從節(jié)點將這個runid保存起來,當(dāng)斷線重連時,從節(jié)點會將這個runid發(fā)送給主節(jié)點。主節(jié)點根據(jù)runid判斷能否進(jìn)行部分復(fù)制:

    • 如果從節(jié)點保存的runid與主節(jié)點現(xiàn)在的runid相同,說明主從節(jié)點之前同步過,主節(jié)點會更具offset偏移量之后的數(shù)據(jù)判斷是否執(zhí)行部分復(fù)制,如果offset偏移量之后的數(shù)據(jù)仍然都在復(fù)制積壓緩沖區(qū)里,則執(zhí)行部分復(fù)制,否則執(zhí)行全量復(fù)制;
    • 如果從節(jié)點保存的runid與主節(jié)點現(xiàn)在的runid不同,說明從節(jié)點在斷線前同步的redis節(jié)點并不是當(dāng)前的主節(jié)點,只能進(jìn)行全量復(fù)制;

    ?

    psync流程:

    復(fù)制

    客戶端向服務(wù)器端發(fā)送:SLAVEOF

    1、設(shè)置主服務(wù)器的地址和端口

    存到masterhost和mastterport兩個屬性里之后,向客戶端發(fā)送ok,然后開始復(fù)制工作。

    2、建立套接字鏈接

    從服務(wù)器根據(jù)命令設(shè)置的地址和端口,創(chuàng)建鏈接,并且為這個套接字創(chuàng)建一個專門處理復(fù)制工作的文件事件處理器。

    主服務(wù)器也會為套接字創(chuàng)建相應(yīng)的客戶端狀態(tài),并且把從服務(wù)器當(dāng)作一個客戶端來對待。

    3、發(fā)送ping命令(檢查)

    檢查套接字狀態(tài)是否正常

    檢查主服務(wù)器是否能正確處理請求。(如果不能,就重連)

    4、身份認(rèn)證

    ?

    5、發(fā)送端口信息

    從服務(wù)器向主服務(wù)器發(fā)送信息,主服務(wù)器記錄。

    6、同步

    從服務(wù)器向主服務(wù)器發(fā)送psync命令。(主服務(wù)器也成為從服務(wù)器的客戶端,因為主服務(wù)器會發(fā)送寫命令給從服務(wù)器)

    7、命令傳播

    完成同步后,進(jìn)入傳播階段,主服務(wù)器一直發(fā)送寫命令,從服務(wù)器一直接受,保證和主服務(wù)器一致。

    心跳檢測

    默認(rèn)一秒一次,從服務(wù)器向主服務(wù)器發(fā)送命令:REPLCONF ACK <offset>

    三個作用:

    檢測網(wǎng)絡(luò)連接狀態(tài):如果主服務(wù)器一秒沒收到命令,就說明出問題了

    輔助實現(xiàn)min-slaves配置:min-slaves-to-write 3? ?min-slaves-max-log 10:當(dāng)從服務(wù)器小于3個或延遲都大于10,主服務(wù)器拒絕寫命令。

    檢測命令丟失:如果命令丟失,主服務(wù)器會發(fā)現(xiàn)偏移量不一樣,然后它就會根據(jù)偏移量,去積壓緩沖區(qū)找到缺少的數(shù)據(jù)并發(fā)給從服務(wù)器。

    4.3、哨兵

    4.3.1什么是哨兵機(jī)制

    Redis的哨兵(sentinel)?系統(tǒng)用于管理/多個?Redis?服務(wù)器,該系統(tǒng)執(zhí)行以下三個任務(wù):

    ·????????監(jiān)控:?哨兵(sentinel)?會不斷地檢查你的Master和Slave是否運(yùn)作正常。

    ·????????提醒:當(dāng)被監(jiān)控的某個?Redis出現(xiàn)問題時,?哨兵(sentinel)?可以通過?API?向管理員或者其他應(yīng)用程序發(fā)送通知。

    ·????????自動故障遷移:當(dāng)一個Master不能正常工作時,哨兵(sentinel)?會開始一次自動故障遷移操作,它會將失效Master的其中一個Slave升級為新的Master,?并讓失效Master的其他Slave改為復(fù)制新的Master;?當(dāng)客戶端試圖連接失效的Master時,集群也會向客戶端返回新Master的地址,使得集群可以使用Master代替失效Master。

    例如下圖所示:

    在Server1 掉線后:

    升級Server2 為新的主服務(wù)器:

    4.3.2、哨兵模式修改配置

    實現(xiàn)步驟:

    1.拷貝到etc目錄

    cp sentinel.conf? /usr/local/redis/etc

    2.修改sentinel.conf配置文件

    sentinel monitor mymast? 192.168.110.133 6379 1? #主節(jié)點 名稱 IP 端口號 選舉次數(shù)

    sentinel auth-pass mymaster 123456?

    3. 修改心跳檢測 5000毫秒

    sentinel down-after-milliseconds mymaster 5000

    4.sentinel parallel-syncs mymaster 2 --- 做多多少合格節(jié)點

    5. 啟動哨兵模式

    ./redis-server /usr/local/redis/etc/sentinel.conf --sentinel &


    1)Sentinel(哨兵) 進(jìn)程是用于監(jiān)控 Redis 集群中 Master 主服務(wù)器工作的狀態(tài)

    2)在 Master 主服務(wù)器發(fā)生故障的時候,可以實現(xiàn) Master 和 Slave 服務(wù)器的切換,保證系統(tǒng)的高可用(High Availability)

    工作方式

    1)每個 Sentinel(哨兵)進(jìn)程以每秒鐘一次的頻率向整個集群中的 Master 主服務(wù)器,Slave 從服務(wù)器以及其他 Sentinel(哨兵)進(jìn)程發(fā)送一個 PING 命令。

    2. 如果一個實例(instance)距離最后一次有效回復(fù) PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 則這個實例會被 Sentinel(哨兵)進(jìn)程標(biāo)記為主觀下線。

    3. 如果一個 Master 主服務(wù)器被標(biāo)記為主觀下線,則正在監(jiān)視這個 Master 主服務(wù)器的所有 Sentinel(哨兵)進(jìn)程要以每秒一次的頻率確認(rèn) Master 主服務(wù)器的確進(jìn)入了主觀下線狀態(tài)。

    4. 當(dāng)有足夠數(shù)量的 Sentinel(哨兵)進(jìn)程(大于等于配置文件指定的值)在指定的時間范圍內(nèi)確認(rèn) Master 主服務(wù)器進(jìn)入了主觀下線狀態(tài), 則Master 主服務(wù)器會被標(biāo)記為客觀下線(ODOWN)。

    5. 在一般情況下, 每個 Sentinel(哨兵)進(jìn)程會以每 10 秒一次的頻率向集群中的所有Master 主服務(wù)器、Slave 從服務(wù)器發(fā)送 INFO 命令。

    6. 當(dāng) Master 主服務(wù)器被 Sentinel(哨兵)進(jìn)程標(biāo)記為客觀下線時,Sentinel(哨兵)進(jìn)程向下線的 Master 主服務(wù)器的所有 Slave 從服務(wù)器發(fā)送 INFO 命令的頻率會從 10 秒一次改為每秒一次。

    7. 若沒有足夠數(shù)量的 Sentinel(哨兵)進(jìn)程同意 Master 主服務(wù)器下線, Master 主服務(wù)器的客觀下線狀態(tài)就會被移除。若 Master 主服務(wù)器重新向 Sentinel(哨兵)進(jìn)程發(fā)送 PING 命令返回有效回復(fù),Master 主服務(wù)器的主觀下線狀態(tài)就會被移除。

    哨兵(sentinel)?的一些設(shè)計思路和zookeeper非常類似


    我們從啟動并初始化說起

    4.3.3啟動并初始化 Sentinel

    啟動一個 Sentinel 可以使用命令:

    $ redis-sentinel /path/to/your/sentinel.conf

    或者命令:

    $ redis-server /path/to/your/sentinel.conf --sentinel

    當(dāng)一個 Sentinel 啟動時, 它需要執(zhí)行以下步驟:

    初始化服務(wù)器。

    首先, 因為 Sentinel 本質(zhì)上只是一個運(yùn)行在特殊模式下的 Redis 服務(wù)器, 所以啟動 Sentinel 的第一步, 就是初始化一個普通的 Redis 服務(wù)器.

    不過, 因為 Sentinel 執(zhí)行的工作和普通 Redis 服務(wù)器執(zhí)行的工作不同, 所以 Sentinel 的初始化過程和普通 Redis 服務(wù)器的初始化過程并不完全相同。

    比如說, 普通服務(wù)器在初始化時會通過載入 RDB 文件或者 AOF 文件來還原數(shù)據(jù)庫狀態(tài), 但是因為 Sentinel 并不使用數(shù)據(jù)庫, 所以初始化 Sentinel 時就不會載入 RDB 文件或者 AOF 文件。

    將普通 Redis 服務(wù)器使用的代碼替換成 Sentinel 專用代碼。

    第二個步驟就是將一部分普通 Redis 服務(wù)器使用的代碼替換成 Sentinel 專用代碼。

    比如說, 普通 Redis 服務(wù)器使用?redis.h/REDIS_SERVERPORT?常量的值作為服務(wù)器端口:

    #define REDIS_SERVERPORT 6379

    而 Sentinel 則使用?sentinel.c/REDIS_SENTINEL_PORT?常量的值作為服務(wù)器端口:

    #define REDIS_SENTINEL_PORT 26379

    為什么在 Sentinel 模式下, Redis 服務(wù)器不能執(zhí)行諸如?SET?、?DBSIZE?、?EVAL?等等這些命令 —— 因為服務(wù)器根本沒有在命令表中載入這些命令。

    初始化 Sentinel 狀態(tài)。

    在應(yīng)用了 Sentinel 的專用代碼之后, 接下來, 服務(wù)器會初始化一個?sentinel.c/sentinelState?結(jié)構(gòu)(后面簡稱“Sentinel 狀態(tài)”), 這個結(jié)構(gòu)保存了服務(wù)器中所有和 Sentinel 功能有關(guān)的狀態(tài) (服務(wù)器的一般狀態(tài)仍然由?redis.h/redisServer?結(jié)構(gòu)保存):

    struct sentinelState {// 當(dāng)前紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移uint64_t current_epoch;// 保存了所有被這個 sentinel 監(jiān)視的主服務(wù)器// 字典的鍵是主服務(wù)器的名字// 字典的值則是一個指向 sentinelRedisInstance 結(jié)構(gòu)的指針dict *masters;// 是否進(jìn)入了 TILT 模式?int tilt;// 目前正在執(zhí)行的腳本的數(shù)量int running_scripts;// 進(jìn)入 TILT 模式的時間mstime_t tilt_start_time;// 最后一次執(zhí)行時間處理器的時間mstime_t previous_time;// 一個 FIFO 隊列,包含了所有需要執(zhí)行的用戶腳本list *scripts_queue;} sentinel;

    初始化 Sentinel 狀態(tài)的?masters?屬性

    Sentinel 狀態(tài)中的?masters?字典記錄了所有被 Sentinel 監(jiān)視的主服務(wù)器的相關(guān)信息:

    • 字典的鍵是被監(jiān)視主服務(wù)器的名字。
    • 而字典的值則是被監(jiān)視主服務(wù)器對應(yīng)的?sentinel.c/sentinelRedisInstance?結(jié)構(gòu)。

    每個?sentinelRedisInstance?結(jié)構(gòu)代表一個被 Sentinel 監(jiān)視的 Redis 服務(wù)器實例(instance), 這個實例可以是主服務(wù)器、從服務(wù)器、或者另外一個 Sentinel 。

    實例結(jié)構(gòu)包含的屬性非常多, 以下代碼展示了一部分屬性

    typedef struct sentinelRedisInstance {// 標(biāo)識值,記錄了實例的類型,以及該實例的當(dāng)前狀態(tài)int flags;// 實例的名字// 主服務(wù)器的名字由用戶在配置文件中設(shè)置// 從服務(wù)器以及 Sentinel 的名字由 Sentinel 自動設(shè)置// 格式為 ip:port ,例如 "127.0.0.1:26379"char *name;// 實例的運(yùn)行 IDchar *runid;// 配置紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移uint64_t config_epoch;// 實例的地址sentinelAddr *addr;// SENTINEL down-after-milliseconds 選項設(shè)定的值// 實例無響應(yīng)多少毫秒之后才會被判斷為主觀下線(subjectively down)mstime_t down_after_period;// SENTINEL monitor <master-name> <IP> <port> <quorum> 選項中的 quorum 參數(shù)// 判斷這個實例為客觀下線(objectively down)所需的支持投票數(shù)量int quorum;// SENTINEL parallel-syncs <master-name> <number> 選項的值// 在執(zhí)行故障轉(zhuǎn)移操作時,可以同時對新的主服務(wù)器進(jìn)行同步的從服務(wù)器數(shù)量int parallel_syncs;// SENTINEL failover-timeout <master-name> <ms> 選項的值// 刷新故障遷移狀態(tài)的最大時限mstime_t failover_timeout;// ...} sentinelRedisInstance;

    創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接。

    ?Sentinel 將成為主服務(wù)器的客戶端, 它可以向主服務(wù)器發(fā)送命令, 并從命令回復(fù)中獲取相關(guān)的信息。

    對于每個被 Sentinel 監(jiān)視的主服務(wù)器來說, Sentinel 會創(chuàng)建兩個連向主服務(wù)器的異步網(wǎng)絡(luò)連接:

    • 一個是命令連接, 這個連接專門用于向主服務(wù)器發(fā)送命令, 并接收命令回復(fù)。
    • 另一個是訂閱連接, 這個連接專門用于訂閱主服務(wù)器的?__sentinel__:hello?頻道。
    為什么有兩個連接?在 Redis 目前的發(fā)布與訂閱功能中, 被發(fā)送的信息都不會保存在Redis 服務(wù)器里面, 如果在信息發(fā)送時, 想要接收信息的客戶 端不在線或者斷線, 那么這個客戶端就會丟失這條信息。因此, 為了不丟失 __sentinel__:hello 頻道的任何信息, Sentinel 必須專門用一個訂閱連接來接收該頻道的信息。而另一方面, 除了訂閱頻道之外, Sentinel 還又必須向主服務(wù) 器發(fā)送命令, 以此來與主服務(wù)器進(jìn)行通訊, 所以 Sentinel 還 必須向主服務(wù)器創(chuàng)建命令連接。并且因為 Sentinel 需要與多個實例創(chuàng)建多個網(wǎng)絡(luò)連接, 所以Sentinel 使用的是異步連接。

    接下來介紹 Sentinel 如何通過命令連接和訂閱連接與被監(jiān)視主服務(wù)器進(jìn)行通訊。

    4.3.4、獲取服務(wù)器信息

    sentinel默認(rèn)每十秒鐘發(fā)送一次INFO命令給主服務(wù)器,并獲取信息:

    1)關(guān)于主服務(wù)器本身的信息

    2)主服務(wù)器屬下所有從服務(wù)器信息

    sentinel發(fā)現(xiàn)主服務(wù)器有新的從服務(wù)器時,會創(chuàng)建相應(yīng)的實例結(jié)構(gòu)和命令連接,訂閱連接

    4.3.5、給服務(wù)器發(fā)送消息

    4.3.6、主觀下線

    指的是單個Sentinel實例對服務(wù)器做出的下線判斷,即單個sentinel認(rèn)為某個服務(wù)下線(有可能是接收不到訂閱,之間的網(wǎng)絡(luò)不通等等原因)。
    如果服務(wù)器在down-after-milliseconds給定的毫秒數(shù)之內(nèi), 沒有返回 Sentinel 發(fā)送的 PING 命令的回復(fù), 或者返回一個錯誤, 那么 Sentinel 將這個服務(wù)器標(biāo)記為主觀下線(SDOWN )。
    sentinel會以每秒一次的頻率向所有與其建立了命令連接的實例(master,從服務(wù),其他sentinel)發(fā)ping命令,通過判斷ping回復(fù)是有效回復(fù),還是無效回復(fù)來判斷實例時候在線(對該sentinel來說是“主觀在線”)。
    sentinel配置文件中的down-after-milliseconds設(shè)置了判斷主觀下線的時間長度,如果實例在down-after-milliseconds毫秒內(nèi),返回的都是無效回復(fù),那么sentinel回認(rèn)為該實例已(主觀)下線,修改其flags狀態(tài)為SRI_S_DOWN。如果多個sentinel監(jiān)視一個服務(wù),有可能存在多個sentinel的down-after-milliseconds配置不同,這個在實際生產(chǎn)中要注意。

    4.3.7、客觀下線


    客觀下線(Objectively Down, 簡稱 ODOWN)指的是多個 Sentinel 實例在對同一個服務(wù)器做出 SDOWN 判斷, 并且通過 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服務(wù)器下線判斷,然后開啟failover。
    客觀下線就是說只有在足夠數(shù)量的 Sentinel 都將一個服務(wù)器標(biāo)記為主觀下線之后, 服務(wù)器才會被標(biāo)記為客觀下線(ODOWN)。
    只有當(dāng)master被認(rèn)定為客觀下線時,才會發(fā)生故障遷移。
    當(dāng)sentinel監(jiān)視的某個服務(wù)主觀下線后,sentinel會詢問其它監(jiān)視該服務(wù)的sentinel,看它們是否也認(rèn)為該服務(wù)主觀下線,接收到足夠數(shù)量(這個值可以配置)的sentinel判斷為主觀下線,既任務(wù)該服務(wù)客觀下線,并對其做故障轉(zhuǎn)移操作。
    sentinel通過發(fā)送 SENTINEL is-master-down-by-addr ip port current_epoch runid

    (ip:主觀下線的服務(wù)id,port:主觀下線的服務(wù)端口,current_epoch:sentinel的紀(jì)元,runid:*表示檢測服務(wù)下線狀態(tài),如果是sentinel 運(yùn)行id,表示用來選舉領(lǐng)頭sentinel)

    來詢問其它sentinel是否同意服務(wù)下線。
    一個sentinel接收另一個sentinel發(fā)來的is-master-down-by-addr后,提取參數(shù),根據(jù)ip和端口,檢測該服務(wù)時候在該sentinel主觀下線,并且回復(fù)is-master-down-by-addr,回復(fù)包含三個參數(shù):down_state(1表示已下線,0表示未下線),leader_runid(領(lǐng)頭sentinal id),leader_epoch(領(lǐng)頭sentinel紀(jì)元)。
    sentinel接收到回復(fù)后,根據(jù)配置設(shè)置的下線最小數(shù)量,達(dá)到這個值,既認(rèn)為該服務(wù)客觀下線。
    客觀下線條件只適用于主服務(wù)器: 對于任何其他類型的 Redis 實例, Sentinel 在將它們判斷為下線前不需要進(jìn)行協(xié)商, 所以從服務(wù)器或者其他 Sentinel 永遠(yuǎn)不會達(dá)到客觀下線條件。只要一個 Sentinel 發(fā)現(xiàn)某個主服務(wù)器進(jìn)入了客觀下線狀態(tài), 這個 Sentinel 就可能會被其他 Sentinel 推選出, 并對失效的主服務(wù)器執(zhí)行自動故障遷移操作。

    4.3.8、選舉大哥sentinel

    一個redis服務(wù)被判斷為客觀下線時,多個監(jiān)視該服務(wù)的sentinel協(xié)商,選舉一個領(lǐng)頭sentinel,對該redis服務(wù)進(jìn)行故障轉(zhuǎn)移操作。選舉領(lǐng)頭sentinel遵循以下規(guī)則:
    1)所有的sentinel都有公平被選舉成領(lǐng)頭的資格。
    2)所有的sentinel都只有一次將某個sentinel選舉成領(lǐng)頭的機(jī)會(在一輪選舉中),一旦選舉,不能更改。
    3)先到先得,一旦當(dāng)前sentinel設(shè)置了領(lǐng)頭sentinel,以后要求設(shè)置sentinel為領(lǐng)頭請求都會被拒絕。
    4)每個發(fā)現(xiàn)服務(wù)客觀下線的sentinel,都會要求其他sentinel將自己設(shè)置成領(lǐng)頭。
    5)當(dāng)一個sentinel(源sentinel)向另一個sentinel(目sentinel)發(fā)送is-master-down-by-addr ip port current_epoch runid命令的時候,runid參數(shù)不是*,而是sentinel運(yùn)行id,就表示源sentinel要求目標(biāo)sentinel選舉其為領(lǐng)頭。
    6)源sentinel會檢查目標(biāo)sentinel對其要求設(shè)置成領(lǐng)頭的回復(fù),如果回復(fù)的leader_runid和leader_epoch為源sentinel,表示目標(biāo)sentinel同意將源sentinel設(shè)置成領(lǐng)頭。
    7)如果某個sentinel被半數(shù)以上的sentinel設(shè)置成領(lǐng)頭,那么該sentinel既為領(lǐng)頭。
    8)如果在限定時間內(nèi),沒有選舉出領(lǐng)頭sentinel,暫定一段時間,再選舉。

    為什么要選?
    簡單來說,就是因為只能有一個sentinel節(jié)點去完成故障轉(zhuǎn)移。
    sentinel is-master-down-by-addr這個命令有兩個作用,一是確認(rèn)下線判定,二是進(jìn)行領(lǐng)導(dǎo)者選舉。
    過程:
    1)每個做主觀下線的sentinel節(jié)點向其他sentinel節(jié)點發(fā)送上面那條命令,要求將它設(shè)置為領(lǐng)導(dǎo)者。
    2)收到命令的sentinel節(jié)點如果還沒有同意過其他的sentinel發(fā)送的命令(還未投過票),那么就會同意,否則拒絕。
    3)如果該sentinel節(jié)點發(fā)現(xiàn)自己的票數(shù)已經(jīng)過半且達(dá)到了quorum的值,就會成為領(lǐng)導(dǎo)者
    4)如果這個過程出現(xiàn)多個sentinel成為領(lǐng)導(dǎo)者,則會等待一段時間重新選舉。

    4.3.9、轉(zhuǎn)移

    1)挑一個新的主服務(wù)器

    2)把其它從服務(wù)器的主服務(wù)器改成新的

    3)把之前的主服務(wù)器改為新主服務(wù)器的從服務(wù)器

    4.3.10、怎么挑新的主服務(wù)器

    1)刪除所有下線服務(wù)器

    2)刪除五秒內(nèi)沒回復(fù)INOF命令的服務(wù)器

    3)刪除數(shù)據(jù)舊的服務(wù)器(連接斷開超過down-after-millseconds*10)

    4)根據(jù)優(yōu)先級,選出最高的。

    4.3.11、重點提煉

    ?

    • Sentinel 是一個特殊模式下的 Redis 服務(wù)器, 它使用了不同的命令表, 所以 Sentinel 能使用的命令和普通服務(wù)器不同。
    • Sentinel 會讀入用戶指定的配置文件, 為每個要被監(jiān)視的主服務(wù)器創(chuàng)建相應(yīng)的實例結(jié)構(gòu), 并創(chuàng)建連向主服務(wù)器的命令連接和訂閱連接, 其中命令連接用于向主服務(wù)器發(fā)送命令請求, 而訂閱連接則用于接收指定頻道的消息。
    • Sentinel 向主服務(wù)器發(fā)送?INFO?命令獲得屬下從服務(wù)器信息, 為這些從服務(wù)器創(chuàng)建實例結(jié)構(gòu)、命令連接和訂閱連接。
    • 默認(rèn) Sentinel 十秒一次向被監(jiān)視的主服務(wù)器和從服務(wù)器發(fā)送?INFO?命令, 當(dāng)主服務(wù)器處于下線狀態(tài), 或者 Sentinel 正在對主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作時, Sentinel 向從服務(wù)器發(fā)送?INFO?命令的頻率會改為每秒一次。
    • 對于監(jiān)視同一個主服務(wù)器和從服務(wù)器的多個 Sentinel 來說, 它們會以每兩秒一次的頻率, 通過向被監(jiān)視服務(wù)器的?__sentinel__:hello?頻道發(fā)送消息來向其他 Sentinel 宣告自己的存在。
    • 每個 Sentinel 也會從?__sentinel__:hello?頻道中接收其他 Sentinel 發(fā)來的信息, 并根據(jù)這些信息為其他 Sentinel 創(chuàng)建相應(yīng)的實例結(jié)構(gòu), 以及命令連接。
    • Sentinel 只會與主服務(wù)器和從服務(wù)器創(chuàng)建命令連接和訂閱連接, Sentinel 與 Sentinel 之間則只創(chuàng)建命令連接。
    • Sentinel 以每秒一次的頻率向?qū)嵗?#xff08;包括主服務(wù)器、從服務(wù)器、其他 Sentinel)發(fā)送?PING?命令, 并根據(jù)實例對?PING?命令的回復(fù)來判斷實例是否在線
    • 當(dāng) Sentinel 將一個主服務(wù)器判斷為主觀下線時, 它會向同樣監(jiān)視這個主服務(wù)器的其他 Sentinel 進(jìn)行詢問, 看它們是否同意這個主服務(wù)器已經(jīng)進(jìn)入主觀下線狀態(tài)。
    • 當(dāng) Sentinel 收集到足夠多的主觀下線投票之后, 它會將主服務(wù)器判斷為客觀下線, 并發(fā)起一次針對主服務(wù)器的故障轉(zhuǎn)移操作。

    ?


    《三天給你聊清楚redis》第3天說說redis大概怎么用,和面試題(18000字)

    五、實戰(zhàn)

    5.1基礎(chǔ)實戰(zhàn)

    ?

    5.1.1實戰(zhàn)點贊

    點贊功能隨處可見,我們都知道點贊是一個非常高頻的操作,redis就非常適合做這種工作。

    實現(xiàn)效果:

    分析:三種類型:給帖子點贊,給評論點贊,給回復(fù)點贊

    我們只實現(xiàn)查看點贊數(shù)量的話,只要一個int記錄一下就可以,但是我們之后還想查看點贊的人,所以要把每一個點贊的信息都記錄好,方便后面的功能繼續(xù)做出來。

    思路:

    點贊:把點贊的信息放進(jìn)去。

    取消:把點贊的信息刪除。

    在此之前,我們要封裝一個get到key的類,方便后面復(fù)用。

    package com.now.community.community.util;public class RedisKeyUtil {private static final String SPLIT = ":";private static final String PREFIX_ENTITY_LIKE = "like:entity";private static final String PREFIX_USER_LIKE = "like:user";// 某個實體的贊// like:entity:entityType:entityId -> set(userId)public static String getEntityLikeKey(int entityType, int entityId) {return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;}// 某個用戶的贊// like:user:userId -> intpublic static String getUserLikeKey(int userId) {return PREFIX_USER_LIKE + SPLIT + userId;} }

    點贊業(yè)務(wù):

    // 點贊public void like(int userId, int entityType, int entityId, int entityUserId) {redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);operations.multi();if (isMember) {operations.opsForSet().remove(entityLikeKey, userId);} else {operations.opsForSet().add(entityLikeKey, userId);}return operations.exec();}});}

    我們要查找是否點贊,還有點贊數(shù)量,方便頁面顯示:

    // 查詢某實體點贊的數(shù)量public long findEntityLikeCount(int entityType, int entityId) {String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().size(entityLikeKey);}// 查詢某人對某實體的點贊狀態(tài)public int findEntityLikeStatus(int userId, int entityType, int entityId) {String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;}

    點贊LikeController

    @RequestMapping(path = "/like", method = RequestMethod.POST)@ResponseBodypublic String like(int entityType, int entityId,int entityUserId,int postId) {User user = hostHolder.getUser();// 點贊likeService.like(user.getId(), entityType, entityId,entityUserId);// 數(shù)量long likeCount = likeService.findEntityLikeCount(entityType, entityId);// 狀態(tài)int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);// 返回的結(jié)果Map<String, Object> map = new HashMap<>();map.put("likeCount", likeCount);map.put("likeStatus", likeStatus);return CommunityUtil.getJSONString(0, null, map);}

    ?

    ?5.1.2實戰(zhàn)關(guān)注

    效果:?

    思路:很好想,把自己的粉絲和自己關(guān)注的人都存起來(set即可),做增刪改查。

    package com.now.community.community.service;import com.now.community.community.entity.User; import com.now.community.community.util.CommunityConstant; import com.now.community.community.util.RedisKeyUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SessionCallback; import org.springframework.stereotype.Service;import java.util.*;@Service public class FollowService implements CommunityConstant {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate UserService userService;public void follow(int userId, int entityType, int entityId) {redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);operations.multi();operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());return operations.exec();}});}public void unfollow(int userId, int entityType, int entityId) {redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);operations.multi();operations.opsForZSet().remove(followeeKey, entityId);operations.opsForZSet().remove(followerKey, userId);return operations.exec();}});}// 查詢關(guān)注的實體的數(shù)量public long findFolloweeCount(int userId, int entityType) {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);return redisTemplate.opsForZSet().zCard(followeeKey);}// 查詢實體的粉絲的數(shù)量public long findFollowerCount(int entityType, int entityId) {String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);return redisTemplate.opsForZSet().zCard(followerKey);}// 查詢當(dāng)前用戶是否已關(guān)注該實體public boolean hasFollowed(int userId, int entityType, int entityId) {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;}// 查詢某用戶關(guān)注的人public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);if (targetIds == null) {return null;}List<Map<String, Object>> list = new ArrayList<>();for (Integer targetId : targetIds) {Map<String, Object> map = new HashMap<>();User user = userService.findUserById(targetId);map.put("user", user);Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);map.put("followTime", new Date(score.longValue()));list.add(map);}return list;}// 查詢某用戶的粉絲public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);if (targetIds == null) {return null;}List<Map<String, Object>> list = new ArrayList<>();for (Integer targetId : targetIds) {Map<String, Object> map = new HashMap<>();User user = userService.findUserById(targetId);map.put("user", user);Double score = redisTemplate.opsForZSet().score(followerKey, targetId);map.put("followTime", new Date(score.longValue()));list.add(map);}return list;} }

    ??5.1.3實戰(zhàn)統(tǒng)計訪問量

    過于簡單不解釋

    ?

    ??5.1.4實戰(zhàn)排行榜

    ?

    const REDIS_TB_NAME='user:actId'; //表名 const REDIS_SEP=":"; //命名分隔符 const REDIS_FIELDS="username|regtime"; //表字段名稱 const REDIS_FIELD_RANK="rank"; //排行 const REDIS_FIELD_ID="id"; //表的自增ID //插入排行榜數(shù)據(jù) for($i=0;$i<RANK_REC_NUM;$i++) //填充數(shù)據(jù) {$redis_increase_id=$redis->get(REDIS_TB_NAME.REDIS_SEP.REDIS_FIELD_ID); //事務(wù)機(jī)制,插入用戶信息及排行信息,自增id$ret=$redis->multi() //開始事務(wù)->hMset(REDIS_TB_NAME.REDIS_SEP.$redis_increase_id,array($fields[0]=>"username".$redis_increase_id, $fields[1]=>(time()+intval(rand(0,1000))))) //username 用戶名 //regtime 注冊時間->Zadd(REDIS_TB_NAME.REDIS_SEP.REDIS_FIELD_RANK,intval(rand(1,100)),$redis_increase_id) //插入排行->incr(REDIS_TB_NAME.REDIS_SEP.REDIS_FIELD_ID) //自增id->exec(); //執(zhí)行事務(wù)if($ret==false) //插入失敗,重新插入{$i--;} } echo "插入".$i."條記錄成功<br>"; <table> <thead> <tr style="font-size:bold;color:red"><td>名次</td><td>分?jǐn)?shù)</td><td>姓名</td><td>注冊時間</td></tr> </thead> <tbody> <?phpconst REDIS_FIELDS="username|regtime"; //表字段名稱$fields=explode('|',REDIS_FIELDS);foreach($rank as $k=>$v){//echo REDIS_TB_NAME.REDIS_SEP.$k.REDIS_SEP.$fields[0];echo "<tr><td>$i</td><td>$v</td><td>".$redis->hget(REDIS_TB_NAME.REDIS_SEP.$k,$fields[0])."</td><td>".date("Y-m-d H:i:s",$redis->hget(REDIS_TB_NAME.REDIS_SEP.$k,$fields[1]))."</td></tr>";$i++;} } ?> </tbody> </table>

    Redis本身支持一些簡單的組合型的命令,比如以NX結(jié)尾命令都是判斷在這個值沒有時才進(jìn)行某個命令

    ????????Redis支持自定義的命令組合,通過MULTI和EXEC,將幾個命令組合起來執(zhí)行

    ????????如:插入排行數(shù)據(jù)和用戶信息,并自增id

    $redis->multi() ->hmset("user:1",array("username"=>"hirryli","regtime"=>1234123483)) ->Zadd("user:rank",$scores,$userId) ->incr("user:id") ->exec();

    ?

    5.2實戰(zhàn)優(yōu)化小項目

    這是我們之前項目的業(yè)務(wù)流程,做一下簡單介紹。

    登錄:

    用戶輸入賬號、密碼、驗證碼。我們先判斷用戶輸入的驗證碼是不是我們session存的驗證碼,然后去查賬號密碼是否正確。

    如果登錄成功,發(fā)送給用戶一張憑證(ticket)。

    登錄后

    之后的每次請求,用戶攜帶ticket,服務(wù)器得到后,根據(jù)ticket去login_ticket表中查找登錄信息,并且根據(jù)登錄信息再查user表獲得更多的用戶信息。

    使用Redis存儲驗證碼


    - 驗證碼需要頻繁的訪問與刷新,對性能要求較高。
    - 驗證碼不需永久保存,通常在很短的時間后就會失效。
    - 分布式部署時,存在Session共享的問題。

    我們重構(gòu)思路:進(jìn)入登錄頁面會訪問驗證碼方法,此方法會自動生成一個驗證碼和圖片,將驗證碼和圖片輸出給瀏覽器,并且下發(fā)一個cookies,這個cookies里面存的是一段隨機(jī)數(shù),這段隨機(jī)數(shù)作為key存在redis里面(之前是存session),value就是驗證碼,并設(shè)置一個過期時間;

    //驗證碼@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {// 生成驗證碼String text = kaptchaProducer.createText();BufferedImage image = kaptchaProducer.createImage(text);// 將驗證碼存入session//session.setAttribute("kaptcha", text);//驗證碼的歸屬String owner= CommunityUtil.generateUUID();Cookie cookie=new Cookie("kaptchaOwner",owner);cookie.setMaxAge(60);cookie.setPath(contextPath);response.addCookie(cookie);//存入redisString redisKey= RedisKeyUtil.getKaptchaKey(owner);redisTemplate.opsForValue().set(redisKey,text,60, TimeUnit.SECONDS);// 將圖片輸出給瀏覽器response.setContentType("image/png");try {OutputStream os = response.getOutputStream();ImageIO.write(image, "png", os);} catch (IOException e) {logger.error("響應(yīng)驗證碼失敗:" + e.getMessage());}} @RequestMapping(path = "/login",method = RequestMethod.POST)public String login(String username,String password,String code,boolean rememberme,Model model,/*HttpSession session,*/HttpServletResponse response,@CookieValue("kaptchaOwner") String kaptchaOwner){// 檢查驗證碼//String kaptcha = (String) session.getAttribute("kaptcha");String kaptcha=null;if(StringUtils.isNotBlank(kaptchaOwner)){String redisKey=RedisKeyUtil.getKaptchaKey(kaptchaOwner);kaptcha=(String) redisTemplate.opsForValue().get(redisKey);}if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){model.addAttribute("codeMsg", "驗證碼不正確!");return "/site/login";}// 檢查賬號,密碼int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;Map<String, Object> map = userService.login(username, password, expiredSeconds);if (map.containsKey("ticket")) {Cookie cookie = new Cookie("ticket", map.get("ticket").toString());cookie.setPath(contextPath);cookie.setMaxAge(expiredSeconds);response.addCookie(cookie);return "redirect:/index";} else {...}}


    使用Redis存儲登錄憑證


    - 處理每次請求時,都要查詢用戶的登錄憑證,訪問的頻率非常高。

    登錄時不存MySQL里,存redis里

    public Map<String,Object> login(String username,String password,int expiredSeconds){Map<String,Object> map=new HashMap<>();// 生成登錄憑證LoginTicket loginTicket = new LoginTicket();loginTicket.setUserId(user.getId());loginTicket.setTicket(CommunityUtil.generateUUID());loginTicket.setStatus(0);loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));//loginTicketMapper.insertLoginTicket(loginTicket);String redisKey= RedisKeyUtil.getTicketKey(loginTicket.getTicket());redisTemplate.opsForValue().set(redisKey,loginTicket);...}

    查找

    退出時也是改redis?

    public void logout(String ticket) {//loginTicketMapper.updateStatus(ticket, 1);String redisKey= RedisKeyUtil.getTicketKey(ticket);LoginTicket loginTicket=(LoginTicket) redisTemplate.opsForValue().get(redisKey);loginTicket.setStatus(1);redisTemplate.opsForValue().set(redisKey,loginTicket);}


    使用Redis緩存用戶信息


    - 處理每次請求時,都要根據(jù)憑證查詢用戶信息,訪問的頻率非常高。

    緩存用戶信息:因為會經(jīng)常根據(jù)userid來查詢user對象,所以使用redis來緩存提高服務(wù)器性能。使用redis的String類型,存入user對象,會自動將整個對象轉(zhuǎn)換成json字符串,同時設(shè)置過期時間;

    取值:優(yōu)先從redis中取,取不到的時候從mysql中取,并將數(shù)據(jù)初始化到redis中

    更新:更新的時候先更新mysql中的值,然后清除緩存數(shù)據(jù);

    // 1.優(yōu)先從緩存中取值private User getCache(int userId) {String redisKey = RedisKeyUtil.getUserKey(userId);return (User) redisTemplate.opsForValue().get(redisKey);}// 2.取不到時初始化緩存數(shù)據(jù)private User initCache(int userId) {User user = userMapper.selectById(userId);String redisKey = RedisKeyUtil.getUserKey(userId);redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);return user;}// 3.數(shù)據(jù)變更時清除緩存數(shù)據(jù)private void clearCache(int userId) {String redisKey = RedisKeyUtil.getUserKey(userId);redisTemplate.delete(redisKey);} public User findUserById(int id) { // return userMapper.selectById(id);User user = getCache(id);if (user == null) {user = initCache(id);}return user;} public int updateHeader(int userId, String headerUrl) {//return userMapper.updateHeader(userId, headerUrl);int rows=userMapper.updateHeader(userId, headerUrl);clearCache(userId);return rows;}

    ?

    ?5.3討論一下為啥用redis解決會話?

    什么是會話?

    ??會話可簡單理解為:用戶開一個瀏覽器,點擊多個超鏈接,訪問服務(wù)器多個web資源,然后關(guān)閉瀏覽器,整個過程稱之為一個會話。

    ?會話過程中要解決的一些問題?

    –每個用戶不可避免各自會產(chǎn)生一些數(shù)據(jù),程序要想辦法為每個用戶保存這些數(shù)據(jù)。

    –例如:用戶點擊超鏈接通過一個servlet購買了一個商品,程序應(yīng)該想辦法保存用戶購買的商品,以便于用戶點結(jié)帳servlet時,結(jié)帳servlet可以得到用戶購買的商品為用戶結(jié)帳。

    ?Cookie

    –Cookie是客戶端技術(shù),程序把每個用戶的數(shù)據(jù)以cookie的形式寫給用戶各自的瀏覽器。當(dāng)用戶使用瀏覽器再去訪問服務(wù)器中的web資源時,就會帶著各自的數(shù)據(jù)去。這樣,web資源處理的就是用戶各自的數(shù)據(jù)了。

    ?HttpSession

    –Session是服務(wù)器端技術(shù),利用這個技術(shù),服務(wù)器在運(yùn)行時可以為每一個用戶的瀏覽器創(chuàng)建一個其獨(dú)享的HttpSession對象,由于session為用戶瀏覽器獨(dú)享,所以用戶在訪問服務(wù)器的web資源時,可以把各自的數(shù)據(jù)放在各自的session中,當(dāng)用戶再去訪問服務(wù)器中的其它web資源時,其它web資源再從用戶各自的session中取出數(shù)據(jù)為用戶服務(wù)。

    總結(jié):cookie存在客戶端,session存在服務(wù)器端

    ?通常結(jié)合使用。

    ?

    我們先用sprintboot演示一下cookie和session操作

    @RequestMapping(path = "/cookie/set",method = RequestMethod.GET)@ResponseBodypublic String setCookie(HttpServletResponse httpServletResponse){Cookie cookie=new Cookie("code", CommunityUtil.generateUUID());cookie.setPath("/community/alpha");cookie.setMaxAge(60*10);httpServletResponse.addCookie(cookie);return "set cookie";}@RequestMapping(path = "/cookie/get",method = RequestMethod.GET)@ResponseBodypublic String getCookie(@CookieValue("code") String code){System.out.println(code);return "get cookie";}@RequestMapping(path = "/session/set", method = RequestMethod.GET)@ResponseBodypublic String setSession(HttpSession session){session.setAttribute("id",1);session.setAttribute("name","Test");return "set session";}@RequestMapping(path = "/session/get", method = RequestMethod.GET)@ResponseBodypublic String getSession(HttpSession session) {System.out.println(session.getAttribute("id"));System.out.println(session.getAttribute("name"));return "get session";}

    隨著服務(wù)器要處理的請求越來越多,我們不得不分布式部署,減小服務(wù)器壓力。

    為了負(fù)載均衡,我們一般采用nginx來分發(fā)請求給各個服務(wù)器處理

    但是這樣session是無法共享的。

    (粘性session)

    你可以設(shè)置nginx的分配策略,下次同一個還讓同一個服務(wù)器來處理

    但是很顯然,這就和分布式和nginx初衷違背了:負(fù)載很難保證均衡。

    (同步session)

    一臺服務(wù)器的session給所有服務(wù)器復(fù)制一份

    第一,性能不好。第二,產(chǎn)生了一定的耦合

    (專門session)

    專門一臺服務(wù)器來解決,存session,其它服務(wù)器來這個服務(wù)器取session再用。

    但是也有問題:你這個服務(wù)器掛了怎么辦?別的服務(wù)器都是依賴這個服務(wù)器工作的。我們分布式部署本來就是為了解決性能的瓶頸啊。

    很容易想到,我們把那個處理session的服務(wù)器搞個集群:

    更不行,想想就知道,本來就是為了解決分布式部署的問題,你把單獨(dú)解決session的服務(wù)器又搞集群,和之前有什么區(qū)別呢?還不如一個服務(wù)器存一份簡單呢。

    (存數(shù)據(jù)庫)

    可以,但是傳統(tǒng)的關(guān)系數(shù)據(jù)庫是存到硬盤里,速度太慢。

    (nosql)

    最終,我們的主流辦法使用nosql數(shù)據(jù)庫,比如redis,來解決這個問題的,如果有不同意見,歡迎討論。

    ?5.4插曲:RedLock小專欄

    概念

    Redis 官方站這篇文章提出了一種權(quán)威的基于 Redis 實現(xiàn)分布式鎖的方式名叫?Redlock,此種方式比原先的單節(jié)點的方法更安全。它可以保證以下特性:

  • 安全特性:互斥訪問,即永遠(yuǎn)只有一個 client 能拿到鎖
  • 避免死鎖:最終 client 都可能拿到鎖,不會出現(xiàn)死鎖的情況,即使原本鎖住某資源的 client crash 了或者出現(xiàn)了網(wǎng)絡(luò)分區(qū)
  • 容錯性:只要大部分 Redis 節(jié)點存活就可以正常提供服務(wù)
  • 單節(jié)點實現(xiàn)

    SET resource_name my_random_value NX PX 30000

    主要依靠上述命令,該命令僅當(dāng) Key 不存在時(NX保證)set 值,并且設(shè)置過期時間 3000ms (PX保證),值 my_random_value 必須是所有 client 和所有鎖請求發(fā)生期間唯一的,釋放鎖的邏輯是:

    if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1]) elsereturn 0 end

    上述實現(xiàn)可以避免釋放另一個client創(chuàng)建的鎖,如果只有 del 命令的話,那么如果 client1 拿到 lock1 之后因為某些操作阻塞了很長時間,此時 Redis 端 lock1 已經(jīng)過期了并且已經(jīng)被重新分配給了 client2,那么 client1 此時再去釋放這把鎖就會造成 client2 原本獲取到的鎖被 client1 無故釋放了,但現(xiàn)在為每個 client 分配一個 unique 的 string 值可以避免這個問題。至于如何去生成這個 unique string,方法很多隨意選擇一種就行了。

    redlock算法

    算法很易懂,起 5 個 master 節(jié)點,分布在不同的機(jī)房盡量保證可用性。為了獲得鎖,client 會進(jìn)行如下操作:

  • 得到當(dāng)前的時間,微秒單位
  • 嘗試順序地在 5 個實例上申請鎖,當(dāng)然需要使用相同的 key 和 random value,這里一個 client 需要合理設(shè)置與 master 節(jié)點溝通的 timeout 大小,避免長時間和一個 fail 了的節(jié)點浪費(fèi)時間
  • 當(dāng) client 在大于等于 3 個 master 上成功申請到鎖的時候,且它會計算申請鎖消耗了多少時間,這部分消耗的時間采用獲得鎖的當(dāng)下時間減去第一步獲得的時間戳得到,如果鎖的持續(xù)時長(lock validity time)比流逝的時間多的話,那么鎖就真正獲取到了。
  • 如果鎖申請到了,那么鎖真正的 lock validity time 應(yīng)該是 origin(lock validity time) - 申請鎖期間流逝的時間
  • 如果 client 申請鎖失敗了,那么它就會在少部分申請成功鎖的 master 節(jié)點上執(zhí)行釋放鎖的操作,重置狀態(tài)
  • 失敗重試

    如果一個 client 申請鎖失敗了,那么它需要稍等一會在重試避免多個 client 同時申請鎖的情況,最好的情況是一個 client 需要幾乎同時向 5 個 master 發(fā)起鎖申請。另外就是如果 client 申請鎖失敗了它需要盡快在它曾經(jīng)申請到鎖的 master 上執(zhí)行 unlock 操作,便于其他 client 獲得這把鎖,避免這些鎖過期造成的時間浪費(fèi),當(dāng)然如果這時候網(wǎng)絡(luò)分區(qū)使得 client 無法聯(lián)系上這些 master,那么這種浪費(fèi)就是不得不付出的代價了。

    放鎖

    放鎖操作很簡單,就是依次釋放所有節(jié)點上的鎖就行了

    性能、崩潰恢復(fù)

    如果我們的節(jié)點沒有持久化機(jī)制,client 從 5 個 master 中的 3 個處獲得了鎖,然后其中一個重啟了,這是注意?整個環(huán)境中又出現(xiàn)了 3 個 master 可供另一個 client 申請同一把鎖!?違反了互斥性。如果我們開啟了 AOF 持久化那么情況會稍微好轉(zhuǎn)一些,因為 Redis 的過期機(jī)制是語義層面實現(xiàn)的,所以在 server 掛了的時候時間依舊在流逝,重啟之后鎖狀態(tài)不會受到污染。但是考慮斷電之后呢,AOF部分命令沒來得及刷回磁盤直接丟失了,除非我們配置刷回策略為 fsnyc = always,但這會損傷性能。解決這個問題的方法是,當(dāng)一個節(jié)點重啟之后,我們規(guī)定在 max TTL 期間它是不可用的,這樣它就不會干擾原本已經(jīng)申請到的鎖,等到它 crash 前的那部分鎖都過期了,環(huán)境不存在歷史鎖了,那么再把這個節(jié)點加進(jìn)來正常工作。

    ?

    六、常見問題匯總

    寫到這里,從原理到簡單的實戰(zhàn)就全部寫完了,這里匯總一些常用的以及面試常問的題目,希望幫助到大家。

    1、什么是redis?


    Redis 本質(zhì)上是一個 Key-Value 類型的內(nèi)存數(shù)據(jù)庫,? 整個數(shù)據(jù)庫加載在內(nèi)存當(dāng)中進(jìn)行操作, 定期通過異步操作把數(shù)據(jù)庫數(shù)據(jù) flush 到硬盤上進(jìn)行保存。

    因為是純內(nèi)存操作, Redis 的性能非常出色, 每秒可以處理超過 10 萬次讀寫操作, 是已知性能
    最快的 Key-Value DB。
    Redis 的出色之處不僅僅是性能, Redis 最大的魅力是支持保存多種數(shù)據(jù)結(jié)構(gòu), 此外單個
    value 的最大限制是 1GB, 不像 memcached 只能保存 1MB 的數(shù)據(jù), 因此 Redis 可以用
    來實現(xiàn)很多有用的功能,比方說用他的 List 來做 FIFO 雙向鏈表,實現(xiàn)一個輕量級的高性 能
    消息隊列服務(wù), 用他的 Set 可以做高性能的 tag 系統(tǒng)等等。

    另外 Redis 也可以對存入的Key-Value 設(shè)置 expire 時間, 因此也可以被當(dāng)作一 個功能加強(qiáng)版的 memcached 來用。
    Redis 的主要缺點是數(shù)據(jù)庫容量受到物理內(nèi)存的限制, 不能用作海量數(shù)據(jù)的高性能讀寫, 因此 Redis 適合的場景主要局限在較小數(shù)據(jù)量的高性能操作和運(yùn)算上


    ?2、相比 memcached 有哪些優(yōu)勢?

  • redis支持更豐富的數(shù)據(jù)類型(支持更復(fù)雜的應(yīng)用場景):Redis不僅僅支持簡單的k/v類型的數(shù)據(jù),同時還提供list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲。memcache支持簡單的數(shù)據(jù)類型,String。
  • Redis支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟的時候可以再次加載進(jìn)行使用,而Memecache把數(shù)據(jù)全部存在內(nèi)存之中。
  • 集群模式:memcached沒有原生的集群模式,需要依靠客戶端來實現(xiàn)往集群中分片寫入數(shù)據(jù);但是 redis 目前是原生支持 cluster 模式的.
  • Memcached是多線程,非阻塞IO復(fù)用的網(wǎng)絡(luò)模型;Redis使用單線程的多路 IO 復(fù)用模型。
  • 3、Redis 的全稱是什么?


    Remote Dictionary Server。

    4、支持哪幾種數(shù)據(jù)類型?


    String、 List、 Set、 Sorted Set、 hashes
    ?

    ?

    5、Redis 有哪幾種數(shù)據(jù)淘汰策略?


    noeviction:返回錯誤當(dāng)內(nèi)存限制達(dá)到并且客戶端嘗試執(zhí)行會讓更多內(nèi)存被使用的命令(大
    部分的寫入指令, 但 DEL 和幾個例外)
    allkeys-lru: 嘗試回收最少使用的鍵(LRU), 使得新添加的數(shù)據(jù)有空間存放。
    volatile-lru: 嘗試回收最少使用的鍵(LRU), 但僅限于在過期集合的鍵,使得新添加的數(shù)據(jù)
    有空間存放。
    allkeys-random: 回收隨機(jī)的鍵使得新添加的數(shù)據(jù)有空間存放。
    volatile-random: 回收隨機(jī)的鍵使得新添加的數(shù)據(jù)有空間存放,但僅限于在過期集合的鍵。
    volatile-ttl: 回收在過期集合的鍵, 并且優(yōu)先回收存活時間(TTL) 較短的鍵,使得新添加的
    數(shù)據(jù)有空間存放
    ?

    6、redis為什么采用跳表而不是紅黑樹

    在做范圍查找的時候,平衡樹比skiplist操作要復(fù)雜。在平衡樹上,我們找到指定范圍的小值之后,還需要以中序遍歷的順序繼續(xù)尋找其它不超過大值的節(jié)點。如果不對平衡樹進(jìn)行一定的改造,這里的中序遍歷并不容易實現(xiàn)。而在skiplist上進(jìn)行范圍查找就非常簡單,只需要在找到小值之后,對第1層鏈表進(jìn)行若干步的遍歷就可以實現(xiàn)。
    平衡樹的插入和刪除操作可能引發(fā)子樹的調(diào)整,邏輯復(fù)雜,而skiplist的插入和刪除只需要修改相鄰節(jié)點的指針,操作簡單又快速。
    從內(nèi)存占用上來說,skiplist比平衡樹更靈活一些。一般來說,平衡樹每個節(jié)點包含2個指針(分別指向左右子樹),而skiplist每個節(jié)點包含的指針數(shù)目平均為1/(1-p),具體取決于參數(shù)p的大小。如果像Redis里的實現(xiàn)一樣,取p=1/4,那么平均每個節(jié)點包含1.33個指針,比平衡樹更有優(yōu)勢。
    查找單個key,skiplist和平衡樹的時間復(fù)雜度都為O(log n),大體相當(dāng);而哈希表在保持較低的哈希值沖突概率的前提下,查找時間復(fù)雜度接近O(1),性能更高一些。所以我們平常使用的各種Map或dictionary結(jié)構(gòu),大都是基于哈希表實現(xiàn)的。
    從算法實現(xiàn)難度上來比較,skiplist比平衡樹要簡單得多。

    7、介紹一下HyperLogLog?

    HyperLogLog 是一種概率數(shù)據(jù)結(jié)構(gòu),用來估算數(shù)據(jù)的基數(shù)。數(shù)據(jù)集可以是網(wǎng)站訪客的 IP 地址,E-mail 郵箱或者用戶 ID。

    基數(shù)就是指一個集合中不同值的數(shù)目,比如 a, b, c, d 的基數(shù)就是 4,a, b, c, d, a 的基數(shù)還是 4。雖然 a 出現(xiàn)兩次,只會被計算一次。

    使用 Redis 統(tǒng)計集合的基數(shù)一般有三種方法,分別是使用 Redis 的 HashMap,BitMap 和 HyperLogLog。前兩個數(shù)據(jù)結(jié)構(gòu)在集合的數(shù)量級增長時,所消耗的內(nèi)存會大大增加,但是 HyperLogLog 則不會。

    Redis 的 HyperLogLog 通過犧牲準(zhǔn)確率來減少內(nèi)存空間的消耗,只需要12K內(nèi)存,在標(biāo)準(zhǔn)誤差0.81%的前提下,能夠統(tǒng)計2^64個數(shù)據(jù)。所以 HyperLogLog 是否適合在比如統(tǒng)計日活月活此類的對精度要不不高的場景。

    這是一個很驚人的結(jié)果,以如此小的內(nèi)存來記錄如此大數(shù)量級的數(shù)據(jù)基數(shù)。


    8、為什么 Redis 需要把所有數(shù)據(jù)放到內(nèi)存中?


    Redis 為了達(dá)到最快的讀寫速度將數(shù)據(jù)都讀到內(nèi)存中, 并通過異步的方式將數(shù)據(jù)寫入磁盤。
    所以 Redis 具有快速和數(shù)據(jù)持久化的特征。 如果不將數(shù)據(jù)放在內(nèi)存中, 磁盤 I/O 速度為嚴(yán)重
    影響 Redis 的性能。 在內(nèi)存越來越便宜的今天, Redis 將會越來越受歡迎。

    ?

    9、Redis支持的數(shù)據(jù)類型?

    String字符串:

    格式: set key value

    string類型是二進(jìn)制安全的。意思是redis的string可以包含任何數(shù)據(jù)。比如jpg圖片或者序列化的對象 。

    string類型是Redis最基本的數(shù)據(jù)類型,一個鍵最大能存儲512MB。

    ?

    Hash(哈希)

    格式: hmset name? key1 value1 key2 value2

    Redis hash 是一個鍵值(key=>value)對集合。

    Redis hash是一個string類型的field和value的映射表,hash特別適合用于存儲對象。

    ?

    List(列表)

    Redis 列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)

    格式: lpush? name? value

    在 key 對應(yīng) list 的頭部添加字符串元素

    格式: rpush? name? value

    在 key 對應(yīng) list 的尾部添加字符串元素

    格式: lrem name? index

    key 對應(yīng) list 中刪除 count 個和 value 相同的元素

    格式: llen name??

    返回 key 對應(yīng) list 的長度

    ?

    Set(集合)

    格式: sadd? name? value

    Redis的Set是string類型的無序集合。

    集合是通過哈希表實現(xiàn)的,所以添加,刪除,查找的復(fù)雜度都是O(1)。

    ?

    zset(sorted set:有序集合)

    格式: zadd? name score value

    Redis zset 和 set 一樣也是string類型元素的集合,且不允許重復(fù)的成員。

    不同的是每個元素都會關(guān)聯(lián)一個double類型的分?jǐn)?shù)。redis正是通過分?jǐn)?shù)來為集合中的成員進(jìn)行從小到大的排序。

    zset的成員是唯一的,但分?jǐn)?shù)(score)卻可以重復(fù)。

    ?

    10、 sds相對c的改進(jìn)?

    ??獲取長度:c字符串并不記錄自身長度,所以獲取長度只能遍歷一遍字符串,redis直接讀取len即可。

    ????緩沖區(qū)安全:c字符串容易造成緩沖區(qū)溢出,比如:程序員沒有分配足夠的空間就執(zhí)行拼接操作。而redis會先檢查sds的空間是否滿足所需要求,如果不滿足會自動擴(kuò)充。

    ????內(nèi)存分配:由于c不記錄字符串長度,對于包含了n個字符的字符串,底層總是一個長度n+1的數(shù)組,每一次長度變化,總是要對這個數(shù)組進(jìn)行一次內(nèi)存重新分配的操作。因為內(nèi)存分配涉及復(fù)雜算法并且可能需要執(zhí)行系統(tǒng)調(diào)用,所以它通常是比較耗時的操作。???
    ?

    11、redis鏈表源碼?有什么特性?

    ?

    雙端、無環(huán)、帶長度記錄、

    多態(tài):使用?void*?指針來保存節(jié)點值, 可以通過?dup?、?free?、?match?為節(jié)點值設(shè)置類型特定函數(shù), 可以保存不同類型的值。

    12、字典是如何實現(xiàn)的?

    其實字典這種數(shù)據(jù)結(jié)構(gòu)也內(nèi)置在很多高級語言中,但是c語言沒有,所以redis自己實現(xiàn)了。

    應(yīng)用也比較廣泛,比如redis的數(shù)據(jù)庫就是字典實現(xiàn)的。不僅如此,當(dāng)一個哈希鍵包含的鍵值對比較多,或者都是很長的字符串,redis就會用字典作為哈希鍵的底層實現(xiàn)。

    13、LRU?redis里的具體實現(xiàn)?

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

    LRU算法的設(shè)計原則是:如果一個數(shù)據(jù)在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當(dāng)限定的空間已存滿數(shù)據(jù)時,應(yīng)當(dāng)把最久沒有被訪問到的數(shù)據(jù)淘汰。

    redis原始的淘汰算法簡單實現(xiàn):當(dāng)需要淘汰一個key時,隨機(jī)選擇3個key,淘汰其中間隔時間最長的key。**基本上,我們隨機(jī)選擇key,淘汰key效果很好。后來隨機(jī)3個key改成一個配置項"N隨機(jī)key"。但把默認(rèn)值提高改成5個后效果大大提高。考慮到它的效果,你根本不用修改他。

    14、redis的持久化?

    RDB持久化可以手動執(zhí)行,也可以配置定期執(zhí)行,可以把某個時間的數(shù)據(jù)狀態(tài)保存到RDB文件中,反之,我們可以用RDB文件還原數(shù)據(jù)庫狀態(tài)。

    AOF持久化是通過保存服務(wù)器執(zhí)行的命令來記錄狀態(tài)的。還原的時候再執(zhí)行一遍即可。

    ?

    15、如何選擇合適的持久化方式?


    一般來說, 如果想達(dá)到足以媲美 PostgreSQL 的數(shù)據(jù)安全性, 你應(yīng)該同時使用兩種持久
    化功能。 如果你非常關(guān)心你的數(shù)據(jù), 但仍然可以承受數(shù)分鐘以內(nèi)的數(shù)據(jù)丟失, 那么你可以
    只使用 RDB 持久化。
    有很多用戶都只使用 AOF 持久化, 但并不推薦這種方式: 因為定時生成 RDB 快照
    (snapshot) 非常便于進(jìn)行數(shù)據(jù)庫備份, 并且 RDB 恢復(fù)數(shù)據(jù)集的速度也要比 AOF 恢復(fù)
    的速度要快, 除此之外, 使用 RDB 還可以避免之前提到的 AOF 程序的 bug。
    ?

    16、Redis 集群方案應(yīng)該怎么做? 都有哪些方案?


    1.twemproxy, 大概概念是, 它類似于一個代理方式, 使用方法和普通 Redis 無任何區(qū)別,
    設(shè) 置 好它 下 屬 的多 個 Redis 實 例 后, 使 用 時在 本 需 要 連接 Redis 的 地 方改 為 連接
    twemproxy, 它會以一個代理的身份接收請求并使用一致性 hash 算法, 將請求轉(zhuǎn)接到具
    體 Redis, 將結(jié)果再返回 twemproxy。 使用方式簡便(相對 Redis 只需修改連接端口), 對
    舊項目擴(kuò)展的首選。 問題: twemproxy 自身單端口實例的壓力, 使用一致性 hash 后, 對
    Redis 節(jié)點數(shù)量改變時候的計算值的改變, 數(shù)據(jù)無法自動移動到新的節(jié)點。
    2. codis, 目前用的最多的集群方案, 基本和 twemproxy 一致的效果, 但它支持在 節(jié)點
    數(shù)量改變情況下, 舊節(jié)點數(shù)據(jù)可恢復(fù)到新 hash 節(jié)點。
    3. Redis cluster3.0 自帶的集群, 特點在于他的分布式算法不是一致性 hash, 而是 hash
    槽的概念, 以及自身支持節(jié)點設(shè)置從節(jié)點。 具體看官方文檔介紹。
    4.在業(yè)務(wù)代碼層實現(xiàn), 起幾個毫無關(guān)聯(lián)的 Redis 實例, 在代碼層, 對 key 進(jìn)行 hash 計算,
    然后去對應(yīng)的 Redis 實例操作數(shù)據(jù)。 這種方式對 hash 層代碼要求比較高, 考慮部分包括,
    節(jié)點失效后的替代算法方案, 數(shù)據(jù)震蕩后的自動腳本恢復(fù), 實例的監(jiān)控, 等等


    MySQL 里有 2000w 數(shù)據(jù), Redis 中只存 20w 的數(shù)據(jù),

    17、如何保證 Redis 中的數(shù)據(jù)都是熱點數(shù)據(jù)?


    Redis 內(nèi)存數(shù)據(jù)集大小上升到一定大小的時候, 就會施行數(shù)據(jù)淘汰策略

    18、Redis 有哪些適合的場景?


    (1)、 會話緩存(Session Cache)
    最常用的一種使用 Redis 的情景是會話緩存(session cache)。 用 Redis 緩存會話比其他
    存儲(如 Memcached) 的優(yōu)勢在于: Redis 提供持久化。 當(dāng)維護(hù)一個不是嚴(yán)格要求一致性
    的緩存時, 如果用戶的購物車信息全部丟失, 大部分人都會不高興的, 現(xiàn)在, 他們還會這樣
    嗎?
    幸運(yùn)的是, 隨著 Redis 這些年的改進(jìn), 很容易找到怎么恰當(dāng)?shù)氖褂?Redis 來緩存會話的文
    檔。 甚至廣為人知的商業(yè)平臺 Magento 也提供 Redis 的插件。
    (2)、 全頁緩存(FPC)
    除基本的會話 token 之外, Redis 還提供很簡便的 FPC 平臺。 回到一致性問題, 即使重啟
    了 Redis 實例, 因為有磁盤的持久化, 用戶也不會看到頁面加載速度的下降, 這是一個極
    大改進(jìn), 類似 PHP 本地 FPC。
    再次以 Magento 為例, Magento 提供一個插件來使用 Redis 作為全頁緩存后端。
    此外, 對 WordPress 的用戶來說, Pantheon 有一個非常好的插件 wp-Redis, 這個插件
    能幫助你以最快速度加載你曾瀏覽過的頁面。
    (3)、 隊列
    Reids 在內(nèi)存存儲引擎領(lǐng)域的一大優(yōu)點是提供 list 和 set 操作,這使得 Redis 能作為一個
    很好的消息隊列平臺來使用。 Redis 作為隊列使用的操作, 就類似于本地程序語言(如
    Python) 對 list 的 push/pop 操作。
    如果你快速的在 Google 中搜索“Redis queues”, 你馬上就能找到大量的開源項目, 這些
    項目的目的就是利用 Redis 創(chuàng)建非常好的后端工具, 以滿足各種隊列需求。 例如, Celery
    有一個后臺就是使用 Redis 作為 broker, 你可以從這里去查看。
    (4)、 排行榜/計數(shù)器
    Redis在內(nèi)存中對數(shù)字進(jìn)行遞增或遞減的操作實現(xiàn)的非常好。集合(Set)和有序集合(Sorted
    Set) 也使得我們在執(zhí)行這些操作的時候變的非常簡單, Redis 只是正好提供了這兩種數(shù)據(jù)
    結(jié)構(gòu)。 所以, 我們要從排序集合中獲取到排名最靠前的 10 個用戶–我們稱之為
    “user_scores”, 我們只需要像下面一樣執(zhí)行即可:
    當(dāng)然, 這是假定你是根據(jù)你用戶的分?jǐn)?shù)做遞增的排序。 如果你想返回用戶及用戶的分?jǐn)?shù), 你
    需要這樣執(zhí)行:
    ZRANGE user_scores 0 10 WITHSCORES
    Agora Games 就是一個很好的例子, 用 Ruby 實現(xiàn)的, 它的排行榜就是使用 Redis 來存儲
    數(shù)據(jù)的, 你可以在這里看到。
    (5)、 發(fā)布/訂閱
    最后 是 Redis 的發(fā)布/訂閱功能。 發(fā)布/訂閱的使用場景確實非
    常多。 我已看見人們在社交網(wǎng)絡(luò)連接中使用, 還可作為基于發(fā)布/訂閱的腳本觸發(fā)器, 甚至
    用 Redis 的發(fā)布/訂閱功能來建立聊天系統(tǒng)。


    19、說說 Redis 哈希槽的概念?


    Redis 集群沒有使用一致性 hash,而是引入了哈希槽的概念, Redis 集群有 16384 個哈希槽,
    每個 key 通過 CRC16 校驗后對 16384 取模來決定放置哪個槽, 集群的每個節(jié)點負(fù)責(zé)一部分
    hash 槽

    20、為什么Redis集群有16384個槽

    (1)如果槽位為65536,發(fā)送心跳信息的消息頭達(dá)8k,發(fā)送的心跳包過于龐大。

    如上所述,在消息頭中,最占空間的是myslots[CLUSTER_SLOTS/8]。 當(dāng)槽位為65536時,這塊的大小是:?65536÷8÷1024=8kb?因為每秒鐘,redis節(jié)點需要發(fā)送一定數(shù)量的ping消息作為心跳包,如果槽位為65536,這個ping消息的消息頭太大了,浪費(fèi)帶寬。

    (2)redis的集群主節(jié)點數(shù)量基本不可能超過1000個。

    如上所述,集群節(jié)點越多,心跳包的消息體內(nèi)攜帶的數(shù)據(jù)越多。如果節(jié)點過1000個,也會導(dǎo)致網(wǎng)絡(luò)擁堵。因此redis作者,不建議redis cluster節(jié)點數(shù)量超過1000個。 那么,對于節(jié)點數(shù)在1000以內(nèi)的redis cluster集群,16384個槽位夠用了。沒有必要拓展到65536個。

    (3)槽位越小,節(jié)點少的情況下,壓縮比高

    Redis主節(jié)點的配置信息中,它所負(fù)責(zé)的哈希槽是通過一張bitmap的形式來保存的,在傳輸過程中,會對bitmap進(jìn)行壓縮,但是如果bitmap的填充率slots / N很高的話(N表示節(jié)點數(shù)),bitmap的壓縮率就很低。 如果節(jié)點數(shù)很少,而哈希槽數(shù)量很多的話,bitmap的壓縮率就很低。

    21、Redis 集群會有寫操作丟失嗎? 為什么?


    Redis 并不能保證數(shù)據(jù)的強(qiáng)一致性, 這意味這在實際中集群在特定的條件下可能會丟失寫操
    作。
    ?

    22、Redis 集群方案應(yīng)該怎么做?都有哪些方案?


    1.twemproxy,大概概念是,它類似于一個代理方式, 使用時在本需要連接 redis 的地方改為連接 twemproxy, 它會以一個代理的身份接收請求并使用一致性 hash 算法,將請求轉(zhuǎn)接到具體 redis,將結(jié)果再返回 twemproxy。
    缺點: twemproxy 自身單端口實例的壓力,使用一致性 hash 后,對 redis 節(jié)點數(shù)量改變時候的計算值的改變,數(shù)據(jù)無法自動移動到新的節(jié)點。

    2.codis,目前用的最多的集群方案,基本和 twemproxy 一致的效果,但它支持在 節(jié)點數(shù)量改變情況下,舊節(jié)點數(shù)據(jù)可恢復(fù)到新 hash 節(jié)點

    3.redis cluster3.0 自帶的集群,特點在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持節(jié)點設(shè)置從節(jié)點。具體看官方文檔介紹。
    ?

    23、為什么要做 Redis 分區(qū)?


    分區(qū)可以讓 Redis 管理更大的內(nèi)存, Redis 將可以使用所有機(jī)器的內(nèi)存。 如果沒有分區(qū), 你
    最多只能使用一臺機(jī)器的內(nèi)存。 分區(qū)使 Redis 的計算能力通過簡單地增加計算機(jī)得到成倍提
    升,Redis 的網(wǎng)絡(luò)帶寬也會隨著計算機(jī)和網(wǎng)卡的增加而成倍增長。
    ?

    24、Redis 分區(qū)有什么缺點?


    涉及多個 key 的操作通常不會被支持。 例如你不能對兩個集合求交集, 因為他們可能被存
    儲到不同的 Redis 實例(實際上這種情況也有辦法, 但是不能直接使用交集指令)。
    同時操作多個 key,則不能使用 Redis 事務(wù).
    分區(qū)使用的粒度是key,不能使用一個非常長的排序key存儲一個數(shù)據(jù)集(The partitioning
    granularity is the key, so it is not possible to shard a dataset with a single huge
    key like a very big sorted set) .
    當(dāng)使用分區(qū)的時候, 數(shù)據(jù)處理會非常復(fù)雜, 例如為了備份你必須從不同的 Redis 實例和主
    機(jī)同時收集 RDB / AOF 文件。
    分區(qū)時動態(tài)擴(kuò)容或縮容可能非常復(fù)雜。 Redis 集群在運(yùn)行時增加或者刪除 Redis 節(jié)點, 能
    做到最大程度對用戶透明地數(shù)據(jù)再平衡,但其他一些客戶端分區(qū)或者代理分區(qū)方法則不支持
    這種特性。 然而, 有一種預(yù)分片的技術(shù)也可以較好的解決這個問題。

    ?

    25、Redis 與其他 key-value 存儲有什么不同?


    Redis 有著更為復(fù)雜的數(shù)據(jù)結(jié)構(gòu)并且提供對他們的原子性操作,這是一個不同于其他數(shù)據(jù)庫
    的進(jìn)化路徑。 Redis 的數(shù)據(jù)類型都是基于基本數(shù)據(jù)結(jié)構(gòu)的同時對程序員透明, 無需進(jìn)行額外
    的抽象。
    Redis 運(yùn)行在內(nèi)存中但是可以持久化到磁盤,所以在對不同數(shù)據(jù)集進(jìn)行高速讀寫時需要權(quán)衡
    內(nèi)存, 應(yīng)為數(shù)據(jù)量不能大于硬件內(nèi)存。 在內(nèi)存數(shù)據(jù)庫方面的另一個優(yōu)點是, 相比在磁盤上
    相同的復(fù)雜的數(shù)據(jù)結(jié)構(gòu), 在內(nèi)存中操作起來非常簡單, 這樣 Redis 可以做很多內(nèi)部復(fù)雜性
    很強(qiáng)的事情。 同時, 在磁盤格式方面他們是緊湊的以追加的方式產(chǎn)生的, 因為他們并不需
    要進(jìn)行隨機(jī)訪問


    26、Redis 的內(nèi)存用完了會發(fā)生什么?


    如果達(dá)到設(shè)置的上限, Redis 的寫命令會返回錯誤信息(但是讀命令還可以正常返回。) 或
    者你可以將 Redis 當(dāng)緩存來使用配置淘汰機(jī)制,當(dāng) Redis 達(dá)到內(nèi)存上限時會沖刷掉舊的內(nèi)容。
    ?

    27、Redis 是單線程的, 如何提高多核 CPU 的利用率?


    可以在同一個服務(wù)器部署多個 Redis 的實例, 并把他們當(dāng)作不同的服務(wù)器來使用, 在某些時
    候, 無論如何一個服務(wù)器是不夠的,
    所以, 如果你想使用多個 CPU, 你可以考慮一下分片(shard)。


    28、一個 Redis 實例最多能存放多少的 keys? List、 Set、Sorted Set 他們最多能存放多少元素?


    理論上 Redis 可以處理多達(dá) 232 的 keys, 并且在實際中進(jìn)行了測試, 每個實例至少存放了 2億 5 千萬的 keys。 我們正在測試一些較大的值。
    任何 list、 set、 和 sorted set 都可以放 232 個元素。
    換句話說, Redis 的存儲極限是系統(tǒng)中的可用內(nèi)存值

    ?

    29、修改配置不重啟 Redis 會實時生效嗎?


    針對運(yùn)行實例, 有許多配置選項可以通過 CONFIG SET 命令進(jìn)行修改, 而無需執(zhí)行任何
    形式的重啟。 從 Redis 2.2 開始, 可以從 AOF 切換到 RDB 的快照持久性或其他方式
    而不需要重啟 Redis。 檢索 ‘CONFIG GET *’ 命令獲取更多信息。
    但偶爾重新啟動是必須的, 如為升級 Redis 程序到新的版本, 或者當(dāng)你需要修改某些目前
    CONFIG 命令還不支持的配置參數(shù)的時候
    ?

    30、哨兵

    Redis sentinel 是一個分布式系統(tǒng)中監(jiān)控 redis 主從服務(wù)器,并在主服務(wù)器下線時自動進(jìn)行故障轉(zhuǎn)移。其中三個特性:

    監(jiān)控(Monitoring):??? Sentinel? 會不斷地檢查你的主服務(wù)器和從服務(wù)器是否運(yùn)作正常。

    提醒(Notification): 被監(jiān)控的某個 Redis 服務(wù)器出現(xiàn)問題時, Sentinel 可以通過 API 向管理員或者其他應(yīng)用程序發(fā)送通知。

    自動故障遷移(Automatic failover): 當(dāng)一個主服務(wù)器不能正常工作時, Sentinel 會開始一次自動故障遷移操作。

    特點:

    1、保證高可用

    2、監(jiān)控各個節(jié)點

    3、自動故障遷移

    缺點:主從模式,切換需要時間丟數(shù)據(jù)

    沒有解決 master 寫的壓力

    ?

    31、緩存穿透


    一般的緩存系統(tǒng),都是按照key去緩存查詢,如果不存在對應(yīng)的value,就去后端系統(tǒng)查找(比如DB)。

    一些惡意的請求會故意查詢不存在的key,請求量很大,就會對后端系統(tǒng)造成很大的壓力。這就叫做緩存穿透。

    ?

    如何避免?

    1:對查詢結(jié)果為空的情況也進(jìn)行緩存,這樣,再次訪問時,緩存層會直接返回空值。緩存時間設(shè)置短一點,或者該key對應(yīng)的數(shù)據(jù)insert了之后清理緩存。

    2:對一定不存在的key進(jìn)行過濾。具體請看布隆過濾器

    ?

    32、緩存擊穿


    是針對緩存中沒有但數(shù)據(jù)庫有的數(shù)據(jù)。

    場景是,當(dāng)Key失效后,假如瞬間突然涌入大量的請求,來請求同一個Key,這些請求不會命中Redis,都會請求到DB,導(dǎo)致數(shù)據(jù)庫壓力過大,甚至扛不住,掛掉。

    解決辦法

    1、設(shè)置熱點Key,自動檢測熱點Key,將熱點Key的過期時間加大或者設(shè)置為永不過期,或者設(shè)置為邏輯上永不過期

    2、加互斥鎖。當(dāng)發(fā)現(xiàn)沒有命中Redis,去查數(shù)據(jù)庫的時候,在執(zhí)行更新緩存的操作上加鎖,當(dāng)一個線程訪問時,其它線程等待,這個線程訪問過后,緩存中的數(shù)據(jù)會被重建,這樣其他線程就可以從緩存中取值。

    ?

    33、緩存雪崩


    是指大量Key同時失效,對這些Key的請求又會打到DB上,同樣會導(dǎo)致數(shù)據(jù)庫壓力過大甚至掛掉。

    解決辦法

    1)讓Key的失效時間分散開,可以在統(tǒng)一的失效時間上再加一個隨機(jī)值,或者使用更高級的算法分散失效時間。

    2)構(gòu)建多個redis實例,個別節(jié)點掛了還有別的可以用。

    3)多級緩存:比如增加本地緩存,減小redis壓力。

    4)對存儲層增加限流措施,當(dāng)請求超出限制,提供降級服務(wù)(一般就是返回錯誤即可)

    ?

    34、單線程的redis為什么這么快

    (一)純內(nèi)存操作
    (二)單線程操作,避免了頻繁的上下文切換
    (三)采用了非阻塞I/O多路復(fù)用機(jī)制

    (其實就是歷史遺留問題,非要吹的這么好。。。)

    ?

    35、redis采用的刪除策略

    ?

    redis采用的是定期刪除+惰性刪除策略。


    36、為什么不用定時刪除策略?


    定時刪除,用一個定時器來負(fù)責(zé)監(jiān)視key,過期則自動刪除。雖然內(nèi)存及時釋放,但是十分消耗CPU資源。在大并發(fā)請求下,CPU要將時間應(yīng)用在處理請求,而不是刪除key,因此沒有采用這一策略.


    37、定期刪除+惰性刪除是如何工作的呢?


    定期刪除,redis默認(rèn)每個100ms檢查,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每個100ms將所有的key檢查一次,而是隨機(jī)抽取進(jìn)行檢查(如果每隔100ms,全部key進(jìn)行檢查,redis豈不是卡死)。因此,如果只采用定期刪除策略,會導(dǎo)致很多key到時間沒有刪除。
    于是,惰性刪除派上用場。也就是說在你獲取某個key的時候,redis會檢查一下,這個key如果設(shè)置了過期時間那么是否過期了?如果過期了此時就會刪除。

    ?

    38、為什么Redis的操作是原子性的,怎么保證原子性的?


    對于Redis而言,命令的原子性指的是:一個操作的不可以再分,操作要么執(zhí)行,要么不執(zhí)行。
    Redis的操作之所以是原子性的,是因為Redis是單線程的。
    Redis本身提供的所有API都是原子操作,Redis中的事務(wù)其實是要保證批量操作的原子性。
    多個命令在并發(fā)中也是原子性的嗎?
    不一定, 將get和set改成單命令操作,incr 。使用Redis的事務(wù),或者使用Redis+Lua==的方式實現(xiàn).
    ?

    39、消息隊列

    不要使用redis去做消息隊列,這不是redis的設(shè)計目標(biāo)。但實在太多人使用redis去做去消息隊列,redis的作者看不下去。

    kafka才好用

    ?

    有問題隨時評論區(qū)提問

    ?

    ?七、開個腦洞

    ?最近又研究了一下redis,說實話,我越研究越覺得不靠譜,為啥會有nosql這種東西?

    以下是我混亂的思考

    未來架構(gòu)越來越復(fù)雜,saas做不了一致性交給redis做。

    如果數(shù)據(jù)庫本身就有個redis呢?是不是我們就不需要這玩意了。

    不對,使用場景還是有區(qū)別的,數(shù)據(jù)庫本身不就有buffer pool么。

    一直在琢磨redis做緩存的必要性。

    我要把。。

    工作上的第三課,就是得考慮成本。之前我是純正的redis吹,但是mysql有緩存那些數(shù)據(jù)和redis速度差不了太多,而且沒有關(guān)系型數(shù)據(jù)庫記錄全面,最重要的是貴!

    最重要的是貴!最重要的是貴!最重要的是貴!

    我從一個幼稚的redis吹,變成了考慮更全面的人,看到了redis越來越多的缺點。希望,是我成長了吧。


    全篇完。?

    總結(jié)

    以上是生活随笔為你收集整理的《这是全网最硬核redis总结,谁赞成,谁反对?》六万字大合集的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

    久久久中文久久久无码 | 午夜精品久久久内射近拍高清 | 夜先锋av资源网站 | 天下第一社区视频www日本 | 又紧又大又爽精品一区二区 | 亚洲精品一区二区三区在线观看 | 国产精品怡红院永久免费 | 啦啦啦www在线观看免费视频 | 老头边吃奶边弄进去呻吟 | 国产午夜亚洲精品不卡下载 | 日本一区二区更新不卡 | 狠狠色丁香久久婷婷综合五月 | 成年美女黄网站色大免费视频 | 日本www一道久久久免费榴莲 | 久久精品国产99精品亚洲 | 日日干夜夜干 | 欧美 日韩 亚洲 在线 | 久久www免费人成人片 | 久久精品人人做人人综合 | 欧美一区二区三区视频在线观看 | 日韩精品a片一区二区三区妖精 | 国产欧美亚洲精品a | 久久人人爽人人爽人人片ⅴ | 久久久久人妻一区精品色欧美 | 日本精品少妇一区二区三区 | 亚洲精品国产精品乱码视色 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国产一区二区不卡老阿姨 | 香港三级日本三级妇三级 | 国产av无码专区亚洲a∨毛片 | 蜜臀av在线播放 久久综合激激的五月天 | 在线成人www免费观看视频 | 熟妇人妻无乱码中文字幕 | 精品一区二区三区波多野结衣 | 成人一区二区免费视频 | 亚洲人成人无码网www国产 | 午夜理论片yy44880影院 | 国产亚洲tv在线观看 | 中文字幕乱码亚洲无线三区 | 麻豆国产丝袜白领秘书在线观看 | 色综合久久中文娱乐网 | 99久久精品午夜一区二区 | 娇妻被黑人粗大高潮白浆 | 麻豆md0077饥渴少妇 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 亚洲欧美精品伊人久久 | 久久99精品久久久久久 | 成在人线av无码免观看麻豆 | 中文字幕+乱码+中文字幕一区 | 国产又粗又硬又大爽黄老大爷视 | 女人被男人爽到呻吟的视频 | 亚洲性无码av中文字幕 | 日日噜噜噜噜夜夜爽亚洲精品 | 2019nv天堂香蕉在线观看 | 大肉大捧一进一出视频出来呀 | 国内精品人妻无码久久久影院蜜桃 | 国产精品久久久午夜夜伦鲁鲁 | 亚洲精品午夜国产va久久成人 | 天天爽夜夜爽夜夜爽 | 免费看男女做好爽好硬视频 | 国产成人无码一二三区视频 | 性欧美牲交在线视频 | 中国女人内谢69xxxxxa片 | 377p欧洲日本亚洲大胆 | 又色又爽又黄的美女裸体网站 | 欧美丰满少妇xxxx性 | 学生妹亚洲一区二区 | 久精品国产欧美亚洲色aⅴ大片 | 精品乱码久久久久久久 | 亚洲日韩av一区二区三区四区 | 东京热男人av天堂 | 成人片黄网站色大片免费观看 | 丰满人妻翻云覆雨呻吟视频 | 少妇久久久久久人妻无码 | 波多野结衣av一区二区全免费观看 | 欧美日本精品一区二区三区 | 亚洲成a人片在线观看无码3d | 中文字幕 亚洲精品 第1页 | 捆绑白丝粉色jk震动捧喷白浆 | 国产麻豆精品一区二区三区v视界 | 色窝窝无码一区二区三区色欲 | 乱中年女人伦av三区 | 国产成人无码av一区二区 | 爽爽影院免费观看 | 亚洲自偷自拍另类第1页 | 精品久久久无码人妻字幂 | 久久综合给合久久狠狠狠97色 | 无码精品国产va在线观看dvd | 97夜夜澡人人双人人人喊 | 无码成人精品区在线观看 | 久久久久久久久888 | 久久久久免费看成人影片 | 麻豆蜜桃av蜜臀av色欲av | 日韩 欧美 动漫 国产 制服 | 国产成人无码a区在线观看视频app | 国产无遮挡又黄又爽免费视频 | 性色av无码免费一区二区三区 | 天天燥日日燥 | 欧美肥老太牲交大战 | 国产亚洲欧美日韩亚洲中文色 | 日本免费一区二区三区最新 | 久久伊人色av天堂九九小黄鸭 | 亚洲爆乳大丰满无码专区 | 亚洲熟妇色xxxxx欧美老妇 | 国产精品国产自线拍免费软件 | 国产av剧情md精品麻豆 | 亚洲精品一区二区三区在线 | 在线播放免费人成毛片乱码 | 成人无码精品一区二区三区 | 99视频精品全部免费免费观看 | 精品乱码久久久久久久 | 欧美性生交xxxxx久久久 | 一区二区三区高清视频一 | 99久久人妻精品免费一区 | 曰本女人与公拘交酡免费视频 | 奇米影视7777久久精品 | 欧美日韩视频无码一区二区三 | 夜夜影院未满十八勿进 | 激情内射日本一区二区三区 | 日产精品99久久久久久 | 人妻人人添人妻人人爱 | 成 人影片 免费观看 | 欧美国产日韩亚洲中文 | 少妇久久久久久人妻无码 | 青青草原综合久久大伊人精品 | 欧美大屁股xxxxhd黑色 | 丝袜美腿亚洲一区二区 | 亚洲综合无码一区二区三区 | 亚洲大尺度无码无码专区 | 秋霞特色aa大片 | 无套内谢老熟女 | 在线亚洲高清揄拍自拍一品区 | 老太婆性杂交欧美肥老太 | 在线观看国产午夜福利片 | 好屌草这里只有精品 | 久久精品女人天堂av免费观看 | 国产精品久久久午夜夜伦鲁鲁 | 荫蒂被男人添的好舒服爽免费视频 | 国内精品人妻无码久久久影院蜜桃 | 波多野结衣aⅴ在线 | 久久99精品国产.久久久久 | 国产成人无码av片在线观看不卡 | 国产日产欧产精品精品app | 国产人妻人伦精品1国产丝袜 | 伊人久久大香线焦av综合影院 | 国产超碰人人爽人人做人人添 | 在线观看欧美一区二区三区 | 色综合久久中文娱乐网 | 亚洲精品久久久久中文第一幕 | 国产午夜福利100集发布 | 久久人妻内射无码一区三区 | 婷婷综合久久中文字幕蜜桃三电影 | 国产av无码专区亚洲a∨毛片 | 精品人人妻人人澡人人爽人人 | 伦伦影院午夜理论片 | 伊在人天堂亚洲香蕉精品区 | 亚洲精品无码人妻无码 | 国产 精品 自在自线 | 国产精品自产拍在线观看 | 欧美日韩在线亚洲综合国产人 | 午夜男女很黄的视频 | 任你躁国产自任一区二区三区 | 少妇厨房愉情理9仑片视频 | 男人的天堂av网站 | 国产精品欧美成人 | 国产成人av免费观看 | 日韩亚洲欧美中文高清在线 | 亚洲精品综合一区二区三区在线 | 国产莉萝无码av在线播放 | 精品 日韩 国产 欧美 视频 | 天天拍夜夜添久久精品大 | 国产精品久久久久7777 | 国产精品久久国产精品99 | 欧美放荡的少妇 | 成人试看120秒体验区 | 久久久久成人精品免费播放动漫 | 又大又黄又粗又爽的免费视频 | 精品成在人线av无码免费看 | 牛和人交xxxx欧美 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 奇米影视7777久久精品 | 欧美丰满老熟妇xxxxx性 | 无码一区二区三区在线 | 成人毛片一区二区 | 国产激情艳情在线看视频 | 国产精品国产三级国产专播 | 中文字幕日产无线码一区 | 午夜福利试看120秒体验区 | 国产无av码在线观看 | 麻豆成人精品国产免费 | 中文字幕久久久久人妻 | 日韩精品无码免费一区二区三区 | 国产精品人妻一区二区三区四 | 少妇性l交大片欧洲热妇乱xxx | 中文无码伦av中文字幕 | 男女爱爱好爽视频免费看 | 2019午夜福利不卡片在线 | 欧美人与牲动交xxxx | 成在人线av无码免费 | 久久99精品国产麻豆 | 亚洲阿v天堂在线 | yw尤物av无码国产在线观看 | 欧美国产日产一区二区 | 99久久精品无码一区二区毛片 | 国产国语老龄妇女a片 | 中文亚洲成a人片在线观看 | 亚洲熟妇色xxxxx欧美老妇y | 欧美性猛交xxxx富婆 | 在线 国产 欧美 亚洲 天堂 | 免费观看的无遮挡av | 久久亚洲精品中文字幕无男同 | 精品无码国产自产拍在线观看蜜 | 国产亚洲视频中文字幕97精品 | 色爱情人网站 | 久久人人爽人人人人片 | 欧美激情内射喷水高潮 | 欧美一区二区三区视频在线观看 | 国产精品永久免费视频 | 亚洲精品一区二区三区四区五区 | 久久久久成人精品免费播放动漫 | 欧美乱妇无乱码大黄a片 | 日韩亚洲欧美精品综合 | 97夜夜澡人人爽人人喊中国片 | 欧美亚洲国产一区二区三区 | 久久精品国产99久久6动漫 | 又大又紧又粉嫩18p少妇 | 国产精品无码一区二区三区不卡 | 中文精品久久久久人妻不卡 | 亚洲一区av无码专区在线观看 | 亚洲s码欧洲m码国产av | 少妇性l交大片欧洲热妇乱xxx | 日本高清一区免费中文视频 | 1000部啪啪未满十八勿入下载 | 亚洲中文字幕在线无码一区二区 | 国产成人无码区免费内射一片色欲 | 久久久久久九九精品久 | 亚洲欧洲日本综合aⅴ在线 | 久久久久免费看成人影片 | 久久97精品久久久久久久不卡 | 国语精品一区二区三区 | 人人爽人人澡人人高潮 | 国产内射爽爽大片视频社区在线 | 亚洲区欧美区综合区自拍区 | 日本一区二区更新不卡 | 亚洲精品美女久久久久久久 | 国产亚洲欧美日韩亚洲中文色 | 蜜桃无码一区二区三区 | 久久99精品久久久久久 | 午夜理论片yy44880影院 | 沈阳熟女露脸对白视频 | 乱人伦中文视频在线观看 | 国产日产欧产精品精品app | 国产av无码专区亚洲awww | 国产亚洲精品久久久久久 | 国产成人一区二区三区别 | 人妻少妇精品无码专区动漫 | 色婷婷综合中文久久一本 | 亚洲爆乳无码专区 | 日日噜噜噜噜夜夜爽亚洲精品 | 日本一区二区三区免费播放 | 国产乱人伦av在线无码 | 欧美第一黄网免费网站 | 国产高清av在线播放 | 国产精品无套呻吟在线 | 性欧美牲交在线视频 | 国产精品-区区久久久狼 | 国产精品久久久午夜夜伦鲁鲁 | 亚洲精品久久久久中文第一幕 | 国产精品久久福利网站 | 亚洲一区二区三区香蕉 | 久久亚洲a片com人成 | 综合激情五月综合激情五月激情1 | 国产精品爱久久久久久久 | 国产电影无码午夜在线播放 | 女高中生第一次破苞av | 欧美国产日韩亚洲中文 | 狠狠综合久久久久综合网 | 六月丁香婷婷色狠狠久久 | 久久久精品欧美一区二区免费 | 国产精品欧美成人 | 55夜色66夜色国产精品视频 | 丰满肥臀大屁股熟妇激情视频 | 欧美日韩亚洲国产精品 | 在线a亚洲视频播放在线观看 | аⅴ资源天堂资源库在线 | 国产日产欧产精品精品app | 久久五月精品中文字幕 | 东京热一精品无码av | 熟女俱乐部五十路六十路av | 中文字幕无码免费久久99 | 欧美丰满熟妇xxxx性ppx人交 | 一本加勒比波多野结衣 | 亚洲日韩乱码中文无码蜜桃臀网站 | 一本色道婷婷久久欧美 | 精品欧美一区二区三区久久久 | 99国产精品白浆在线观看免费 | 亚洲精品无码国产 | 亚洲无人区一区二区三区 | 国产综合色产在线精品 | 亚洲国产av精品一区二区蜜芽 | 2020久久超碰国产精品最新 | 伦伦影院午夜理论片 | 久久综合给合久久狠狠狠97色 | 国产亚洲精品久久久ai换 | 日本熟妇人妻xxxxx人hd | av香港经典三级级 在线 | 国产精品无码mv在线观看 | 欧美老妇交乱视频在线观看 | 麻豆人妻少妇精品无码专区 | 噜噜噜亚洲色成人网站 | 国产成人综合美国十次 | 国产精品久久福利网站 | 中文字幕精品av一区二区五区 | 国产精品办公室沙发 | 欧美成人家庭影院 | 内射爽无广熟女亚洲 | 欧美三级a做爰在线观看 | 亚洲一区二区三区 | 欧美xxxx黑人又粗又长 | 日韩人妻无码中文字幕视频 | 宝宝好涨水快流出来免费视频 | 国产成人无码av一区二区 | 国产成人无码av在线影院 | 国产又爽又黄又刺激的视频 | 久久精品中文闷骚内射 | 亚洲の无码国产の无码步美 | 欧美性猛交内射兽交老熟妇 | 精品久久久中文字幕人妻 | 亚洲男人av香蕉爽爽爽爽 | 四虎4hu永久免费 | 久久国产精品_国产精品 | 亚洲无人区一区二区三区 | 国产成人无码区免费内射一片色欲 | 网友自拍区视频精品 | 性欧美videos高清精品 | 国产午夜亚洲精品不卡 | 亚洲国产午夜精品理论片 | 日韩人妻少妇一区二区三区 | 中文字幕av伊人av无码av | 欧美三级不卡在线观看 | 高中生自慰www网站 | 成人欧美一区二区三区黑人免费 | 久久这里只有精品视频9 | 漂亮人妻洗澡被公强 日日躁 | 又粗又大又硬毛片免费看 | 中文字幕+乱码+中文字幕一区 | 兔费看少妇性l交大片免费 | 亚洲午夜久久久影院 | 久久人人97超碰a片精品 | 久久精品中文字幕一区 | 久久久久99精品成人片 | 亚洲欧洲中文日韩av乱码 | 在线天堂新版最新版在线8 | 牛和人交xxxx欧美 | 2019午夜福利不卡片在线 | 亚洲中文字幕无码一久久区 | 亚洲国产精品一区二区第一页 | 久久99久久99精品中文字幕 | 亚洲色大成网站www | 日韩欧美群交p片內射中文 | 东京热男人av天堂 | 人人妻人人澡人人爽欧美一区九九 | 内射巨臀欧美在线视频 | 国产亚洲美女精品久久久2020 | 无遮挡国产高潮视频免费观看 | 青青草原综合久久大伊人精品 | 成人精品天堂一区二区三区 | 无码人妻丰满熟妇区五十路百度 | 999久久久国产精品消防器材 | 黑人巨大精品欧美一区二区 | 狠狠噜狠狠狠狠丁香五月 | 久久99精品国产.久久久久 | 国产sm调教视频在线观看 | 亚洲欧美国产精品专区久久 | 国产凸凹视频一区二区 | 国内精品九九久久久精品 | 国产真实夫妇视频 | 一个人免费观看的www视频 | 又大又硬又黄的免费视频 | 中文字幕人妻丝袜二区 | 伦伦影院午夜理论片 | 色婷婷综合激情综在线播放 | 欧美成人午夜精品久久久 | 理论片87福利理论电影 | 好爽又高潮了毛片免费下载 | 狠狠色欧美亚洲狠狠色www | 99国产精品白浆在线观看免费 | 国产精品无套呻吟在线 | 国产美女极度色诱视频www | 久久精品国产99久久6动漫 | 国产超碰人人爽人人做人人添 | 国产凸凹视频一区二区 | 国产精品亚洲五月天高清 | 久久国内精品自在自线 | 精品人妻中文字幕有码在线 | 亚洲高清偷拍一区二区三区 | 九九综合va免费看 | 大胆欧美熟妇xx | 男女爱爱好爽视频免费看 | 免费无码肉片在线观看 | 少妇被粗大的猛进出69影院 | 国语自产偷拍精品视频偷 | 成人亚洲精品久久久久 | 国产三级精品三级男人的天堂 | 国内老熟妇对白xxxxhd | 国产在线精品一区二区三区直播 | 成 人 网 站国产免费观看 | 久久亚洲精品中文字幕无男同 | 国产人妻精品一区二区三区不卡 | 日韩av无码一区二区三区 | 国产亚洲视频中文字幕97精品 | 国产女主播喷水视频在线观看 | 俄罗斯老熟妇色xxxx | 亚洲一区二区三区四区 | 人妻夜夜爽天天爽三区 | 精品国产av色一区二区深夜久久 | 在线视频网站www色 | 国产激情一区二区三区 | 亚洲欧洲日本无在线码 | 亚洲va中文字幕无码久久不卡 | 无码人妻出轨黑人中文字幕 | 麻豆国产人妻欲求不满谁演的 | 美女扒开屁股让男人桶 | 少妇人妻偷人精品无码视频 | 中文精品久久久久人妻不卡 | 性做久久久久久久免费看 | 国产午夜手机精彩视频 | 无人区乱码一区二区三区 | 青草视频在线播放 | 中文字幕无码人妻少妇免费 | 精品水蜜桃久久久久久久 | 欧美老妇交乱视频在线观看 | aa片在线观看视频在线播放 | 西西人体www44rt大胆高清 | 国产明星裸体无码xxxx视频 | 欧美国产日韩久久mv | 大地资源中文第3页 | 日本精品久久久久中文字幕 | 亲嘴扒胸摸屁股激烈网站 | 蜜臀av在线观看 在线欧美精品一区二区三区 | 中文字幕无码免费久久99 | 国产精品久久久久影院嫩草 | 色综合久久久久综合一本到桃花网 | 宝宝好涨水快流出来免费视频 | 久久国内精品自在自线 | 亚洲自偷自偷在线制服 | 亚洲精品久久久久久久久久久 | 国产婷婷色一区二区三区在线 | 亚洲中文字幕在线观看 | 亚洲精品欧美二区三区中文字幕 | 国产激情精品一区二区三区 | 99精品久久毛片a片 | 久久久无码中文字幕久... | 人妻少妇被猛烈进入中文字幕 | 久久综合狠狠综合久久综合88 | 性欧美疯狂xxxxbbbb | 中文字幕无线码免费人妻 | 欧美精品国产综合久久 | 国产精品.xx视频.xxtv | 欧美 日韩 人妻 高清 中文 | 国产av人人夜夜澡人人爽麻豆 | 岛国片人妻三上悠亚 | 国产无遮挡吃胸膜奶免费看 | 丁香花在线影院观看在线播放 | 国产精品香蕉在线观看 | 亚洲欧美国产精品专区久久 | 国产一区二区三区精品视频 | 黑人粗大猛烈进出高潮视频 | 久久婷婷五月综合色国产香蕉 | 真人与拘做受免费视频 | 又大又硬又黄的免费视频 | 欧美成人家庭影院 | 亚洲精品国偷拍自产在线麻豆 | 在线播放无码字幕亚洲 | 影音先锋中文字幕无码 | 亚洲 激情 小说 另类 欧美 | 熟女体下毛毛黑森林 | 亚洲小说春色综合另类 | 欧美人与物videos另类 | 人妻人人添人妻人人爱 | 精品国精品国产自在久国产87 | 丁香啪啪综合成人亚洲 | 爱做久久久久久 | 亚洲精品国偷拍自产在线麻豆 | 国产99久久精品一区二区 | 亚洲熟熟妇xxxx | 又大又硬又黄的免费视频 | 狂野欧美激情性xxxx | 少妇邻居内射在线 | 99久久精品午夜一区二区 | 午夜福利试看120秒体验区 | 亚洲成a人片在线观看无码3d | 在线a亚洲视频播放在线观看 | 蜜桃视频插满18在线观看 | 女人和拘做爰正片视频 | 日韩欧美中文字幕在线三区 | 一本一道久久综合久久 | 亚洲一区二区三区无码久久 | 国产亚av手机在线观看 | 国产黑色丝袜在线播放 | 欧美老熟妇乱xxxxx | 在线а√天堂中文官网 | 久久熟妇人妻午夜寂寞影院 | 久久成人a毛片免费观看网站 | 亚洲自偷自偷在线制服 | 免费国产黄网站在线观看 | 国产xxx69麻豆国语对白 | 一本色道久久综合亚洲精品不卡 | 亚洲乱码国产乱码精品精 | 成年美女黄网站色大免费视频 | 无码人妻av免费一区二区三区 | 日韩精品无码一区二区中文字幕 | 午夜丰满少妇性开放视频 | 国产成人无码a区在线观看视频app | 国产av一区二区三区最新精品 | 亚洲精品一区二区三区在线 | 丝袜足控一区二区三区 | 亚洲熟悉妇女xxx妇女av | 美女毛片一区二区三区四区 | 成在人线av无码免费 | 国产高清av在线播放 | 国产亚洲欧美日韩亚洲中文色 | 色婷婷av一区二区三区之红樱桃 | 99久久99久久免费精品蜜桃 | 99精品无人区乱码1区2区3区 | 亚洲精品综合一区二区三区在线 | 四十如虎的丰满熟妇啪啪 | 国产色在线 | 国产 | 我要看www免费看插插视频 | aⅴ在线视频男人的天堂 | 欧美熟妇另类久久久久久不卡 | 亚洲经典千人经典日产 | 日韩亚洲欧美中文高清在线 | 131美女爱做视频 | 亚洲の无码国产の无码影院 | 特级做a爰片毛片免费69 | 色噜噜亚洲男人的天堂 | 乱中年女人伦av三区 | 国产美女极度色诱视频www | 特黄特色大片免费播放器图片 | 自拍偷自拍亚洲精品被多人伦好爽 | aⅴ在线视频男人的天堂 | 伊人久久大香线蕉午夜 | 国产明星裸体无码xxxx视频 | 欧美猛少妇色xxxxx | 成人无码视频免费播放 | 国产精品久久精品三级 | 国产办公室秘书无码精品99 | 亚洲男人av香蕉爽爽爽爽 | 大地资源中文第3页 | 国产三级久久久精品麻豆三级 | 亚洲色大成网站www | 色欲av亚洲一区无码少妇 | 小泽玛莉亚一区二区视频在线 | 亚洲 a v无 码免 费 成 人 a v | 国产色视频一区二区三区 | 亚洲熟妇色xxxxx欧美老妇 | 久久久久久av无码免费看大片 | 国语自产偷拍精品视频偷 | 久久久久亚洲精品男人的天堂 | 国产香蕉尹人综合在线观看 | 久久久国产精品无码免费专区 | 久久精品国产一区二区三区 | 欧美一区二区三区视频在线观看 | 亚洲国产综合无码一区 | 在线播放无码字幕亚洲 | 四虎国产精品一区二区 | 亚洲精品无码国产 | 久久综合激激的五月天 | 亚洲中文字幕在线观看 | 国产在线aaa片一区二区99 | 欧美日韩精品 | 国内少妇偷人精品视频 | 无码av最新清无码专区吞精 | 狠狠色色综合网站 | 欧美激情一区二区三区成人 | 亚洲gv猛男gv无码男同 | 国产欧美精品一区二区三区 | 成人女人看片免费视频放人 | 日本成熟视频免费视频 | 中文字幕亚洲情99在线 | 麻豆果冻传媒2021精品传媒一区下载 | 一本无码人妻在中文字幕免费 | 樱花草在线播放免费中文 | 亚洲日韩中文字幕在线播放 | 久久久久久九九精品久 | 131美女爱做视频 | 曰韩无码二三区中文字幕 | 狠狠躁日日躁夜夜躁2020 | 强奷人妻日本中文字幕 | 亚洲区小说区激情区图片区 | 日本大乳高潮视频在线观看 | 蜜臀av在线观看 在线欧美精品一区二区三区 | 精品人妻av区 | 国产av一区二区精品久久凹凸 | 欧美肥老太牲交大战 | 夜夜夜高潮夜夜爽夜夜爰爰 | 午夜福利不卡在线视频 | 熟女少妇人妻中文字幕 | 特大黑人娇小亚洲女 | 欧美性色19p | 亚洲综合另类小说色区 | 亚洲一区二区三区含羞草 | 亚洲精品午夜无码电影网 | 欧美国产日韩亚洲中文 | 99久久亚洲精品无码毛片 | 中文字幕+乱码+中文字幕一区 | 国产精品久久久久无码av色戒 | 国产精品.xx视频.xxtv | 亚洲人成影院在线无码按摩店 | 国产精品自产拍在线观看 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 牲交欧美兽交欧美 | 99精品无人区乱码1区2区3区 | 亚洲人成影院在线观看 | 1000部夫妻午夜免费 | 欧美放荡的少妇 | 色爱情人网站 | 特大黑人娇小亚洲女 | 丰满少妇女裸体bbw | 99久久无码一区人妻 | 久久久久av无码免费网 | 中文字幕av日韩精品一区二区 | 97夜夜澡人人爽人人喊中国片 | 久久精品国产亚洲精品 | 中文毛片无遮挡高清免费 | 国产av人人夜夜澡人人爽麻豆 | 中文字幕久久久久人妻 | 日日麻批免费40分钟无码 | 99精品久久毛片a片 | 国产内射爽爽大片视频社区在线 | 无码人妻出轨黑人中文字幕 | 亚洲中文字幕成人无码 | 亚洲最大成人网站 | 日本熟妇乱子伦xxxx | 久久午夜夜伦鲁鲁片无码免费 | 日本精品久久久久中文字幕 | 国产精品久久久久久亚洲影视内衣 | 最近免费中文字幕中文高清百度 | 漂亮人妻洗澡被公强 日日躁 | 久久精品国产日本波多野结衣 | 国产乱人无码伦av在线a | 日本乱偷人妻中文字幕 | 国産精品久久久久久久 | 丰满少妇高潮惨叫视频 | 中文字幕乱码中文乱码51精品 | 欧美日韩一区二区综合 | 亚洲熟妇自偷自拍另类 | 日韩精品乱码av一区二区 | 99久久人妻精品免费一区 | 亚洲精品久久久久久一区二区 | 俺去俺来也www色官网 | 精品无码成人片一区二区98 | 99久久99久久免费精品蜜桃 | 奇米影视888欧美在线观看 | 青春草在线视频免费观看 | 中文字幕日韩精品一区二区三区 | 日日摸天天摸爽爽狠狠97 | 日本www一道久久久免费榴莲 | 亚洲精品国偷拍自产在线观看蜜桃 | 国产综合在线观看 | 四虎永久在线精品免费网址 | 丝袜美腿亚洲一区二区 | 亚洲另类伦春色综合小说 | 国产成人精品久久亚洲高清不卡 | 精品国产精品久久一区免费式 | 午夜福利不卡在线视频 | 亚洲精品中文字幕久久久久 | 婷婷六月久久综合丁香 | 蜜桃无码一区二区三区 | 久久精品女人天堂av免费观看 | 日韩亚洲欧美中文高清在线 | 成年美女黄网站色大免费全看 | 亚洲aⅴ无码成人网站国产app | 久久精品中文字幕大胸 | 亚洲熟女一区二区三区 | 无码福利日韩神码福利片 | 亚洲综合伊人久久大杳蕉 | 日本va欧美va欧美va精品 | 狠狠综合久久久久综合网 | 在线精品亚洲一区二区 | 欧美精品在线观看 | 日本xxxx色视频在线观看免费 | 中文字幕日韩精品一区二区三区 | 曰韩少妇内射免费播放 | 色婷婷av一区二区三区之红樱桃 | 精品久久久无码中文字幕 | 无码人妻久久一区二区三区不卡 | 久久综合香蕉国产蜜臀av | 在线观看欧美一区二区三区 | 中文字幕无码免费久久9一区9 | 性生交大片免费看l | 亚洲日韩av一区二区三区四区 | 亚洲色偷偷男人的天堂 | 国产精品多人p群无码 | 少妇太爽了在线观看 | 四虎影视成人永久免费观看视频 | 国产成人无码区免费内射一片色欲 | a国产一区二区免费入口 | 丰满人妻翻云覆雨呻吟视频 | 中文字幕乱码人妻无码久久 | 亚洲色欲色欲天天天www | 熟女俱乐部五十路六十路av | 成熟女人特级毛片www免费 | 国产又粗又硬又大爽黄老大爷视 | 对白脏话肉麻粗话av | 4hu四虎永久在线观看 | 国产成人无码区免费内射一片色欲 | 久久国产精品_国产精品 | 亚洲阿v天堂在线 | 日韩av无码中文无码电影 | 性欧美熟妇videofreesex | 久久久精品国产sm最大网站 | 久久精品中文闷骚内射 | 特黄特色大片免费播放器图片 | 精品国产乱码久久久久乱码 | 国产三级精品三级男人的天堂 | 色欲av亚洲一区无码少妇 | 给我免费的视频在线观看 | 野狼第一精品社区 | 人人妻人人澡人人爽欧美精品 | 狂野欧美性猛xxxx乱大交 | 免费网站看v片在线18禁无码 | 久久亚洲精品成人无码 | 老太婆性杂交欧美肥老太 | 7777奇米四色成人眼影 | 国产乱人伦偷精品视频 | 牲欲强的熟妇农村老妇女 | 无码国产乱人伦偷精品视频 | 久久久久人妻一区精品色欧美 | 麻豆av传媒蜜桃天美传媒 | 日本一区二区更新不卡 | 国产熟妇高潮叫床视频播放 | 人妻少妇被猛烈进入中文字幕 | 国产精品高潮呻吟av久久4虎 | 成熟人妻av无码专区 | 国产精品久久久久无码av色戒 | 国产美女极度色诱视频www | 成人性做爰aaa片免费看不忠 | 中文字幕无码免费久久99 | 国产美女极度色诱视频www | 欧美成人高清在线播放 | 无码人妻av免费一区二区三区 | www国产亚洲精品久久久日本 | 性生交大片免费看女人按摩摩 | 国产乱子伦视频在线播放 | 日日噜噜噜噜夜夜爽亚洲精品 | 亚洲精品一区二区三区婷婷月 | 精品国偷自产在线视频 | 少妇无码一区二区二三区 | 亚洲精品鲁一鲁一区二区三区 | 亚洲一区二区三区无码久久 | 国产人妻精品一区二区三区 | 亚洲精品一区国产 | 亚洲国产精品久久久久久 | 老子影院午夜伦不卡 | 精品水蜜桃久久久久久久 | 国产激情综合五月久久 | 未满小14洗澡无码视频网站 | 国产精品无码一区二区桃花视频 | 国产精品理论片在线观看 | 久久精品人妻少妇一区二区三区 | 精品 日韩 国产 欧美 视频 | 国产无遮挡又黄又爽又色 | 性生交大片免费看l | 亚洲一区av无码专区在线观看 | 亚洲精品国偷拍自产在线麻豆 | 激情综合激情五月俺也去 | 成人亚洲精品久久久久软件 | 亚洲日韩av一区二区三区四区 | 高中生自慰www网站 | 亚洲国产欧美国产综合一区 | 爽爽影院免费观看 | 亚洲精品综合一区二区三区在线 | 少妇人妻偷人精品无码视频 | 欧美日韩人成综合在线播放 | 最近的中文字幕在线看视频 | 夜夜躁日日躁狠狠久久av | 亚洲大尺度无码无码专区 | 国产欧美精品一区二区三区 | 亚洲国产午夜精品理论片 | 图片区 小说区 区 亚洲五月 | 人妻无码αv中文字幕久久琪琪布 | 无码一区二区三区在线 | 又黄又爽又色的视频 | 色老头在线一区二区三区 | 久久这里只有精品视频9 | 成人片黄网站色大片免费观看 | www一区二区www免费 | 狂野欧美性猛xxxx乱大交 | 精品国产一区av天美传媒 | 日本一区二区三区免费播放 | 免费观看的无遮挡av | 无码国产色欲xxxxx视频 | 综合激情五月综合激情五月激情1 | 久久久久久九九精品久 | 久久久婷婷五月亚洲97号色 | 国产精品资源一区二区 | 人人爽人人澡人人高潮 | 日韩欧美群交p片內射中文 | 妺妺窝人体色www在线小说 | 水蜜桃av无码 | 精品日本一区二区三区在线观看 | 51国偷自产一区二区三区 | 奇米影视888欧美在线观看 | 国産精品久久久久久久 | 国产欧美精品一区二区三区 | 激情综合激情五月俺也去 | 午夜熟女插插xx免费视频 | 日日摸日日碰夜夜爽av | 国产乱人无码伦av在线a | 老司机亚洲精品影院 | 午夜理论片yy44880影院 | 亚洲国产日韩a在线播放 | 日韩精品一区二区av在线 | 天下第一社区视频www日本 | √天堂资源地址中文在线 | 国产在线aaa片一区二区99 | 欧美熟妇另类久久久久久不卡 | 一本无码人妻在中文字幕免费 | 蜜臀aⅴ国产精品久久久国产老师 | 亚洲精品无码国产 | 午夜福利试看120秒体验区 | 成人一在线视频日韩国产 | 亚洲天堂2017无码中文 | 人人澡人人妻人人爽人人蜜桃 | 国产乱人偷精品人妻a片 | 欧美国产日产一区二区 | 巨爆乳无码视频在线观看 | 影音先锋中文字幕无码 | 中文字幕av伊人av无码av | 51国偷自产一区二区三区 | 精品人人妻人人澡人人爽人人 | 国产另类ts人妖一区二区 | www国产精品内射老师 | 欧美国产日韩亚洲中文 | 色妞www精品免费视频 | 国产免费无码一区二区视频 | 永久黄网站色视频免费直播 | 国产激情无码一区二区app | 欧美黑人性暴力猛交喷水 | 国产精品久久久午夜夜伦鲁鲁 | 久久精品女人的天堂av | 国产片av国语在线观看 | 蜜桃视频韩日免费播放 | 青春草在线视频免费观看 | 亚洲男女内射在线播放 | 欧美人与禽zoz0性伦交 | 日韩人妻少妇一区二区三区 | 国产亚洲精品久久久久久 | 国产极品美女高潮无套在线观看 | 国产精品无码一区二区桃花视频 | 亚洲欧美综合区丁香五月小说 | 欧洲极品少妇 | 国产成人无码一二三区视频 | 少妇久久久久久人妻无码 | 波多野结衣高清一区二区三区 | 动漫av一区二区在线观看 | 日本爽爽爽爽爽爽在线观看免 | 亚洲欧美综合区丁香五月小说 | 丰满人妻一区二区三区免费视频 | 国产精品久久久久无码av色戒 | 亚洲日韩中文字幕在线播放 | 成年美女黄网站色大免费视频 | 中文字幕中文有码在线 | 国产乱人伦av在线无码 | 欧美肥老太牲交大战 | 中文字幕无码av波多野吉衣 | 婷婷六月久久综合丁香 | 国产成人精品视频ⅴa片软件竹菊 | 日本一区二区三区免费播放 | 人人爽人人澡人人高潮 | 无码成人精品区在线观看 | 亚洲精品国产品国语在线观看 | 亚洲色欲久久久综合网东京热 | 久久精品国产一区二区三区 | 中文精品无码中文字幕无码专区 | 一区二区三区乱码在线 | 欧洲 | 国产欧美精品一区二区三区 | 永久免费观看国产裸体美女 | 日本肉体xxxx裸交 | 人妻互换免费中文字幕 | 99久久婷婷国产综合精品青草免费 | 老太婆性杂交欧美肥老太 | 国产成人精品一区二区在线小狼 | 国产人妻精品午夜福利免费 | 国产精品亚洲lv粉色 | 97精品人妻一区二区三区香蕉 | 成熟女人特级毛片www免费 | 国内精品久久毛片一区二区 | 国产无遮挡吃胸膜奶免费看 | 久久亚洲精品成人无码 | 色情久久久av熟女人妻网站 | 性史性农村dvd毛片 | 国产无遮挡又黄又爽免费视频 | www一区二区www免费 | 色窝窝无码一区二区三区色欲 | 无套内谢老熟女 | 色窝窝无码一区二区三区色欲 | 国产麻豆精品一区二区三区v视界 | 中文字幕 亚洲精品 第1页 | 精品国产乱码久久久久乱码 | 久久精品国产日本波多野结衣 | 国产人妻人伦精品1国产丝袜 | 一本色道婷婷久久欧美 | 精品久久久久久人妻无码中文字幕 | 国产舌乚八伦偷品w中 | 国产精品久久久久无码av色戒 | 亚洲自偷自偷在线制服 | 在线看片无码永久免费视频 | 色婷婷欧美在线播放内射 | 在线精品国产一区二区三区 | 99国产欧美久久久精品 | 精品国产一区av天美传媒 | 国产亲子乱弄免费视频 | 人妻互换免费中文字幕 | 亚洲男人av香蕉爽爽爽爽 | 搡女人真爽免费视频大全 | 人妻少妇精品无码专区二区 | 成人无码精品一区二区三区 | 日本熟妇浓毛 | 亚洲热妇无码av在线播放 | 天天av天天av天天透 | 中文字幕无码人妻少妇免费 | 精品偷拍一区二区三区在线看 | 综合激情五月综合激情五月激情1 | 国产农村乱对白刺激视频 | 岛国片人妻三上悠亚 | 久9re热视频这里只有精品 | 人妻熟女一区 | 综合人妻久久一区二区精品 | 精品国产乱码久久久久乱码 | 老头边吃奶边弄进去呻吟 | 青青青爽视频在线观看 | 亚洲精品久久久久久久久久久 | 久久久精品国产sm最大网站 | 国产精品亚洲五月天高清 | 一本精品99久久精品77 | 婷婷色婷婷开心五月四房播播 | 四虎国产精品免费久久 | 在线欧美精品一区二区三区 | 无码吃奶揉捏奶头高潮视频 | 亚洲国产av美女网站 | 欧美日本精品一区二区三区 | 国产精品嫩草久久久久 | 婷婷综合久久中文字幕蜜桃三电影 | 久久国产精品精品国产色婷婷 | 人人澡人人妻人人爽人人蜜桃 | 黑森林福利视频导航 | 男人的天堂2018无码 | 在线播放免费人成毛片乱码 | 天堂一区人妻无码 | 亚洲自偷精品视频自拍 | 亚洲精品无码国产 | 伊人久久婷婷五月综合97色 | 日本肉体xxxx裸交 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 鲁一鲁av2019在线 | 色欲av亚洲一区无码少妇 | 久久精品中文闷骚内射 | 亚洲日韩av一区二区三区中文 | 熟女体下毛毛黑森林 | 免费人成在线观看网站 | 中文字幕无码免费久久9一区9 | 国内精品久久毛片一区二区 | 性啪啪chinese东北女人 | 小鲜肉自慰网站xnxx | 牛和人交xxxx欧美 | 国产精品鲁鲁鲁 | 丁香啪啪综合成人亚洲 | 久久国语露脸国产精品电影 | 国产精品永久免费视频 | 亚洲性无码av中文字幕 | 国产精品高潮呻吟av久久 | 日韩视频 中文字幕 视频一区 | 久久久久se色偷偷亚洲精品av | 大胆欧美熟妇xx | 999久久久国产精品消防器材 | 国产成人一区二区三区别 | 精品人妻人人做人人爽 | 少妇一晚三次一区二区三区 | 麻豆果冻传媒2021精品传媒一区下载 | 亚洲人成影院在线无码按摩店 | 亚洲区欧美区综合区自拍区 | 在线 国产 欧美 亚洲 天堂 | 国产一区二区不卡老阿姨 | 国产熟女一区二区三区四区五区 | 日日麻批免费40分钟无码 | 亚洲国产欧美日韩精品一区二区三区 | 国产免费无码一区二区视频 | 亚洲国产欧美在线成人 | 日韩少妇白浆无码系列 | 国内少妇偷人精品视频免费 | 高中生自慰www网站 | 福利一区二区三区视频在线观看 | 成人欧美一区二区三区黑人免费 | 久久综合色之久久综合 | 性啪啪chinese东北女人 | 国产成人无码专区 | 国产在线无码精品电影网 | 亚洲精品一区二区三区大桥未久 | 久久人人爽人人爽人人片av高清 | 欧美xxxx黑人又粗又长 | 久久精品国产99精品亚洲 | 亚洲成a人片在线观看无码3d | 欧美熟妇另类久久久久久多毛 | av在线亚洲欧洲日产一区二区 | 女高中生第一次破苞av | yw尤物av无码国产在线观看 | 婷婷六月久久综合丁香 | 亚洲欧洲日本综合aⅴ在线 | 欧美性生交xxxxx久久久 | 精品国产一区二区三区四区 | 亚洲天堂2017无码中文 | 99久久人妻精品免费一区 | 内射后入在线观看一区 | 中文字幕人妻无码一夲道 | 亚洲男人av天堂午夜在 | 牲交欧美兽交欧美 | 中文字幕 人妻熟女 | 免费看男女做好爽好硬视频 | 日韩无套无码精品 | 色婷婷欧美在线播放内射 | 欧美精品一区二区精品久久 | 日韩av无码中文无码电影 | 亚洲综合精品香蕉久久网 | 少妇人妻大乳在线视频 | 少妇激情av一区二区 | 麻豆蜜桃av蜜臀av色欲av | 无码精品人妻一区二区三区av | 高清国产亚洲精品自在久久 | 久久精品中文字幕一区 | 久久久精品国产sm最大网站 | 老熟女乱子伦 | 伊人久久大香线蕉午夜 | 成人aaa片一区国产精品 | 无码精品人妻一区二区三区av | 国产后入清纯学生妹 | 国内精品九九久久久精品 | 国产精品亚洲综合色区韩国 | 久久久久人妻一区精品色欧美 | 亚洲呦女专区 | 老子影院午夜精品无码 | 亚洲人成影院在线观看 | 人人妻在人人 | 伊人久久大香线蕉av一区二区 | 亚欧洲精品在线视频免费观看 | 久久国产精品_国产精品 | 国产网红无码精品视频 | 又湿又紧又大又爽a视频国产 | 日日摸天天摸爽爽狠狠97 | 丰满人妻一区二区三区免费视频 | 性欧美疯狂xxxxbbbb | 亚洲精品欧美二区三区中文字幕 | 老太婆性杂交欧美肥老太 | 鲁一鲁av2019在线 | 亚洲熟妇色xxxxx亚洲 | 国产精品无码一区二区三区不卡 | 国产午夜无码精品免费看 | 国产乱码精品一品二品 | 国产美女极度色诱视频www | 欧美成人午夜精品久久久 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国产亚洲精品久久久久久国模美 | 妺妺窝人体色www在线小说 | 无码精品国产va在线观看dvd | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 丰满人妻精品国产99aⅴ | 亚洲 a v无 码免 费 成 人 a v | 国产人妻大战黑人第1集 | 免费人成在线视频无码 | 女人被爽到呻吟gif动态图视看 | 丰满护士巨好爽好大乳 | 日本欧美一区二区三区乱码 | 日欧一片内射va在线影院 | 伊人久久婷婷五月综合97色 | 国产美女精品一区二区三区 | 日韩人妻无码一区二区三区久久99 | 日韩人妻无码一区二区三区久久99 | 中文字幕无码日韩专区 | 三上悠亚人妻中文字幕在线 | 精品一二三区久久aaa片 | 精品日本一区二区三区在线观看 | 久久久久久久久蜜桃 | 欧洲欧美人成视频在线 | 丰满岳乱妇在线观看中字无码 | 日本熟妇人妻xxxxx人hd | 真人与拘做受免费视频 | 1000部啪啪未满十八勿入下载 | 亚洲一区二区观看播放 | aⅴ亚洲 日韩 色 图网站 播放 | 久久精品人人做人人综合试看 | 97无码免费人妻超级碰碰夜夜 | 国产婷婷色一区二区三区在线 | 久久久久免费精品国产 | 无套内谢的新婚少妇国语播放 | 亚洲一区二区三区四区 | 亚洲熟妇色xxxxx欧美老妇y | 国产综合色产在线精品 | 99久久久无码国产精品免费 | aⅴ在线视频男人的天堂 | 狠狠色色综合网站 | 色老头在线一区二区三区 | 亚洲中文字幕在线观看 | 天堂亚洲免费视频 | 97久久国产亚洲精品超碰热 | 呦交小u女精品视频 | 亚洲欧美日韩成人高清在线一区 | 亚洲精品国产a久久久久久 | 欧美 丝袜 自拍 制服 另类 | 狠狠cao日日穞夜夜穞av | 成人片黄网站色大片免费观看 | 国精品人妻无码一区二区三区蜜柚 | 久久99久久99精品中文字幕 | 麻豆成人精品国产免费 | 欧美精品国产综合久久 | 午夜肉伦伦影院 | 国产激情综合五月久久 | 亚洲国产成人av在线观看 | 亚洲人亚洲人成电影网站色 | 久久99精品久久久久久动态图 | 2019午夜福利不卡片在线 | 精品无码成人片一区二区98 | 精品久久久无码中文字幕 | 亚洲中文字幕无码中文字在线 | 国产午夜福利亚洲第一 | 欧美亚洲日韩国产人成在线播放 | 丰腴饱满的极品熟妇 | 免费人成在线视频无码 | 久久午夜无码鲁丝片 | 日本成熟视频免费视频 | 性做久久久久久久久 | 天天av天天av天天透 | 久久久久久亚洲精品a片成人 | 亚洲综合久久一区二区 | 亚洲国产av精品一区二区蜜芽 | 久久综合九色综合97网 | 西西人体www44rt大胆高清 | 99久久久无码国产aaa精品 | 对白脏话肉麻粗话av | 精品无码成人片一区二区98 | 久久 国产 尿 小便 嘘嘘 | 人人妻人人藻人人爽欧美一区 | 在线 国产 欧美 亚洲 天堂 | 亚洲а∨天堂久久精品2021 | 亚洲精品久久久久久一区二区 | 国内精品人妻无码久久久影院 | 欧美午夜特黄aaaaaa片 | 国产乱人偷精品人妻a片 | 呦交小u女精品视频 | 自拍偷自拍亚洲精品被多人伦好爽 | 国产在线精品一区二区三区直播 | 天海翼激烈高潮到腰振不止 | 久久久亚洲欧洲日产国码αv | 未满小14洗澡无码视频网站 | 理论片87福利理论电影 | 国产国语老龄妇女a片 | 亚洲中文字幕av在天堂 | 国产激情无码一区二区app | 伊人色综合久久天天小片 | 色婷婷欧美在线播放内射 | 国产精品久久久久无码av色戒 | 亚洲精品无码国产 | 婷婷五月综合激情中文字幕 | 国产精品人人爽人人做我的可爱 | 小sao货水好多真紧h无码视频 | 久久久精品国产sm最大网站 | 玩弄人妻少妇500系列视频 | 97se亚洲精品一区 | 国内老熟妇对白xxxxhd | 老太婆性杂交欧美肥老太 | 高清国产亚洲精品自在久久 | 久热国产vs视频在线观看 | 久精品国产欧美亚洲色aⅴ大片 | 亚洲精品国偷拍自产在线麻豆 | 亚洲中文字幕va福利 | 成熟女人特级毛片www免费 | 亚洲日本va中文字幕 | 精品成人av一区二区三区 | 久久无码人妻影院 | 久久亚洲精品中文字幕无男同 | 婷婷五月综合激情中文字幕 | 未满成年国产在线观看 | 国产成人精品一区二区在线小狼 | 岛国片人妻三上悠亚 | 亚洲一区二区三区四区 | 乱人伦人妻中文字幕无码久久网 | 国产猛烈高潮尖叫视频免费 | 亚洲精品中文字幕乱码 | 久久99久久99精品中文字幕 | 熟女少妇人妻中文字幕 | 超碰97人人做人人爱少妇 | 国产乱码精品一品二品 | 亚洲无人区一区二区三区 | 伊人久久大香线蕉午夜 | 草草网站影院白丝内射 | 日本xxxx色视频在线观看免费 | 精品久久久无码人妻字幂 | 国产成人无码午夜视频在线观看 | 18禁黄网站男男禁片免费观看 | 色窝窝无码一区二区三区色欲 | 久久精品无码一区二区三区 | 自拍偷自拍亚洲精品被多人伦好爽 | 国产麻豆精品一区二区三区v视界 | 国精品人妻无码一区二区三区蜜柚 | 熟女少妇在线视频播放 | 久久天天躁狠狠躁夜夜免费观看 | 东京无码熟妇人妻av在线网址 | 鲁鲁鲁爽爽爽在线视频观看 | 精品 日韩 国产 欧美 视频 | 在线观看国产一区二区三区 | 日本一卡2卡3卡四卡精品网站 | 成 人 网 站国产免费观看 | 亚洲最大成人网站 | 免费看男女做好爽好硬视频 | 伊人久久大香线蕉av一区二区 | 精品无码av一区二区三区 | 日日摸日日碰夜夜爽av | 无码国产乱人伦偷精品视频 | 成人欧美一区二区三区黑人 | 一本精品99久久精品77 | 国产精品理论片在线观看 | 成人精品视频一区二区 | 人人妻人人澡人人爽欧美一区 | 欧美 丝袜 自拍 制服 另类 | 国产成人综合美国十次 | 精品久久综合1区2区3区激情 | 日产精品99久久久久久 | 亚洲男人av天堂午夜在 | 黑人粗大猛烈进出高潮视频 | 蜜臀aⅴ国产精品久久久国产老师 | 欧美丰满老熟妇xxxxx性 | 色婷婷欧美在线播放内射 | 亚洲日韩中文字幕在线播放 | 久久久国产精品无码免费专区 | 亚洲成a人片在线观看无码 | 国产无套粉嫩白浆在线 | 精品国产国产综合精品 | 中文字幕无码视频专区 | 日韩人妻无码中文字幕视频 | 亚洲精品国产a久久久久久 | 日本护士xxxxhd少妇 | 领导边摸边吃奶边做爽在线观看 | 无套内谢的新婚少妇国语播放 | 亚洲中文字幕乱码av波多ji | 久久人人爽人人爽人人片av高清 | 强开小婷嫩苞又嫩又紧视频 | 亚洲色www成人永久网址 | 一个人看的视频www在线 | 精品无码国产一区二区三区av | 国产欧美熟妇另类久久久 | 欧美熟妇另类久久久久久多毛 | 国产免费久久久久久无码 | 亚洲国产精品美女久久久久 | 国产精品无码一区二区桃花视频 | 日欧一片内射va在线影院 | 国产成人无码一二三区视频 | 免费中文字幕日韩欧美 | 乌克兰少妇xxxx做受 | 中文毛片无遮挡高清免费 | 狂野欧美性猛xxxx乱大交 | 久久99精品国产麻豆 | 欧美性猛交内射兽交老熟妇 | 激情国产av做激情国产爱 | 又湿又紧又大又爽a视频国产 | 国产乱人伦av在线无码 | 国产精品久久久久久亚洲毛片 | 99久久99久久免费精品蜜桃 | 精品一区二区三区波多野结衣 | 人妻少妇被猛烈进入中文字幕 | 国产综合久久久久鬼色 | 中国女人内谢69xxxxxa片 | 国产亚洲人成在线播放 | 婷婷六月久久综合丁香 | 鲁一鲁av2019在线 | 装睡被陌生人摸出水好爽 | 国产精品99久久精品爆乳 | 欧美激情一区二区三区成人 | 亚洲日韩一区二区三区 | 丰满人妻被黑人猛烈进入 | 色偷偷av老熟女 久久精品人妻少妇一区二区三区 | 国产精品二区一区二区aⅴ污介绍 | 西西人体www44rt大胆高清 | 窝窝午夜理论片影院 | 国产97在线 | 亚洲 | 2020最新国产自产精品 | 高潮毛片无遮挡高清免费 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 在线欧美精品一区二区三区 | 免费人成网站视频在线观看 | 亚洲国产精品毛片av不卡在线 | 欧美35页视频在线观看 | 欧美精品国产综合久久 | 蜜桃av抽搐高潮一区二区 | 亚洲色无码一区二区三区 | 国产精品美女久久久久av爽李琼 | 国产成人午夜福利在线播放 | 国产精品对白交换视频 | 国产精品美女久久久 | 一本久道高清无码视频 | 人人爽人人澡人人人妻 | 丁香花在线影院观看在线播放 | 久久精品女人天堂av免费观看 | 亚无码乱人伦一区二区 | 精品偷拍一区二区三区在线看 | 国产精品久久久久久久影院 | 国产无套内射久久久国产 | 国产婷婷色一区二区三区在线 | 3d动漫精品啪啪一区二区中 | 无遮挡国产高潮视频免费观看 | 女人和拘做爰正片视频 | 一二三四在线观看免费视频 | 亚洲另类伦春色综合小说 | 国产人妻久久精品二区三区老狼 | 全黄性性激高免费视频 | 国产精品久久国产精品99 | 女人被男人躁得好爽免费视频 | 国产免费无码一区二区视频 | 日韩精品无码一区二区中文字幕 | 熟妇人妻中文av无码 | 麻豆精产国品 | 无码av免费一区二区三区试看 | 国产乱子伦视频在线播放 | 亚洲爆乳精品无码一区二区三区 | 国产高潮视频在线观看 | 夜精品a片一区二区三区无码白浆 | 国产成人无码午夜视频在线观看 | 丰满少妇熟乱xxxxx视频 | 精品一二三区久久aaa片 | 色偷偷人人澡人人爽人人模 | 亚洲春色在线视频 | 亚洲国产综合无码一区 | 人妻少妇精品无码专区动漫 | 在线天堂新版最新版在线8 | 理论片87福利理论电影 | 亚洲伊人久久精品影院 | 初尝人妻少妇中文字幕 | 国产特级毛片aaaaaa高潮流水 | 亚洲日本在线电影 | 久久天天躁狠狠躁夜夜免费观看 | 波多野结衣 黑人 | 伊人久久大香线焦av综合影院 | 精品久久久久久亚洲精品 | 亚洲日韩av一区二区三区中文 | 亚洲欧洲日本无在线码 | 曰韩少妇内射免费播放 | 色综合久久88色综合天天 | 国产97人人超碰caoprom | 亲嘴扒胸摸屁股激烈网站 | 亚洲人成影院在线观看 | 无码精品国产va在线观看dvd | 亚洲人成影院在线无码按摩店 | 夜夜夜高潮夜夜爽夜夜爰爰 | 亚洲成a人片在线观看日本 | 免费看少妇作爱视频 | 特级做a爰片毛片免费69 | 欧美成人家庭影院 | 精品欧洲av无码一区二区三区 | 国产亚洲欧美日韩亚洲中文色 | 国产精品.xx视频.xxtv | 国产莉萝无码av在线播放 | 欧美亚洲日韩国产人成在线播放 | 纯爱无遮挡h肉动漫在线播放 | 国产午夜福利100集发布 | 俺去俺来也www色官网 | 久久天天躁夜夜躁狠狠 | 国产真人无遮挡作爱免费视频 | 亚洲熟女一区二区三区 | 美女极度色诱视频国产 | 好爽又高潮了毛片免费下载 | 天天躁日日躁狠狠躁免费麻豆 | 色综合视频一区二区三区 | 国产欧美熟妇另类久久久 | 最近免费中文字幕中文高清百度 | 久久午夜无码鲁丝片午夜精品 | 色综合久久网 | 日日摸天天摸爽爽狠狠97 | 国产尤物精品视频 | 国产精品毛片一区二区 | 成人精品一区二区三区中文字幕 | 亚拍精品一区二区三区探花 | 蜜臀aⅴ国产精品久久久国产老师 | 日韩无套无码精品 | 亚洲精品国偷拍自产在线麻豆 | 精品国产一区av天美传媒 | 国产一区二区三区日韩精品 | 日产精品99久久久久久 | 无码国模国产在线观看 | 日本www一道久久久免费榴莲 | 女人被男人躁得好爽免费视频 | 捆绑白丝粉色jk震动捧喷白浆 | 最新版天堂资源中文官网 | 亚洲大尺度无码无码专区 | 免费人成在线观看网站 | 久久人人爽人人人人片 | 女人和拘做爰正片视频 | 亚洲综合色区中文字幕 | 免费人成网站视频在线观看 | 国产性生大片免费观看性 | 中文字幕无码人妻少妇免费 | 久久久久久国产精品无码下载 | 女人和拘做爰正片视频 | 久9re热视频这里只有精品 | 波多野结衣一区二区三区av免费 | 久久国产精品萌白酱免费 | 久久久久人妻一区精品色欧美 | 精品亚洲韩国一区二区三区 | 国产成人av免费观看 | 美女毛片一区二区三区四区 | 成人精品天堂一区二区三区 | 成 人影片 免费观看 | 亚洲中文字幕久久无码 | 欧美老妇交乱视频在线观看 | 西西人体www44rt大胆高清 | 精品国产aⅴ无码一区二区 | 欧美日韩一区二区综合 | 国产人妻精品午夜福利免费 | 玩弄中年熟妇正在播放 | 国产精品第一区揄拍无码 | 国内丰满熟女出轨videos | 中文字幕乱码人妻无码久久 | 娇妻被黑人粗大高潮白浆 | 少妇人妻偷人精品无码视频 | 国产国产精品人在线视 | 国产99久久精品一区二区 | 国色天香社区在线视频 | 久久无码中文字幕免费影院蜜桃 | 国产精品毛多多水多 | 在线播放免费人成毛片乱码 | 日本在线高清不卡免费播放 | 国产成人无码a区在线观看视频app | 成人影院yy111111在线观看 | 亚洲精品国偷拍自产在线麻豆 | 无码中文字幕色专区 | 免费无码一区二区三区蜜桃大 | 国产色视频一区二区三区 | 精品一二三区久久aaa片 | 正在播放老肥熟妇露脸 | 中文字幕 亚洲精品 第1页 | 精品厕所偷拍各类美女tp嘘嘘 | 秋霞成人午夜鲁丝一区二区三区 | 日本爽爽爽爽爽爽在线观看免 | 国产亚洲精品久久久久久 | 色综合视频一区二区三区 | 国产办公室秘书无码精品99 | 色妞www精品免费视频 | 国产片av国语在线观看 | 欧美 日韩 亚洲 在线 | 网友自拍区视频精品 | 亚洲一区二区三区播放 | 乱码午夜-极国产极内射 | 动漫av网站免费观看 | 娇妻被黑人粗大高潮白浆 | 国产一区二区不卡老阿姨 | 中文字幕无线码免费人妻 | 久久久久久久人妻无码中文字幕爆 | 精品乱子伦一区二区三区 | 奇米影视7777久久精品人人爽 | av人摸人人人澡人人超碰下载 | 免费无码一区二区三区蜜桃大 | 内射白嫩少妇超碰 | 国产精品久久久一区二区三区 | 真人与拘做受免费视频一 | 性欧美熟妇videofreesex | 7777奇米四色成人眼影 | 亚洲中文字幕久久无码 | 日韩精品久久久肉伦网站 | 精品欧洲av无码一区二区三区 | 精品国产福利一区二区 | 亚洲色欲色欲天天天www | 大地资源中文第3页 | 亚洲一区二区三区 | 天天摸天天透天天添 | 无码免费一区二区三区 | 久久久久久久久蜜桃 | 精品久久8x国产免费观看 | 性欧美videos高清精品 | 人人妻人人澡人人爽人人精品 | 欧美自拍另类欧美综合图片区 | 日本高清一区免费中文视频 | 日本一区二区三区免费播放 | 亚洲精品国产第一综合99久久 | 在线观看国产午夜福利片 | 欧美老人巨大xxxx做受 | www国产亚洲精品久久久日本 | 亚洲 高清 成人 动漫 | 丰满人妻一区二区三区免费视频 | 麻豆国产人妻欲求不满谁演的 | 噜噜噜亚洲色成人网站 | 又大又黄又粗又爽的免费视频 | 2019nv天堂香蕉在线观看 | 国产精品第一国产精品 | 青青青爽视频在线观看 | 国产偷抇久久精品a片69 | 国产无遮挡又黄又爽免费视频 | 国产成人人人97超碰超爽8 | 国产精品人妻一区二区三区四 | 国产成人无码区免费内射一片色欲 | 久久99精品久久久久婷婷 | 午夜肉伦伦影院 | 日本护士xxxxhd少妇 | 精品成在人线av无码免费看 | 黑人巨大精品欧美黑寡妇 | 少妇人妻偷人精品无码视频 | 国产无遮挡又黄又爽又色 | 人人妻在人人 | 鲁大师影院在线观看 | 亚洲中文字幕乱码av波多ji | 亚欧洲精品在线视频免费观看 | 日欧一片内射va在线影院 | 日本va欧美va欧美va精品 | 国产乱人无码伦av在线a | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 日韩视频 中文字幕 视频一区 | 欧美乱妇无乱码大黄a片 | 一本无码人妻在中文字幕免费 | 日日躁夜夜躁狠狠躁 | 免费看男女做好爽好硬视频 | 亚洲国产精品一区二区美利坚 | 男人扒开女人内裤强吻桶进去 | 无码毛片视频一区二区本码 | 精品日本一区二区三区在线观看 | 高清国产亚洲精品自在久久 | 骚片av蜜桃精品一区 | 免费无码肉片在线观看 | 国精产品一品二品国精品69xx | 特黄特色大片免费播放器图片 | 熟女少妇在线视频播放 | 欧美亚洲日韩国产人成在线播放 | 精品久久久久久亚洲精品 | 女人被爽到呻吟gif动态图视看 | 波多野结衣aⅴ在线 | 国产后入清纯学生妹 | 大肉大捧一进一出视频出来呀 | 女人被男人爽到呻吟的视频 | 亚洲自偷精品视频自拍 | 纯爱无遮挡h肉动漫在线播放 | 欧美自拍另类欧美综合图片区 | 久久久久se色偷偷亚洲精品av | 国产免费无码一区二区视频 | 国产熟妇另类久久久久 | 日韩 欧美 动漫 国产 制服 | 99re在线播放 | 日韩精品a片一区二区三区妖精 | 无人区乱码一区二区三区 | 国产乱人无码伦av在线a | 亚洲精品午夜国产va久久成人 | 久久精品人人做人人综合试看 | 四十如虎的丰满熟妇啪啪 | 亚洲国产高清在线观看视频 | 日本护士xxxxhd少妇 | 强开小婷嫩苞又嫩又紧视频 | 亚洲毛片av日韩av无码 | 色欲综合久久中文字幕网 | 乱人伦人妻中文字幕无码久久网 | 动漫av一区二区在线观看 | 国产精品久久久久无码av色戒 | 国色天香社区在线视频 | 国产艳妇av在线观看果冻传媒 | 国产在线一区二区三区四区五区 | 国产成人无码午夜视频在线观看 | 精品国产青草久久久久福利 | 久久综合色之久久综合 | 乌克兰少妇性做爰 | 一个人免费观看的www视频 | 18黄暴禁片在线观看 | 男人的天堂2018无码 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国产suv精品一区二区五 | 亚洲人交乣女bbw | 国产 精品 自在自线 | 男女超爽视频免费播放 | 亚洲日韩av片在线观看 | 中文字幕人妻无码一区二区三区 | 亚洲午夜久久久影院 | 国产乱子伦视频在线播放 | 久久国产自偷自偷免费一区调 | 荫蒂添的好舒服视频囗交 | 亚洲精品中文字幕乱码 | 国产精品亚洲五月天高清 | 人妻无码久久精品人妻 | 中文精品久久久久人妻不卡 | 国产精品久久久久久亚洲影视内衣 | 久久zyz资源站无码中文动漫 | 免费人成网站视频在线观看 | 成人免费视频在线观看 | 在线观看国产一区二区三区 | 国产一区二区三区日韩精品 | 亚洲国产精品无码久久久久高潮 | 亚洲の无码国产の无码影院 | 欧美性猛交内射兽交老熟妇 | 国产人成高清在线视频99最全资源 | 色综合久久中文娱乐网 | 骚片av蜜桃精品一区 | 亚洲中文字幕无码一久久区 | 国产无av码在线观看 | 亚洲欧美综合区丁香五月小说 | 一本大道久久东京热无码av | 美女毛片一区二区三区四区 | 日韩精品久久久肉伦网站 | 激情国产av做激情国产爱 | 亚洲天堂2017无码 | 激情内射日本一区二区三区 | 日韩 欧美 动漫 国产 制服 | 国产成人无码一二三区视频 | 丰满少妇人妻久久久久久 | 久久精品99久久香蕉国产色戒 | 98国产精品综合一区二区三区 | 欧美真人作爱免费视频 | 欧美xxxxx精品 | 初尝人妻少妇中文字幕 | 国产亚洲人成a在线v网站 | 东京一本一道一二三区 | 国产成人无码专区 | 福利一区二区三区视频在线观看 | 国产亚洲精品久久久久久久 | 曰本女人与公拘交酡免费视频 | 色五月丁香五月综合五月 | 久久久中文久久久无码 | 人妻少妇精品视频专区 | 国产精品99久久精品爆乳 | 成人无码视频免费播放 | 综合激情五月综合激情五月激情1 | 亚洲成熟女人毛毛耸耸多 | 狠狠色噜噜狠狠狠7777奇米 | 久久天天躁狠狠躁夜夜免费观看 | 无码国产乱人伦偷精品视频 | 日本精品少妇一区二区三区 | 亚洲熟女一区二区三区 | 夜先锋av资源网站 | 精品无码一区二区三区的天堂 | 丰满少妇弄高潮了www | 国产区女主播在线观看 | 丰满妇女强制高潮18xxxx | 国产片av国语在线观看 | 99视频精品全部免费免费观看 | 国产精品无码久久av | 精品无人国产偷自产在线 | 我要看www免费看插插视频 | 亚洲日韩av一区二区三区中文 | 国产成人亚洲综合无码 | 亚洲 a v无 码免 费 成 人 a v | 久在线观看福利视频 | 色综合久久88色综合天天 | 久久精品中文闷骚内射 | 女人被爽到呻吟gif动态图视看 | 漂亮人妻洗澡被公强 日日躁 | 国产成人无码av在线影院 | 久久综合激激的五月天 | 亚洲中文字幕无码一久久区 | 亚洲 欧美 激情 小说 另类 | 九一九色国产 | 久久久久久av无码免费看大片 | 六十路熟妇乱子伦 | 欧美刺激性大交 | 亚洲成a人一区二区三区 | 乱人伦人妻中文字幕无码久久网 | 强开小婷嫩苞又嫩又紧视频 | 国产综合久久久久鬼色 | 亚洲国产成人a精品不卡在线 | 欧洲熟妇精品视频 | 国产绳艺sm调教室论坛 | 国产精品鲁鲁鲁 | 国产成人亚洲综合无码 | 国精品人妻无码一区二区三区蜜柚 | 麻豆md0077饥渴少妇 | 欧美黑人性暴力猛交喷水 | 午夜不卡av免费 一本久久a久久精品vr综合 | 粗大的内捧猛烈进出视频 | 国产成人亚洲综合无码 | www成人国产高清内射 | 精品aⅴ一区二区三区 | 无遮挡啪啪摇乳动态图 | 午夜性刺激在线视频免费 | 亚洲人亚洲人成电影网站色 | 亚洲第一网站男人都懂 |