架构师-Redis(一)

参考资料
官网
源码
《Redis设计与实现》黄健宏
《Redis5设计与源码分析》 陈雷

背景

redis官网定义

诞生历程

08年意大利西西里岛的小伙子antirez创建 了一个访客信息网站 LLOOGG.COM。记录需要知道网站的访问情况,比如访客的 IP、操作系统、浏览器、使用的搜索关键词、所在地区、访问的网页地址等等。

在 国内,有很多网站提供了这个功能,比如 CNZZ,百度统计,国外也有谷歌的 Google Analytics。

LLOOGG.COM 提供的就是这种功能,它可以查看最多 10000 条的最新浏览记录。 这样的话,它需要为每一个网站创建一个列表(List),不同网站的访问记录进入到不同 的列表。如果列表的长度超过了用户指定的长度,它需要把最早的记录删除(先进先出)当 LLOOGG.COM 的用户越来越多的时候,它需要维护的列表数量也越来越多,这 种记录最新的请求和删除最早的请求的操作也越来越多

最开始antirez最初使用的数据库是 MySQL,可想而知,因为每一次记录和删除都要读写磁盘,因为数据量和并发量 太大,在这种情况下无论怎么去优化数据库都不管用了。

考虑到最终限制数据库性能的瓶颈在于磁盘,所以 antirez 打算放弃磁盘,自己去实 现一个具有列表结构的数据库的原型,把数据放在内存而不是磁盘,这样可以大大地提 升列表的 push 和 pop 的效率。antirez 发现这种思路确实能解决这个问题,所以用 C 语 言重写了这个内存数据库,并且加上了持久化的功能,09 年,Redis 横空出世了。从最 开始只支持列表的数据库,到现在支持多种数据类型,并且提供了一系列的高级特性, Redis (Remote Dictionary Service)已经成为一个在全世界被广泛使用的开源项目

SQL与NoSQL

关系型数据库的特点:

  • 1、它以表格的形式,基于行存储数据,是一个二维的模式。
  • 2、它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应 表结构。
  • 3、表与表之间存在关联(Relationship)。
  • 4、大部分关系型数据库都支持 SQL(结构化查询语言)的操作,支持复杂的关联查 询。
  • 5、通过支持事务(ACID 酸)来提供严格或者实时的数据一致性。

关系型数据库的缺点:

  • 1、要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据的存储,就要扩 大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容。水平扩容需要复杂的技术来实 现,比如分库分表。
  • 2、表结构修改困难,因此存储的数据格式也受到限制。
  • 3、在高并发和高数据量的情况下,我们的关系型数据库通常会把数据持久化到磁盘, 基于磁盘的读写压力比较大
    非关系型数据库的特点:NoSQL
  • 1、存储非结构化的数据,比如文本、图片、音频、视频。
  • 2、表与表之间没有关联,可扩展性强。
  • 3、保证数据的最终一致性。遵循 BASE(碱)理论。 Basically Available(基本 可用); Soft-state(软状态); Eventually Consistent(最终一致性)。
  • 4、支持海量数据的存储和高并发的高效读写。 5、支持分布式,能够对数据进行分片存储,扩缩容简单

存储类型不同的NoSQL

  • 1、KV 存储,用 Key Value 的形式来存储数据。比较常见的有 Redis 和 MemcacheDB
  • 2、文档存储,MongoDB。
  • 3、列存储,HBase。
  • 4、图存储,这个图(Graph)是数据结构,不是文件格式。Neo4j。
  • 5、对象存储。
  • 6、XML 存储等等等等。

Redis特性

官网介绍 中文网站

硬件层面有 CPU 的缓存;浏览器也有缓存;手机的应用也有缓存。我们把数据缓存 起来的原因就是从原始位置取数据的代价太大了,放在一个临时位置存储起来,取回就 可以快一些

Redis is an open source (BSD licensed), in-memory data structure store, 
used as a database, cache and message broker. It supports data structures 
such as strings, hashes, lists, sets, sorted sets with range queries, 
bitmaps, hyperloglogs, geospatial indexes with radius queries and 
streams. Redis has built-in replication, Lua scripting, LRU eviction,
 transactions and different levels of on-disk persistence, and provides 
high availability via Redis Sentinel and automatic partitioning with 
Redis Cluster

Redis 的特性:

  • 1)开源基于内存数据结构存储,可以用作数据库,缓存,消息broker
  • 2)更丰富的数据类型
  • 3)进程内与跨进程;单机与分布式
  • 4)功能丰富:持久化机制、过期策略
  • 5)支持多种编程语言
  • 6)高可用,集群

安装

单机

源码编译安装

# 下载地址redis.io 安装到/usr/local/soft
cd /usr/local/soft/
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
# 解压
tar -zxvf redis-5.0.5.tar.gz
# 安装gcc
yum install gcc
# 编译安装
cd redis-5.0.5
make MALLOC=libc
# 将/usr/local/soft/redis-5.0.5/src目录下二进制文件安装到/usr/local/bin
cd src
make install
#修改配置文件/usr/local/soft/redis-5.0.5/redis.conf
#####################################################
daemonize yes # 后台启动
bind 0.0.0.0  # 将127.0.0.1改成0.0.0.0或注释 否则只能在本机访问
requirepass yourpassword # 需要密码登录
#####################################################
# 指定配置文件启动
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
# 进入客户端
/usr/local/soft/redis-5.0.5/src/redis-cli
# 停止redis
redis>shutdown
# 或
ps -aux|grep redis
kill -9 xxx
##########配置alias#############
vim /etc/bashrc
# 添加
alias redis-server='/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf'
alias redis-cli='/usr/local/soft/redis-5.0.5/src/redis-cli'
###############################

哨兵模式

##############Redis##Sentinel集群####################
#  3个Sentinel实例  3个Redis服务(1主2从) 
#  192.168.0.111  Master: 6379 / Sentinel 26379
#  192.168.0.112  Slave: 6379  / Sentinel 26379
#  192.168.0.113  Slave:6379   / Sentinel 26379
#################################################
#################Master-Slave配置################
# 在112和113的redis.conf中添加
slaveof 192.168.0.111 6379
##########Sentinel配置文件#######################
# 在111,112,113修改sentinel.conf
cd /usr/local/soft/redis-5.0.5
vim sentinel.conf
##########sentinel.conf###############
port 26379  
daemonize yes
logfile "/tmp/redis/redis-sentinel.log"
dir "/tmp/redis/"
sentinel monitor mymaster 192.168.0.111 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
######################################
# 在3台机器上分别启动Redis和Sentinel
cd /usr/local/soft/redis-5.0.5/src
./redis-server ../redis.conf
./redis-sentinel ../sentinel.conf
# 查看集群状态
redis>info replication
# 模拟master宕机 在111执行
redis>shutdwon

集群模式

##############Redis Cluster(3主3从)######################
#   192.168.0.111  7291(主) 7292(从)
#   192.168.0.112  7291(主) 7292(从)
#   192.168.0.113  7291(主) 7292(从)
###################################################
# 在3台机器上执行
cd /usr/loca/soft/redis-5.0.5
mkdir redis-cluster
mkdir redis-cluster/7291 redis-cluster/7292
# 将redis.conf复制到7291
cp redis.conf redis-cluster/7291/
# 修改7291下的redis.conf
#############################
port 7291
dir "/usr/local/soft/redis-5.0.5/redis-cluster/7291/"
cluster-enabled yes
cluster-config-file nodes-7291.conf
cluster-node-timeout 5000
appendonly yes
pidfile /var/run/redis_7291.pid
############7292####################
# 将7291的conf复制到7292
cd /usr/local/soft/redis-5.0.5/redis-cluster
cp 7291/redis.conf 7292/
sed -i 's/7291/7292/g' 7292/redis.conf
##############################
# 分别在三个机器上启动redis服务
redis-server redis-cluster/7291/redis.conf
redis-server redis-cluster/7292/redis.conf
# 查看是否启动成功
ps -ef|grep redis
#############创建集群#######
redis-cli --cluster create 192.168.0.111:7291 192.168.0.111:7292 192.168.0.112:7291 192.168.0.112:7292 192.168.0.113:7291 192.168.0.113:7292 --cluster-replicas 1
##########创建集群Redis会给出一个预计的方案 对6个节点分配3主3从如果没有问题输入yes确认########
[root@manager redis-5.0.7]# redis-cli --cluster create 192.168.0.111:7291 192.168.0.111:7292 192.168.0.112:7291 192.168.0.112:7292 192.168.0.113:7291 192.168.0.113:7292 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.0.112:7292 to 192.168.0.111:7291
Adding replica 192.168.0.113:7292 to 192.168.0.112:7291
Adding replica 192.168.0.111:7292 to 192.168.0.113:7291
M: 54eb1531986794c2ee24a8c03b7f32c4349e83f9 192.168.0.111:7291
   slots:[0-5460] (5461 slots) master
S: e933dfe1cd0a3540015f9baa408bbf880bac530b 192.168.0.111:7292
   replicates 21e822505ab6b8a4ca37cc4341c5f23b148d5f31
M: b03908bc3f2f40f42e1f3e9f327bee2d953a1184 192.168.0.112:7291
   slots:[5461-10922] (5462 slots) master
S: 218b42f728719b6df8111af421ac2102479fdbae 192.168.0.112:7292
   replicates 54eb1531986794c2ee24a8c03b7f32c4349e83f9
M: 21e822505ab6b8a4ca37cc4341c5f23b148d5f31 192.168.0.113:7291
   slots:[10923-16383] (5461 slots) master
S: 03accf01200b51551c318d01a60925375838964a 192.168.0.113:7292
   replicates b03908bc3f2f40f42e1f3e9f327bee2d953a1184
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
...
>>> Performing Cluster Check (using node 192.168.0.111:7291)
M: 54eb1531986794c2ee24a8c03b7f32c4349e83f9 192.168.0.111:7291
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 03accf01200b51551c318d01a60925375838964a 192.168.0.113:7292
   slots: (0 slots) slave
   replicates b03908bc3f2f40f42e1f3e9f327bee2d953a1184
S: 218b42f728719b6df8111af421ac2102479fdbae 192.168.0.112:7292
   slots: (0 slots) slave
   replicates 54eb1531986794c2ee24a8c03b7f32c4349e83f9
M: b03908bc3f2f40f42e1f3e9f327bee2d953a1184 192.168.0.112:7291
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: e933dfe1cd0a3540015f9baa408bbf880bac530b 192.168.0.111:7292
   slots: (0 slots) slave
   replicates 21e822505ab6b8a4ca37cc4341c5f23b148d5f31
M: 21e822505ab6b8a4ca37cc4341c5f23b148d5f31 192.168.0.113:7291
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
###############slot分布###############
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
#################重置集群#################
# 在每个节点上执行cluster reset,然后冲重新创建集群
cluster reset
# 连接客户端
redis-cli -p 7291
############批量写入值##############
cd /usr/local/soft/redis-5.0.5/redis-cluster/
vim setkey.sh
##################
#!/bin/bash
for ((i=0;i<20000;i++))
do
echo -en "helloworld" | redis-cli -h 192.168.0.111 -p 7291 -c -x set name$i >>redis.log
done
#####################################
chmod +x setkey.sh
./setkey.sh
#################
# 查看每个节点分布的数据
redis>dbsize
#############其他命令,如添加节点、删除节点、重新分布数据##########################
[root@worker02 7292]# redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN
                 --cluster-replicas <arg>
  check          host:port
                 --cluster-search-multiple-owners
  info           host:port
  fix            host:port
                 --cluster-search-multiple-owners
  reshard        host:port
                 --cluster-from <arg>
                 --cluster-to <arg>
                 --cluster-slots <arg>
                 --cluster-yes
                 --cluster-timeout <arg>
                 --cluster-pipeline <arg>
                 --cluster-replace
  rebalance      host:port
                 --cluster-weight <node1=w1...nodeN=wN>
                 --cluster-use-empty-masters
                 --cluster-timeout <arg>
                 --cluster-simulate
                 --cluster-pipeline <arg>
                 --cluster-threshold <arg>
                 --cluster-replace
  add-node       new_host:new_port existing_host:existing_port
                 --cluster-slave
                 --cluster-master-id <arg>
  del-node       host:port node_id
  call           host:port command arg arg .. arg
  set-timeout    host:port milliseconds
  import         host:port
                 --cluster-from <arg>
                 --cluster-copy
                 --cluster-replace
  help

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.
################集群命令###################
# 打印集群的信息
cluster info
# 列出集群当前已知的所有节点(node),以及这些节点的相关信息
cluster nodes 
# 将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子
cluster meet 
#从集群中移除 node_id 指定的节点(保证空槽道)
cluster forget <node_id> 
# 将当前节点设置为 node_id 指定的节点的从节点
cluster replicate <node_id> 
# 将节点的配置文件保存到硬盘里面
cluster saveconfig 
################槽slot命令###################
 # 将一个或多个槽(slot)指派(assign)给当前节点
cluster addslots [slot …]
# 移除一个或多个槽对当前节点的指派
cluster delslots [slot …] 
# 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点
cluster flushslots 
#将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,
# 那么先让另一个节点删除该槽>,然后再进行指派
cluster setslot node <node_id> 
# 将本节点的槽 slot 迁移到 node_id 指定的节点中
cluster setslot migrating <node_id> 
# 从 node_id 指定的节点中导入槽 slot 到本节点
cluster setslot importing <node_id> 
# 取消对槽 slot 的导入(import)或者迁移(migrate)
cluster setslot stable 
################键命令###################
# 计算键 key 应该被放置在哪个槽上
cluster keyslot 
# # 返回槽 slot 目前包含的键值对数量
cluster countkeysinslot 
# 返回 count 个 slot 槽中的键
cluster getkeysinslot 

Redis的数据类型

Redis中有5中基本对象类型(数据类型)String,Hash,Set,List,Zset
其他 BitMaps,HyperLogLog、Geo、Streams

redisDb

src/server.h

/* 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 */
    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 */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
# redis 基本操作
# 默认有16个库(0-15),可以在配置文件中修改,默认使用db0
database 16
# 切换数据库 redis中数据库没有完全隔离,不适合把不同的库分配给不同的业务
select 0
# 对换指定的两个数据库
swapdb 0 1
# 清空当前数据库
flushdb
# 清空所有的数据库 删库跑路~~
flushall
# 检查给定的key是否存在
exists key
# key 重命名  renamenx
rename key newKey
# 将当前数据库的key移动到给定的数据库db中 当key不存或在两个库中都存在,返回失败
move song 1 # 将song移动到db1 ,特性可以做锁(locking)原语(primitive)
# 删除key
del key [key2]
# 从当前数据库中随机返回(不删除)一个key 
randomkey
dbszie # 返回当前数据库的key数量
# 查看所有符合给定模式pattern的key
keys * ,keys h?ll0, keys h*llo,keys h[ae]llo
# 返回或保存给定列表、集合、有序集合key中及经过排序的元素
################sort###########################
lpush today_cost 30 1.5 10 8
sort today_cost # 按数字排序
sort today desc # 逆序
# alpha 对字符串排序
SORT website ALPHA
# limit 返回限制结果 offset 指定跳过的元素 count指定返回的个数 可以分页
# sort key limit offset count desc
rpush rank 1 3 5 7 9 
SORT rank LIMIT 0 5 
# by 外部健排序
SORT uid BY user_level_*
# get
SORT uid GET user_name_*
SORT uid by user_level_* GET user_name*
SORT uid get user_level_* GETuser_name*
# 具体参考 http://redisdoc.com/database/sort.html

###########################################
# SCAN 游标 
# SCAN 迭代当前数据库中的数据库健
SCAN 0 # 可以增量迭代
SCAN  0 MATCH test* COUNT 3 # 返回每个元素的数据库健
# SSCAN HSCAN ZSCAN  

redisObject

# 查看对象类型
type  key
# 查看对象编码
object encoding test
/** 
 * redis对象 src/server.h中
 * redis对象系统实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象时,
 * 这个对象所有占用的内存就会自动释放
 * redis还通过引用计数技术实现了对象共享机制
 **/
typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // LRU|LFU  记录对象最后一次命令访问的时间
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    // 引用计数                     
    int refcount;
    // 指向底层实现数据结构的指针
    void *ptr;
} robj;

对象类型

/* The actual Redis Object */
/**
 * 字符串对象  int,embstr,raw编码
 * embstr 只读 对它做任何修改,都会转成raw
*/
#define OBJ_STRING 0    /* String object. */
/**
 * 列表对象: ziplist,linkedlist编码
 * ziplist底层用压缩列表实现,每个节点保存一个元素  quicklist
 * linkedlist底层使用双端列表,每个双端列表节点都保存一个字符串对象,每个字符串对象保存一个列表元素
 * 配置参数: 
 *  list-max-ziplist-size -2
 *  list-compress-depth 0
 * **/
#define OBJ_LIST 1      /* List object. */
/**
 * 集合对象:
 * intset: 保存的元素都是整数值,元素数量不超过 512 
 * hashtable: key中保存集合元素,value设置为NULL
 * 转换参数: 
 *  set-max-intset-entries 512
 * **/
#define OBJ_SET 2       /* Set object. */
/**
 * 有序集合对象:
 * ziplist: 压缩列表,相邻两个节点保存,第一个节点保存元素成员,第二个元素保存分值
 * skiplist: 跳跃表zsl按分值从小到大保存所有集合元素
 * 转换参数:
 *   zset-max-ziplist-entries 128
 *   zset-max-ziplist-value 64
 * 
 * zset结构中的dict为有序集合创建了一个从成员到分值的映射
 * **/
#define OBJ_ZSET 3      /* Sorted set object. */
/**
 * 哈希对象
 * ziplist: 先将保存保存键的压缩列表节点推入压缩列表表尾,再讲保存值的压缩列表节点推入到压缩列表表尾
 * hashtable:使用字典作为底层实现
 * 转换参数: 
 *  hash-max-ziplist-entries 512
 *  hash-max-ziplist-value 64
 * **/
#define OBJ_HASH 4      /* Hash object. */

对象编码

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

类型和编码关系

类型 编码 对象
OBJ_STRING OBJ_ENCODING_INT 使用整数值实现的字符串对象(“int”)
OBJ_STRING OBJ_ENCODING_EMBSTR 使用embstr编码的简单动态字符串对象(“embstr”)
OBJ_STRING OBJ_ENCODING_RAW SDS实现的字符串对象(“raw”)
OBJ_LIST OBJ_ENCODING_ZIPLIST 使用压缩列表实现列表对象(“ziplist”)
OBJ_LIST OBJ_ENCODING_LINKEDLIST 使用双端列表实现列表对象(“linkedlist”)
OBJ_LIST OBJ_ENCODING_QUICKLIST 使用ziplist作为节点的双向链表
OBJ_HASH OBJ_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象"ziplist"
OBJ_HASH OBJ_ENCODING_HT 使用字典实现的哈希对象(“hashtable”)
OBJ_SET OBJ_ENCODING_INTSET 使用整数集合实现的集合对象(“intset”)
OBJ_SET OBJ_ENCODING_HT 使用字典的key实现的集合对象
OBJ_ZSET OBJ_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
OBJ_ZET OBJ_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

String字符串对象

命令

# 设置key  EX encods PX milliseconds NX 
SET key "value" 
SET key "value" EX 1000 NX
# 如果key不存在,才设置值 成功返回1,失败返回0
SETNX job "programmer" 
SETEX key "value" 1000 # 带过期时间 秒为单位
PSETEX key millseconds value # 毫秒为单位带过期时间
# 返回key的值
GET key
# 设置新值,返回旧值
GETSET key newValue
# 字符串长度
STRLEN key
# 追加字符串  编码会升级
APPEND key value
# 其他命令
SETRANGE key offset value
GETRANGE key start end
# 整数 自增
INCR key
INCRBY key increment
DECR key
DECRBY key increment
# 操作多个key
MSET,MSETNX,MSET,MGET 

Binary-safe strings 源码t_string.c封装操作string的命令实现

String应用场景

  • 缓存

    STRING 类型 例如:热点数据缓存(例如报表,明星出轨),对象缓存,全页缓存。 可以提升热点数据的访问速度

  • 数据共享分布式

    因为 Redis 是分布式的独立服务,可以在多个应用之间共享 例如:分布式 Session

    <dependency>
    	<groupId>org.springframework.session</groupId> 
    	<artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
  • 分布式锁

    STRING 类型 setnx 方法,只有不存在时才能添加成功,返回 true

  • 全局 ID

    INT 类型,INCRBY,利用原子性,分库分表的场景,一次性拿一段
    incrby userid 1000

  • 计数器

    INT 类型,INCR 方法 例如:文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到 数据库。

  • 限流

    INT 类型,INCR 方法 以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。

  • 位统计

    String 类型的 BITCOUNT。 字符是以 8 位二进制存储的
    因为 bit 非常节省空间(1 MB=8388608 bit),可以用来做大数据量的统计。 例如:在线用户统计,留存用户统计

    # 字符是以 8 位二进制存储的
    set k1 a 
    setbit k1 6 1 
    setbit k1 7 0 
    get k1
    # 支持按位与、按位或等等操作
    setbit onlineusers 0 1 
    setbit onlineusers 1 1 
    setbit onlineusers 2 0
    BITOP AND destkey key [key ...] #对一个或多个 key 求逻辑并,并将结果保存到 destkey 。 
    BITOP OR destkey key [key ...] #对一个或多个 key 求逻辑或,并将结果保存到 destkey 。 
    BITOP XOR destkey key [key ...] #对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
    BITOP NOT destkey key #对给定 对给定 key 求逻辑非,并将结果保存到 destkey
    # 计算出 7 天都在线的用户
    BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
    

存储类型SDS

sds.hsds.c

/**
 *  之前版本
 * typedef char *sds; // 类别名,用于指向sdshdr的buf属性
 * struct sdshdr {
 *      int len; // buf已占用的空间长度
 *      int free; // buf中剩余可用空间长度
 *      char buf[]; // 数据空间
 * };
 * /
struct __attribute__ ((__packed__)) sdshdr8 {
    // 有效字符串长度
    uint8_t len; /* used */
    // 分配的内存空间长度
    uint8_t alloc; /* excluding the header and null terminator(终结符号)) */
    // 低三位表示 sds类型
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

Redis 只使用C字符串作为字面量,其他情况用SDS作为字符串表示
相对C字符串,SDS优点:

  • 1 获取字符串长度时间复杂度为 O(1),因为定义了 len 属性
  • 2 不用担心内存溢出问题,如果需要会对 SDS 进行扩容
  • 3 通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多 次重分配内存
  • 4 二进制安全
  • 5 兼容部分C字符串函数

字符串类型的内部编码有三种:
1、int,存储 8 个字节的长整型(long,2^63-1)。
2、embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串), 存储小于 44 个字节的字符串。
3、raw,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)。

object.c文件中定义embstr大小#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

embstr 和 raw 的区别
embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw 需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。 因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次 空间,以及对象的所有数据连在一起,寻找方便。 而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。

int 和 embstr 什么时候转化为 raw
当 int 数 据 不 再 是 整 数 , 或 大 小 超 过 了 long 的 范 围时,自动转化为 embstr。

明明没有超过阈值,为什么变成 raw 了
对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先 转化为 raw 再进行修改。 因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节

当长度小于阈值时,会还原吗?
关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完 成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)

为什么要对底层的数据结构进行一层包装呢
通过封装,可以根据对象的类型动态地选择存储结构和可以使用的命令,实现节省 空间和优化查询速度

List列表对象

列表命令

##############实现栈和队列的命令##############
# 在列表头部[尾部]插入元素,并返回列表的总长度
lpush key value [value...]
# x  当列表key不存在时,啥也不做
lpushx key value
rpush key value [value...]
rpushx key value 
# 从列表头部[尾部]弹出元素
lpop key
lpopx key
rpop key
rpopx key
# 列表key为空,会阻塞客户端,有超时时间
blpop key [key...] timeout
brpop key [key...] timeout
# 从列表source尾部弹出,插入列表destintation头部,将返回客户端
rpoplpush source destination
brpoplpush source destionation timeout
###################获取列表的数据###############
# 获取索引为index的元素
lindex key index
# 获取指定索引范围的的元素
lrange key start end
# 获取列表长度
llen key
# 设置指定索引的元素值
lset key index value
# 插入元素 位于pivot之前或之后
linset key before|after pivot value
# 移除列表中与value相等的元素,并返回移除的元素个数
# count >0,从表尾删除count的元素;=0,删除所有与value相等的元素
# count <0,从表尾向表头搜索,删除“count的绝对值"个数据元素
lrem key count value
# 剪裁列表,只保留start与stop内的元素

应用场景
用户消息数据线,list是有序的
消息队列
List 提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间。
阻塞队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列。 栈:先进后出:rpush brpop

存储类ziplist压缩列表

ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一 个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能, 来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值 小的场景里面

压缩列表本质上就是一个字节数组,是Redis为节约内存设计的一种线性数据结构
源码ziplist.hziplist.c

/**
 * 压缩表 是列表键和哈希键的底层实现之一
 * 
 * 当一个列表键只包含少量的列表项,并且每个列表要么就是小整数值,要么就是长度比较短的字符串,
 * 那么Redis就会使用压缩表来做列表键的底层实现
 * 
 * 压缩列表是Redis为了节约内存而开发的,是有一些列特殊编码的连续内存块组成的顺序数据结构
 * 
 * {zlbytes}{zltail}{zllen}{entry1}{entry2}{...}{entryN}{zlend}
 * zlbytes记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重新分配,或计算zlend的位置使用
 * zltail 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节;通过这个偏移量,可以直接定位到表尾地址
 * zllen 压缩表节点数
 * entryX 列表节点
 * zlend 特殊值0xFF,用于标记压缩列表的末端
 * 
 * 压缩表节点组成部分
 * {previous_entry_length}{encoding}{content}
 * previous_entry_length:  记录压缩了列表前一个节点长度
 * encoding 记录节点属性content属性保存数据类型以及长度
 * content  属性值
 * 
 * e1,e2,e3,e4
 *
 * 压缩列表 连续内存 会产生连续更新
 * 添加一个元素和删除一个元素 会引起连锁更新,但这种的操作出现几率并不高
 * **/
/**
 * ======================
 *
 * The general layout of the ziplist is as follows:
 *
 *      ...  
 *
 * NOTE: all fields are stored in little endian, if not specified otherwise.
 *
 *  is an unsigned integer to hold the number of bytes that
 * the ziplist occupies, including the four bytes of the zlbytes field itself.
 * This value needs to be stored to be able to resize the entire structure
 * without the need to traverse it first.
 *
 *  is the offset to the last entry in the list. This allows
 * a pop operation on the far side of the list without the need for full
 * traversal.
 *
 *  is the number of entries. When there are more than
 * 2^16-2 entries, this value is set to 2^16-1 and we need to traverse the
 * entire list to know how many items it holds.
 *
 *  is a special entry representing the end of the ziplist.
 * Is encoded as a single byte equal to 255. No other normal entry starts
 * with a byte set to the value of 255.
 * ZIPLIST ENTRIES
 * ===============
 *
 * Every entry in the ziplist is prefixed by metadata that contains two pieces
 * of information. First, the length of the previous entry is stored to be
 * able to traverse the list from back to front. Second, the entry encoding is
 * provided. It represents the entry type, integer or string, and in the case
 * of strings it also represents the length of the string payload.
 * So a complete entry is stored like this:
 *
 *   
 *
 * Sometimes the encoding represents the entry itself, like for small integers
 * as we'll see later. In such a case the  part is missing, and we
 * could have just:
 *
 *  
 *
 * The length of the previous entry, , is encoded in the following way:
 * If this length is smaller than 254 bytes, it will only consume a single
 * byte representing the length as an unsinged 8 bit integer. When the length
 * is greater than or equal to 254, it will consume 5 bytes. The first byte is
 * set to 254 (FE) to indicate a larger value is following. The remaining 4
 * bytes take the length of the previous entry as value.
 *
 * So practically an entry is encoded in the following way:
 *
 *   
 */
//   
// ziplist 末端表示符,以及5个字节长长度标识符
/**
 * previous_entry_length的长度可以为1或者5字节,具体解释如下 
 *  1. 前一节点长度小于254字节,previous_entry_length长度为1字节,长度就保存在previous_entry_length中 
 * 2. 前一节点长度大于等于254字节,previous_entry_length长度为5,第一字节为0xFE,后面四个字节保存实际长度
 * **/
#define ZIP_END 255         /* Special "end of ziplist" entry. */
#define ZIP_BIG_PREVLEN 254 /* Max number of bytes of the previous entry, for
                               the "prevlen" field prefixing each entry, to be
                               represented with just a single byte. Otherwise
                               it is represented as FF AA BB CC DD, where
                               AA BB CC DD are a 4 bytes unsigned integer
                               representing the previous entry len. */

/* Different encoding/length possibilities */
// 字符串编码和整数编码
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30

// 整数编码类型
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe


// 定义ziplist的节点信息结构,但是不是真正保存在ziplist的entry
typedef struct zlentry {
    // 前置节点编码prevrawlen所需的字节大小
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    // 前置节点的长度
    unsigned int prevrawlen;     /* Previous entry len. */
    
    // 编码len所需的字节大小
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    // 当前节点值得长度                                
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    // 当前节点head的大小                                
    unsigned int headersize;     /* prevrawlensize + lensize. */
    // 当前节点值所有的编码类型
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    // 指向当前节点的指针(节点起始点)                                
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

连锁更新

存储类型quickList

在Redis3.2之前: Redis采用压缩列表(ziplist)以及双向链表表(linkedlist),当元素个数交少且元素长度较小时,采用ziplist,其他使用linkedlist

quicklist A doubly linked list of ziplist 能够在时间效率和空间效率实现较好的折中
quicklist是一个双向链表,链表中的每个节点是一个ziplist结构。可以看成用双向链表将若干个小型的ziplist连接到一起的组成的数据结构。当ziplist节点个数过多,quicklist退化为双向链表,一个极端的情况下是每个ziplist节点只包含一个entry,即只有一个元素。当ziplist元素个数过少时,quicklist可以退化为ziplist,极端情况就是一个quicklist中只有一个ziplist

quicklist.h/c
quicklist

/*
 * quicklist redis3.2 中新加的数据结构。用在列表的底层实现 
*  quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,
*  而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是一片数据,而不再是一个数据
* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
*                of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
   // 指向双向列表的表头
   quicklistNode *head;
   // 指向双向列表的表尾
   quicklistNode *tail;
   // 所有ziplist数据项的个数总和
   unsigned long count;        /* total count of all entries in all ziplists */
   // 双向链表的长度,node的数量。
   unsigned long len;          /* number of quicklistNodes */
   // ziplist大小设置,存放list-max-ziplist-size参数的值
   int fill : 16;              /* fill factor for individual nodes */
   // 节点压缩深度设置,存放list-compress-depth参数的值
   unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
// 定义ziplist的节点信息结构,但是不是真正保存在ziplist的entry
typedef struct zlentry { 
   unsigned int prevrawlensize; /* 上一个链表节点占用的长度 */ 
   unsigned int prevrawlen; /* 存储上一个链表节点的长度数值所需要的字节数 */ 
   unsigned int lensize; /* 存储当前链表节点长度数值所需要的字节数 */ 
   unsigned int len; /* 当前链表节点占用的长度 */ 
   unsigned int headersize; /* 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小 */ 
   unsigned char encoding; /* 编码方式 */ 
   unsigned char *p; /* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置 */ 
} zlentry;

quicklistNode 中的*zl 指向一个 ziplist,一个 ziplist 可以存放多个元素

/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
// quicklist节点结构
/**
 * list-max-ziplist-size -2 
 * 当数字为负数,表示以下含义:
 *  -1 每个quicklistNode节点的ziplist字节大小不能超过4kb。(建议)
 *  -2 每个quicklistNode节点的ziplist字节大小不能超过8kb。(默认配置)
 *  -3 每个quicklistNode节点的ziplist字节大小不能超过16kb。(一般不建议)
 *  -4 每个quicklistNode节点的ziplist字节大小不能超过32kb。(不建议)
 *  -5 每个quicklistNode节点的ziplist字节大小不能超过64kb。(正常工作量不建议)
 * 当数字为正数,表示:ziplist结构所最多包含的entry个数。最大值为 215215。
 * 
 * compress成员对应的配置:list-compress-depth 0 
 * 后面的数字有以下含义:
 *  0 表示不压缩。(默认)
 *  1 表示quicklist列表的两端各有1个节点不压缩,中间的节点压缩。
 *  2 表示quicklist列表的两端各有2个节点不压缩,中间的节点压缩。
 *  3 表示quicklist列表的两端各有3个节点不压缩,中间的节点压缩。
 * **/
typedef struct quicklistNode {
    // 指向上一个node节点
    struct quicklistNode *prev;
    // 指向下一个node节点
    struct quicklistNode *next;
    // 数据指针,如果当前节点的数据没有压缩,它指向一个ziplist结构;否则指向一个quicklistLZF结构
    // 不设置压缩数据参数recompress时指向一个ziplist结构
    // 设置压缩数据参数recompress指向quicklistLZF结构
    unsigned char *zl;
    // zl指向的ziplist的总大小(包括zlbytes,zltail,zllen,zlend和各个数据项)
    // 注意:如果zl被压缩了,sz值仍然是压缩前的ziplist大小
    unsigned int sz;             /* ziplist size in bytes */
    //表示ziplist里面包含的数据项个数 
    unsigned int count : 16;     /* count of items in ziplist */
    //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */

    // 表示一个quicklistNode节点是否采用ziplist结构保存数据,
    // 2表示压缩了,1表示没压缩,默认是2,占2bits长度
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */

    // 标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
    // 如果recompress为1,则等待被再次压缩
    unsigned int recompress : 1; /* was this node previous compressed? */
    //测试时使用
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    //额外扩展位,占10bits长度
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
// quicklist的迭代器结构
typedef struct quicklistIter {
    // 指向所属的quicklist的指针
    const quicklist *quicklist;
    // 指向当前迭代的quicklist节点的指针
    quicklistNode *current;
    // 指向当前quicklist节点中迭代的ziplist
    unsigned char *zi;
    // 当前ziplist结构中的偏移量 
    long offset; /* offset in current ziplist */
    // 迭代方向
    int direction;
} quicklistIter;
// 管理quicklist中quicklistNode节点中ziplist信息的结构
typedef struct quicklistEntry {
    // 指向所属的quicklist的指针
    const quicklist *quicklist;
    // 指向所属的quicklistNode节点的指针
    quicklistNode *node;
    // 指向当前ziplist结构的指针
    unsigned char *zi;
    // 指向当前ziplist结构的字符串vlaue成员
    unsigned char *value;
    // 指向当前ziplist结构的整数value成员
    long long longval;
    // 保存当前ziplist结构的字节数大小
    unsigned int sz;
    // 保存相对ziplist的偏移量
    int offset;
} quicklistEntry;

散列表对象(Hash)

命令

####设置命令###
hset key field value
hmset key field value [field value...]
hsetnx key field value
######读取命令#######
hexist key field
hget key field
hmget key field [feild...]
hkeys key # 取key的所有feild值
hvals key
hgetall key
hlen key # feild个个数
hscan key cursor [MATCH pattern] [COUNT cout]
########删除命令#####
hdel key field [field...]
######自增命令####
hincrby key field increment # 将field对应的value增加increment
hincrbyfloat key field increment

应用场景

  • String可以做的事情,Hash都可做
  • 存储对象类型的数据,比如对象或一张表的数据,比String节省更多key的空间,也更加便于集中管理
  • 购物车
    key:用户 id;field:商品 id;value:商品数量。 +1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen

存储(实现原理)
t_hash.c
Redis 的 Hash 本身也是一个 KV 的结构,类似于 Java 中的 HashMap。 外层的哈希(Redis KV 的实现)只用到了 hashtable。当存储 hash 数据类型时, 我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现:

  • ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)
  • hashtable:OBJ_ENCODING_HT(哈希表)

ziplist的存储顺序和插入顺序一致,散列表的存储则不一致
ziplist存储条件

  • (1) 所有的键值对的字符串长度都小于等于hash-max-ziplist-value(默认64byte)
  • (2) 健值对的个数小于hash-max-ziplist-entries(默认512)
    哈希的存储结构

从最低层道最高层dictEntry – dictht—dict —OBJ_ENCODING_HT

# redis.conf
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

存储类型Dict

dict.h

// 哈希表节点
typedef struct dictEntry { 
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下一个哈希表节点,形成链表->解决hash冲突
    struct dictEntry *next;
} dictEntry;
/*
 * 字典类型特定函数
 **/
typedef struct dictType { 
    // 计算哈希值的函数
    uint64_t (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值得函数
    void *(*valDup)(void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    // 销毁值得函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
/***
 * 哈希表 
 *  每个字典都使用两个哈希表,从而实现渐进式rehash
 */
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算哈希值
    // 总是等于 size-1
    unsigned long sizemask;
    // 该哈希表已有节点的数量 load_factor = used / size
    unsigned long used;
} dictht;

// redis 中字典结构
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表 字典只使用ht[0]哈希表,ht[1]哈希表只会在堆ht[0]哈希表进行rehash时使用
    dictht ht[2];
    // rehash索引 当rehash不在进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数据量
    unsigned long iterators; /* number of iterators currently running */
} dict;

为什么要定义两个哈希表呢?ht[2]
redis 的 hash 默认使用的是 ht[0],ht[1]不会初始化和分配空间。
哈希表 dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决 于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率:

  • 比率在 1:1 时(一个哈希表 ht 只存储一个节点 entry),哈希表的性能最好;
  • 如果节点数量比哈希表的大小要大很多的话(这个比例用 ratio 表示,5 表示平均一个 ht 存储 5 个 entry),那么哈希表就会退化成多个链表,哈希表本身的性能 优势就不再存在
    在这种情况下需要扩容。Redis 里面的这种操作叫做 rehash

rehash 的步骤:
1、为字符 ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以 及 ht[0]当前包含的键值对的数量。
扩展:ht[1]的大小为第一个大于等于 ht[0].used*2。
2、将所有的 ht[0]上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放 入指定的位置。 3、当 ht[0]全部迁移到了 ht[1]之后,释放 ht[0]的空间,将 ht[1]设置为 ht[0]表, 并创建新的 ht[1],为下次 rehash 做准备

什么时候触发扩容?
负载因子(源码位置:dict.c):

// 指示字典是否启用 rehash 的标识
static int dict_can_resize = 1;
// 强制 rehash 的比率
static unsigned int dict_force_resize_ratio = 5;

ratio = used / size,已使用节点与字典大小的比例
dict_can_resize 为 1 并且 dict_force_resize_ratio 已使用节点数和字典大小之间的 比率超过 1:5,触发扩容

扩容判断 _dictExpandIfNeeded(源码 dict.c)

/* Expand the hash table if needed
  根据需要,初始化字典(的哈希表),或者对字典(的现有哈希表)进行扩展

 */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    // 渐进式rehash已经在进行,直接返回
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    // 如果字典的0号哈希表为空,那么创建并返回初始化0号哈希表
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    // 以下两个条件之一为真,对字典进行扩展
    // 1) 字典已经使用节点和字典大小之间比率近1:1,并且dict_can_resize为真
    // 2) 已使用节点数和字典大小间的比率超过dict_force_resize_ratio
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {   // 新的哈希表的大小至少是目前已使用节点的两倍
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

扩容方法 dictExpand(源码 dict.c)

/* Expand or create the hash table */
/**
 * 创建一个新的哈希表,并根据字典情况,选择以下其中一个动作来进行:
 * 
 * 1) 如果字典的0号哈希表为空,那么将新的哈希表设置为0号哈希表
 * 2) 如果字典的0号哈希表非空,那么将新的哈希表设置为1号哈希表,
 *    并打开字典的rehash标识,使得程序可以开始对字典进行rehash
 * 
 * size 参数不够大,或者rehash已经在进行时,返回DICT_ERRO
 * 
 * 成功创建0号哈希表,或1号哈希表时 返回DICT_OK
 * O(n)
 * 
 * */
int dictExpand(dict *d, unsigned long size)
{
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    // 不能在字典正在rehash时进行
    // size的值也不能小于0号哈希表的当前已使用的节点
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    // 新哈希表
    dictht n; /* the new hash table */
    // 根据size参数,计算哈希表的大小
    unsigned long realsize = _dictNextPower(size);

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */   
    // 为哈希表分配空间
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    // 如果0号哈希表为空,那么这是一次初始化
    // 程序将新哈希表给0号哈希表的指针,然后字典就可以开始处理键值对
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    // 如果0号哈希表费控,那么为将要rehash操作分配空间
    // 将新哈希表设置1,打开rehash标识,让字典可以进行rehash
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;

缩容:server.c

int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}

集合对象(Set)

Redis 的set实现了无序集合,集合成员唯一,set底层基于dict和inset
集合对象满足以下两个条件,对象使用intset

  • 集合对象保存的元素超过512 set-max-intset-entries 512
  • 集合对象保存的所有的元素都是整数值

命令

#####集合操作######
# 为集合添加新成员
sadd key member [member...]
# 删除集合的中指定的元素
srem key member [member...]
# 返回集合所有的元素
smembers key
# 随机返回集合元素
srandmember key [count]
# 随机返回集合元素,但会删除元素
spop key [count]
# 判断元素是否在集合中
sismember key member
# 移动元素至指定集合
smove source destination member
# 获取集合中的元素数量
scard key
# 增量变量集合元素
sscan cursor [match pattern] [count count]
#######集合运算##########
# 多个集合的交集
sinter key [key...]
# 接结合的交集保存到集合destination中
sinterstor destination key [key...]
# 多集合的并集
sunion key [key...]
# 集合间的差集
sdiff key [key...]

应用场景

  • 抽奖 随机获取元素spop myset
  • 点赞、签到、打卡

    微博的 ID 是 t1001,用户 ID 是 u3001。
    用 like:t1001 来维护 t1001 这条微博的所有点赞用户。
    点赞了这条微博:sadd like:t1001 u3001
    取消点赞:srem like:t1001 u3001
    是否点赞:sismember like:t1001 u3001
    点赞的所有用户:smembers like:t1001
    点赞数:scard like:t1001 比关系型数据库简单许多

  • 商品标签

    用 tags:i5001 来维护商品所有的标签
    sadd tags:i5001 画面清晰细腻
    sadd tags:i5001 真彩清晰显示屏
    sadd tags:i5001 流畅至极

  • 商品筛选

    iPhone11 上市了。
    sadd brand:apple iPhone11
    sadd brand:ios iPhone11
    sadd screensize:6.0-6.24 iPhone11
    sadd screentype:lcd iPhone11
    筛选商品,苹果的,iOS 的,屏幕在 6.0-6.24 之间的,屏幕材质是 LCD 屏幕
    sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd

  • 用户关注、推荐模型

存储类型整数集合(intset)

// 编码 intset.c
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
/**intset.h
 * 整数集合 
 * 整数集合是集合键的底层实现之一,
 * 数组以有序,无重复的方式保存元素 在有需要的时候,程序会根据新添加的元素的类型,改变这个数组的类型
 * 数组升级操作为整数集合带来灵活性,并且尽可能地节约内存
 * 整数集合支持升级操作,不支持降级操作
 * 
 * **/
typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组 
    // 各个项在数组中按值得大小从小到大有序地排列,并且数组中不包含任何重复项
    // 数组的真正类型取决于encoding属性值,如:INTSET_ENC_INT32,content类型是int32_t
    // eg:encoding=INTSET_ENC_INT16,length=5 contents大小:sizeof(int16_t)*5=16*5=80
    // 数组支持升级,不支持降级 按最最大元素类型做类型
    int8_t contents[];
} intset;

有序集合对象(zset)

有序集合中(源码t_zset.c),用到的数据结构是ziplist以及dict和skiplist
当服务器属性server.zset_max_ziplist_entries的值大于0且元素的member长度小于服务器属性server.zset_max_ziplist_value的值(默认64)时使用ziplist,否则使用dict和skiplist

ziplist实现的有序集合,每个集合元素使用两个紧挨在一起的压缩列表节点保存,第一个保存元素的成员(member),第二个保存元素的分值(score)。集合元素按照分值从小到大进行排序,分值较小放在表头

# redis.conf
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

skiplist和dict的实现

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

zset中的zsl跳跃表按照分值从小到大保存又有集合元素,每个跳跃表节点都保存一个集合元素,跳跃表节点object属性保存元素成员,score属性保存元素分值,zrank,zrange基于跳跃表api实现
zset中的dict为有序集合创建一个成员到分值的映射,字典健值保存元素的成员,字典的值保存成员的分值,根据成员查找api基于字典实现

命令

# 添加元素
zadd key [NX|XX] [CH] [INCR] score member [score member...]
# 删除有序集合元素
zrem key member [member...]
# 获取有序集合的个数
zcard key
# 返回有序集合key中的score值在[min,max]区间的成员的数量
zcount key min max
# 获取有序集合key中成员member的分值
zscore key member
# 在有序集合key的member的分值上增加increment
zincrby key increment member
# 按照分值从小到大返回有序集合成员member的排名,排名从0开始计算
zrank key member
# 按照从大到小返回member的排名
zrevrank key member
# 迭代有序集合中的元素成员和分值,其中cursor游标,MATCH中可以通过正则匹配,count是返回元素的数量
zscan key cusor [MATCH pattern] [COUNT count]
############区间操作###################
# 获取有序集合key中指定区间的成员,成员按照分值递增排序,分值相同,则按照字典排序
zrange key start stop [WITHSCORES]
# zrevrange 相反 成员按照分值递减排序
zrevrange key start stop [WITHSCORES]
# 返回有序集合key中,所有score值介于min和max之间(含min或max) ,score值递增排序
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
# score值递减排序
zrevrangebyscore key min max [WITHSCORES] [LIMIT offset count]
# 返回有序集合key中值(member)介于min和max之间的成员,成员按照字典排序
# min和max参数必须包含"("或者"["
zrangebylex key min max [LIMIT offset count]
zrangebylex sortset - (d
# 返回给定条件的成员数量
zlexcount key min max
zlexcount sortset [b [d
# 移除有序集合key中排名区间的所有成员
zremrangebyrank key start stop
# 按照score移除
zremrangebyscore key min max
# 按照member移除
zremrangebylex key min max
#########集合运算###########
# 求并集,并将结果存储到集合destination
zunionstore destination numkeys key [key ...] [WEIGHTS weight [weight ...] [AGGREGATE SUM|MIN|MAX]
zunionstore setC 2 setA setB WEIGHTS 1 1 AGGREATE MIN
# 交集
zintersore destination numkeys key [key ...] [WEIGHTS weight [weight ...] [AGGREGATE SUM|MIN|MAX]

应用场景

  • 排行榜

存储类型跳跃表(skiplist)

对于有序集合底层实现,可以用数组、链表、平衡树等结构
数组不便于元素的插入和删除;链表的查询效率低,需要遍历所有元素
平衡树或红黑树等结构虽然效率高但实现复杂
跳跃表的效率堪比红黑树,然而实现远比红黑树简单。 分层有序链表

server.h

/* ZSETs use a specialized version of Skiplists */
// 跳跃表节点 有序集合的的底层实现之一 
// 跳跃表和平衡树: 大部分情况下,跳跃表的效率可和平衡树相媲美,但是实现比平衡树简单
typedef struct zskiplistNode {
    // 成员对象,唯一的
    sds ele;
    // 分值 
    //跳跃表中的所有节点都按照分值从小到大排序,多个节点的分值可以是相同
    // 分值相同,按照成员对象大小排序
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    // level数组,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度
    // 一般来说,层的数量越多,访问其他节点的速度就越快
    // 每次创建一个新跳跃表节点的时候,程序都根据冥次定律随机生成一个介于1和32之间的值作为level数组的大小
    // 这个大小就层的“高度”
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度 用于记录两个节点之间的距离
        // 指向NULL的所有前进指针的跨度为0
        // 跨度用于计算排位(rank)的,在查到某个节点的过程中将沿途访问的跨度累计
        // ,得到的结果就是目标节点在跳跃表中的排位
        unsigned long span;
    } level[];
} zskiplistNode;

// 跳跃表
typedef struct zskiplist {
    // 表头节点和表尾结点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层次最大的节点的层数
    int level;
} zskiplist;

随机获取层数的函数t_zset.c

/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. */
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

数据流对象(stream)

Redis5.0.0引入的数据类型,支持多播的可持久化消息队列,用于实现发布订阅功能,借鉴kafka的设计
在Stream的实现中,用到的关键数据机构是rax(用于快速索引)、listpack(存储具体的消息)
官网文档 中文文档

命令

# 将指定消息数据追加到指定的Stream队列中或者裁剪队列中的数据长度
xadd key [MAXLEN [~|=] <count>] <ID or *> [field value] [field value] ..
##########xadd 命令说明##############
# 每条消息是由一个或多个阈值对组成,消息插入Stream队列中后会返回唯一消息ID。xadd是唯一可以向Stream队列添加数据的命令
# MAXLEN: 当Stream中数据量过大时,可以通过此关键字来剪裁长度,删除stream中旧数据至指定的值;
# 当数据量小于等于指定值时,不进行剪切。剪切模式有两种
# 1) ~: 模糊裁剪,优化精确裁剪,一般用此模式,效率更高
# 2) =:精确裁剪,在数据存储的listpack结构体中,剪裁长度的所有阈值是依照数据从老到新的方式,
# 依次把listpack释放掉,但在此模式下,删除最后一个listpack中的数据比较费时,所以推荐使用模糊裁剪
# ID: 添加消息可指定具体值或者用"*"代替,指定的值必须大于当前stream队列中最大消息ID,
# 为“*”时默认生成一个最新ID,ID最值 当前时间+序列号
# eg:
# 添加一条数据不指定ID
xadd mytopic * name tom age 20
# 指定ID
xadd mytopic 1580192614806-0 name jack age 30
# 修改长度 消息缩减至100万条
xadd mytopic MAXLEN ~ 1000000 * name mic age 19
###################
# 读取给定的ID范围内的消息数据 - 最小id + 最大id
xrange key start end [COUNT count]
xrange mytopic - +
xrevrange mytopic + - # 顺序相反
# 删除Stream队列中指定一个或者多个消息ID对应的数据
xdel key ID [ID...]
##############消费组########
# 用于队列的消费组管理,包含对消费组的创建、删除、修改等操作
xgroup [CREATE key groupname id-or-$] # 创建新的消费组
       [SETID  key id-or-$] # 修改某个消费组消息的消息last_id
       [DESTROY key groupname] # 删除指定消费组
       [DELCONTSUMER key groupname consumername] # 删除指定消费组中的某个消费者
       [HELP]  # 查看帮助
# 创建一个消费组cg1,从消费id为1580192614806-0的消息开始消费
xgroup CREATE mytopic cg1 1580192614806-0       
# 修改消费组,从消息id为1580192614999-0开始消费
xgroup SETID mytopic cg1 1580192614999-0
# 用于从消费组中可靠地消费n条消息,如果指定的消费者不存在,则创建
xreadgroup GROUP group  # 消费组名称
          consumer [COUNT count] # 消费者名称 消费多少条数据
          [BLOCK milliseconds]   # 是否为阻塞 
          STREAMS key [key...] #  stream 队列名称可以是多个
          ID [ID...] # 读取大于消息ID后未确认的消息 ">" 读取未传递给其他任何消费者的消息,也就是新消息  NOACJ 该消息不需要确认
xreadgroup GROUP cg1 c1 COUNT 1 STREAMS mytopic >
# 用于从stream队列中读取N条消息,一般用作遍历队列中的消息
xread [COUNT count] [BLOCK milliseconds] STREAMS key [key...] ID [ID...]
# 用于确认一或多个指定ID消息,使其从待确认列表中删除
xack key group ID [ID...]
# 用于读取某消费组或者某个消费者的未确认消息,返回未确认的消息ID、空闲时间、被读取次数
xpending key group [start end count] [consumer]
# 读取消费组cg1中消费者c1的所有待确认消息
xpending mytopic cg1 - + 2 c1
# 读取消费组cg1的所有待确认消息
xpending mytopic cg1 - + 10
# 用于改变一或者多个未确认消息的所有权,新的所有者是在命令参数中指定
xclaim key group consumer min-idle-time ID [ID...] [IDLE ms] 
       [TIME ms-unix-time] 
       [RETRYCONUT count] 
       [force] 
       [justid] 
# 读取消息队列、消费组、消息等信息
xinfo [CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]
# 缩减消息队列
xtrim key MAXLEN [~] count
# 用于获取Stream队列的数据长度
xlen key ID [ID...]         
/* Stream item ID: a 128 bit number composed of a milliseconds time and
 * a sequence counter. IDs generated in the same millisecond (or in a past
 * millisecond if the clock jumped backward) will use the millisecond time
 * of the latest generated ID and an incremented sequence. */
typedef struct streamID {
    uint64_t ms;        /* Unix time in milliseconds. */
    uint64_t seq;       /* Sequence number. */
} streamID;

typedef struct stream {
    rax *rax;               /* The radix tree holding the stream. */
    uint64_t length;        /* Number of elements inside this stream. */
    streamID last_id;       /* Zero if there are yet no items. */
    rax *cgroups;           /* Consumer groups dictionary: name -> streamCG */
} stream;

/* We define an iterator to iterate stream items in an abstract way, without
 * caring about the radix tree + listpack representation. Technically speaking
 * the iterator is only used inside streamReplyWithRange(), so could just
 * be implemented inside the function, but practically there is the AOF
 * rewriting code that also needs to iterate the stream to emit the XADD
 * commands. */
typedef struct streamIterator {
    stream *stream;         /* The stream we are iterating. */
    streamID master_id;     /* ID of the master entry at listpack head. */
    uint64_t master_fields_count;       /* Master entries # of fields. */
    unsigned char *master_fields_start; /* Master entries start in listpack. */
    unsigned char *master_fields_ptr;   /* Master field to emit next. */
    int entry_flags;                    /* Flags of entry we are emitting. */
    int rev;                /* True if iterating end to start (reverse). */
    uint64_t start_key[2];  /* Start key as 128 bit big endian. */
    uint64_t end_key[2];    /* End key as 128 bit big endian. */
    raxIterator ri;         /* Rax iterator. */
    unsigned char *lp;      /* Current listpack. */
    unsigned char *lp_ele;  /* Current listpack cursor. */
    unsigned char *lp_flags; /* Current entry flags pointer. */
    /* Buffers used to hold the string of lpGet() when the element is
     * integer encoded, so that there is no string representation of the
     * element inside the listpack itself. */
    unsigned char field_buf[LP_INTBUF_SIZE];
    unsigned char value_buf[LP_INTBUF_SIZE];
} streamIterator;

/* Consumer group. */
typedef struct streamCG {
    streamID last_id;       /* Last delivered (not acknowledged) ID for this
                               group. Consumers that will just ask for more
                               messages will served with IDs > than this. */
    rax *pel;               /* Pending entries list. This is a radix tree that
                               has every message delivered to consumers (without
                               the NOACK option) that was yet not acknowledged
                               as processed. The key of the radix tree is the
                               ID as a 64 bit big endian number, while the
                               associated value is a streamNACK structure.*/
    rax *consumers;         /* A radix tree representing the consumers by name
                               and their associated representation in the form
                               of streamConsumer structures. */
} streamCG;

/* A specific consumer in a consumer group.  */
typedef struct streamConsumer {
    mstime_t seen_time;         /* Last time this consumer was active. */
    sds name;                   /* Consumer name. This is how the consumer
                                   will be identified in the consumer group
                                   protocol. Case sensitive. */
    rax *pel;                   /* Consumer specific pending entries list: all
                                   the pending messages delivered to this
                                   consumer not yet acknowledged. Keys are
                                   big endian message IDs, while values are
                                   the same streamNACK structure referenced
                                   in the "pel" of the conumser group structure
                                   itself, so the value is shared. */
} streamConsumer;

/* Pending (yet not acknowledged) message in a consumer group. */
typedef struct streamNACK {
    mstime_t delivery_time;     /* Last time this message was delivered. */
    uint64_t delivery_count;    /* Number of times this message was delivered.*/
    streamConsumer *consumer;   /* The consumer this message was delivered to
                                   in the last delivery. */
} streamNACK;

/* Stream propagation informations, passed to functions in order to propagate
 * XCLAIM commands to AOF and slaves. */
typedef struct sreamPropInfo {
    robj *keyname;
    robj *groupname;
} streamPropInfo;

## 其他数据类型

Hyperloglogs

Hyperloglogs:提供了一种不太准确的基数统计方法,比如统计网站的 UV,存在 一定的误差

你可能感兴趣的:(架构师,分布式)