1

1. Redis简介

​ redis全称 Remote Dictionary Service(远程字典服务)。Redis存储的都是K-V对,Redis里存放的任何一条数据都有唯一的key作为名称。Redis中,所有的key都存储在一个很大的字典中。一个Redis实例有多个库(database),可用过配置文件里的“databases”配置,库索引从0到databases-1,可以使用指令SELECT <index>切换到不通的库。

注意:在Redis命令行,Redis的指令不区分大小写,但是key区分大小写。

2. Redis基础数据结构

​ Redis有五种基础的数据结构:string、list、hash、set、zset。其中list、hash、set、zset称为容器型数据结构,容器型数据结构有两条通用规则:

  • 如果容器不存在,就创建一个,再进行操作。
  • 如果容器里没有数据了,就立即删除,回收内存。

2.1 string(字符串)

string是Redis最简单的数据结构,一个key对应一个value。Redis的string实际上是使用数组实现的。

string基本指令有:

  • SET:设置一个kv对,示例:SET name lily
  • GET:获取一个key对应的的value,示例:GET name
  • MSET:设置一个或多个kv对,示例:MSET name lily age 18
  • MGET:获取一个或多个key对应的value,示例:MGET name age
  • SETNX:设置一个kv对,如果该key不存在,设置成功;如果改成存在,设置失败,示例:SETNX name lilei
  • INCR:如果 value 是整数,还可以进行原子加一操作,示例:INCR age
  • DECR:如果 value 是整数,还可以进行原子减一操作,示例:DECR age

2.2 list(列表)

​ Redis的list是链表实现的,所以其插入删除操作非常快,时间复杂度都是O(1),但是索引定位很慢,时间复杂度是O(n)。当list中没有元素了,该数据结构会自动删除,内存被回收。

list的基本指令有:

  • LPUSH:从左边插入元素,可以插入多个。示例:LPUSH students lily lilei zhangsan
  • LPOP:从左边删除元素,默认删除一个元素。可以指定需要删除的个数,示例:LPOP students 5
  • RPUSH:从右边插入元素,用法同LPUSH
  • RPOP:从右边删除元素,用法同LPOP
  • LRANGE:列出指定范围里的元素(不会删除元素),示例:LRANGE students 0 -1
  • LINDEX:(时间复杂度O(n))获取指定位置的元素,示例:LINDEX students 1
  • LTRIM:(时间复杂度O(n))删除指定范围的元素,示例:LTRIM students 1 2

list常用场景:

  1. 队列/消息队列

    从list的一边push数据,从另一边pop数据,就可以作为队列或者消息队列使用。如果list里面没有数据,LPOPRPOP不会阻塞,都会立即返回空。如果用作消息队列,消费者需要处理LPOPRPOP立即返回空的情况,一般的做法是如果返回为空,等待一段时间,再次获取,但是等待时间不好确定,等待时间太短会拉高CPU,造成浪费;等待时间太长会降低QPS,影响性能。Redis为list设计了阻塞读指令:BLPOPBRPOP,指令用法是BLPOP key1 [key2 ...] timeout,这里BLPOP/BRPOP里的Bblocking的意思。阻塞读在list没有数据的时候,会立即休眠,一旦有数据到了,则立刻醒过来。

  2. 在list的同一边push和pop数据,就可以作为栈使用

  3. 消息队列

    LPOP或者RPOP操作不会阻塞,如果list里面没有数据,LPOPRPOP都会立即返回空,如果

2.3 hash(字典)

​ Redis的hash的值只能是字符串。

hash的基本指令:

  • HMSET:写入数据,支持批量。示例: HMSET 001 name lily age 18,其中 001 是该条hash的key,name、age 是hash里面的field。
  • HMGET:获取指定hash的某个field的value。示例:HMGET 001 name
  • HGETALL:获取指定hash的所有数据。示例:HGETALL 001
  • HINCRBY :如果hash内部的value是整型,可以使用HINCRBY进行原子加减操作(只需讲操作数设置为负数就是减法操作),示例:HINCRBY 001 age 1HINCRBY 001 age -1

2.4 set(集合)

​ Redis的set是无序的。

set的基本指令:

  • SADD:加入集合。示例:SADD students lily lilei
  • SMEMBERS:列出集合内成员。示例:SMEMBERS students
  • SISMEMBER:查询某个成员是否存在。示例:SISMEMBER students lily
  • SREM:删除某个成员。示例:SREM students lily
  • SCARD:获取集合长度。示例:SCARD students
  • SPOP:弹出成员,可以指定需要弹出的个数(不知道默认是1)。示例:SPOP students 5

2.5 zset(有序集合)

​ Redis的zset是一个很有特色的数据结构,一方面它是一个set,保证了内部value的唯一性,另一方面它可以给每个value赋予一个score,代表这个value的排序权重。它的内部实现采用的是”跳跃列表(skiplist)“。

zset基本指令:

  • ZADD:增加成员。示例:ZADD students 95 lily 85 lilei 70 zhangsan 55 lisi
  • ZRANGE:正序获取指定个数的成员。示例:ZRANGE students 0 -1
  • ZREVRANGE:倒序获取指定个数的成员。示例:ZREVRANGE statuents 0 -1
  • ZRANGEBYSCORE:根据score范围获取成员,正序排列。示例:ZRANGEBYSCORE students 0 60
  • ZREVRANGEBYSCORE:根据score范围获取成员,倒叙排列。示例:ZREVRANGEBYSCORE students 100 0
  • ZREM:删除指定成员。示例:ZREM students zhangsan
  • ZREMRANGEBYSCORE :按score范围删除成员。示例:ZREMRANGEBYSCORE students 0 60
  • ZSCORE:获取成员score。示例:ZSCORE students lilei
  • ZCARD:获取zset长度。示例:ZCARD students

3. Redis通用指令

3.1 EXPIRE

EXPIRE指令用于设置超时,某个key超时后会自动删除。示例:EXPIRE students 10,10秒后students将被自动删除。

超时相关指令有:

  • TTL:查看剩余过期时间,单位是秒。示例:TTL students
  • PTTL:查看剩余过期时间,单位是豪秒。示例:PTTL students

3.2 KEYS

KEYS获取当前库所有的或者特定的key列表,支持通配符”*“。示例:KEYS *查询全部key。

3.3 SCAN

KEYS可以获取key列表,但是有两个明显的缺点:

  1. 没有 offset、limit 参数,一次性返回所有满足条件的key。
  2. KEYS算法是遍历算法,复杂度是O(n),如果当前库中key很多,使用KEYS会导致Redis卡顿。

    ​ Redis提供了SCAN指令,SCAN相比KEYS有一下特点:

    1. 复杂度虽然也是O(n),但是它是通过游标分步进行的,不会阻塞线程。
    2. 提供limit参数,可以控制每次返回结果的最大条数。
    3. 提供模式匹配功能。
    4. 服务器不需要为游标保存状态,游标的唯一状态就是SCAN返回给客户端的游标整数。
    5. 返回的结果可能会有重复,需要客户端去重。
    6. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的。
    7. 单词返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零。

SCAN用法: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type],示例:scan 0 match s* count 5 type stringSCAN有四个参数,第一个是cursor整数值(相当于offset),第二个是key的正则模式,第三个是count(相当于limit),第四个是key的类型。第一次遍历时cursor值为0,然后将返回结果中的第一个整数值作为下一次遍历的cursor,一直遍历到返回的cursor值为0时结束。需要注意的是,每次SCAN返回的个数不一定是count,因为这个count不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量,只要返回的cursor不是0,就表示遍历还没结束。

3.4 INFO

​ 通过INFO指令,可以清晰地看到Redis内部一系列运行参数,如下:

127.0.0.1:6379> info
# Server
redis_version:6.2.1
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:fa5dfffb0053744e
...

# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
...

# Memory
used_memory:895736
used_memory_human:874.74K
used_memory_rss:8294400
used_memory_rss_human:7.91M
...

# Persistence
loading:0
current_cow_size:0
current_fork_perc:0.00%
...

# Stats
total_connections_received:16
total_commands_processed:140
instantaneous_ops_per_sec:0
...

# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:b81735795edbd0987a13af0b42d5896e0c87e10c
...

# CPU
used_cpu_sys:178.131199
used_cpu_user:250.434150
used_cpu_sys_children:0.009278
...

# Modules

# Errorstats
errorstat_ERR:count=15

# Cluster
cluster_enabled:0

# Keyspace
db0:keys=1,expires=0,avg_ttl=0
db1:keys=2,expires=0,avg_ttl=0

INFO指令可以查看某一方面地数据,如下:

127.0.0.1:6379> info clients
# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
...

127.0.0.1:6379> info cpu
# CPU
used_cpu_sys:178.614214
used_cpu_user:251.278258
used_cpu_sys_children:0.009278
...

3.5 其他指令

  • TYPE,查看数据类型。实例:TYPE students
  • SAVE:持久化。
  • BGSAVE:后台持久化。

4. Redis高级功能

4.1 分布式锁

​ 锁的通用使用方法:“获取锁--业务逻辑--释放锁”。使用Redis实现分布式锁的本质就是”占位“,具体实现是使用SETNX操作同一个key。哪个进程能够SETNX成功,就表示得到了锁(因为一旦key存在了,其他进程SETNX就会失败)。为了防止进程获取锁后在释放锁之前崩溃,会使用EXPIRE给锁设置个超时。为了防止进程在SETNX之后,EXPIRE之前崩溃,需要保证SETNXEXPIRE的执行是原子化的。SET指令可以实现,示例:SET lock true ex 10 nx

使用Redis做分布式锁需要注意的问题:

  1. 如果进程在加锁和锁超时之间逻辑执行的时间太长,进程本身可能会出现问题,这个需要根据业务处理。而且锁超时之后,其他进程就可以获取到锁,但是如果这时前一个进程释放锁,就可能把第二个进程获取到的锁释放掉。解决这个问题,就需要在SET设置锁的时候把value设置成一个随机数,释放锁的时候先匹配随机数是否一致,然后再删除key。

4.2 延时队列

​ 可以使用Redis的zset实现简单的延时队列,思想很简单,就是把score设置成当前时间戳加上延时时长,然后有一个轮循,使用ZRANGEBYSCORE获取小于当前时间戳的元素。如果没有到期,是获取不到放入的元素的。

用法示例:

client = redis.Redis(host="10.53.3.62")
now = int(time.time())
print("start ", now)
client.zadd("tasks", {"task1": now + 5})

while 1:
    a = client.zrangebyscore("tasks", 0, int(time.time()))
    print(int(time.time()), a)
    time.sleep(1)

# 输出:(可以看到,5s之后才能获取到数据,达到了延时效果)
start  1618295567
1618295567 [b'+']
1618295568 [b'+']
1618295569 [b'+']
1618295570 [b'+']
1618295571 [b'+']
1618295572 [b'+', b'task1']
1618295573 [b'+', b'task1']
1618295574 [b'+', b'task1']

4.3 位图

​ Redis提供了位图数据结构,实际上就是string。Redis提供了以下指令操作位图:

  • SETBIT:设置某位的值。示例:SETBIT w 1 1,表示给w的第1位设置为1。
  • GETBIT:获取某位的值。示例:GETBIT w 1,表示获取w的第1位置。
  • BITCOUNT:统计制定范围内1的个数。示例:BITCOUNT w 0 1
  • BITPOS:查找指定范围内出现的第一个0或1。用法:BITPOS key bit [start] [end],示例:BITPOS w 1 0 1
  • BITFIELD:支持一次操作多个位。BITFIELD有三个子指令,分别是GET/SET/INCRBY,它们都可以对指定位片段进行读写,但是最多只能处理64个连续的位,如果超过64位,就得使用多个子指令,BITFIELD可以一次执行多个子指令。

4.4 HyperLogLog

​ HyperLogLog数据结构是Redis的高级数据结构。HyperLogLog提供不精确的去重计数方案,用于不需要精确统计的统计场景。HyperLogLog不保存源数据,只去重并统计个数。HyperLogLog没有提供类似查找的方法,不能使用HyperLogLog判断某个元素是否存在。

HyperLogLog用法示例:

  • PFADD:加入元素(会去重)。示例:PFADD students lily
  • PFCOUNT:统计个数。示例:PFCOUNT students

4.5 布隆过滤器

​ 布隆过滤器用于判断某个元素是否存在。当布隆过滤器说这个元素存在时,这个元素可能存在;当它说这个元素不存在时,那就肯定不存在。Redis提供了支持布隆过滤器的插件,需要安装该插件,才能使用。

用法示例:

  • BF.ADD:加入元素。示例:BF.ADD students lily
  • BF.EXISTS:判断元素是否存在。示例:BF.EXISTS students lilei
  • BF.MADD:批量加入元素。示例:BF.MADD students zhangsan lisi wangwu
  • BF.MEXISTS:判断多个元素是否存在。示例:BF.MEXISTS students zhangsan lisi wangwu

布隆过滤器的原理

​ 每个布隆过滤器对应到Redis的数据结构里面就是一个大型的位数组和几个不一样的无偏hash函数。向布隆过滤器种添加key时,会使用多个hash函数对key进行hash算得一个整数索引值然后对数组长度取模得到一个位置,每个hash函数都会算得一个不同得位置,再把位数组得这几个位置都置为1就完成了add操作。所以查询key是否存在时,即使hash后的所有位都为1,也不一定存在,可能有的位置的1是其他key的hash结果,但是如果hash后的位置中有某个位置的结果不是0,那就表示该key一定不存在。

4.6 zset简单限流

​ 可以使用Redis的zset限制某个用户在规定的时间内允许访问的次数。基本思想也不复杂:就是能获取到一个时间段范围的次数统计。使用zset的ZRANGEBYSCORE指令就可以做到。用户没访问一次,就往zset里面加入一个元素,score使用时间戳(要精确的话可以使用毫秒级的),value只要唯一就行,简单起见,可以和score一同使用毫秒级时间戳。通常只需要保留规定时间范围内的访问次数,如果规定时间以前的次数不需要存储,可以使用ZREMRANGEBYSCORE删掉规定时间之前的数据。

用法示例:

client = redis.Redis(host="10.53.3.62")


def is_action_allowed(user_id, action_key, period, max_count):
    """
    指定用户 user_id 的某个行为 action_key 在特定的时间内 period 只允许发生一定的次数 max_count
    """
    key = 'hist:%s:%s' % (user_id, action_key)
    now_ts = int(time.time() * 1000)  # 毫秒时间戳
    with client.pipeline() as pipe:
        pipe.zadd(key, {now_ts: now_ts})  # 记录访问次数
        pipe.zremrangebyscore(key, 0, now_ts - period * 1000)  # 移除老旧数据
        pipe.zcard(key)  # 获取规定时间内的次数
        _, _, current_count = pipe.execute()

    return current_count <= max_count


for i in range(20):
    can_reply = is_action_allowed("lily", "request", 60, 10)
    print(i, can_reply)
    
# ======= 输出:
0 True
1 True
2 True
3 True
4 True
5 True
6 True
7 True
8 True
9 True
10 True
11 True
12 True
13 True
14 False
15 False
16 False
17 False
18 False
19 False

4.7 GeoHash

​ Redis提供了地理位置模块GeoHash,实际是使用zset存储的。

GeoHash用法:

  • GEOADD:增加位置。
  • GEODIST:计算距离。
  • GEOPOS:查询经纬度,可批量。
  • GEOHASH:获取元素geohash值,可以使用该geohash值到网站http://geohash.org/${hash}直...
  • GEORADIUSBYMEMBER:获取元素附近的其他元素。
  • GEORADIUS:根据坐标值查询附近的元素。

示例:

127.0.0.1:6379> GEOADD sz 114.035529 22.615108 beizhan
(integer) 1
127.0.0.1:6379> GEOADD sz 113.821705 22.638172 jichang
(integer) 1
127.0.0.1:6379> GEODIST sz beizhan jichang km
"22.1019"
127.0.0.1:6379> GEOPOS sz beizhan
1) 1) "114.03552979230880737"
   2) "22.61510789529109644"
127.0.0.1:6379> GEOHASH sz beizhan
1) "ws10duyns30"
127.0.0.1:6379> GEORADIUSBYMEMBER sz beizhan 30 km count 3 asc
1) "beizhan"
2) "jichang"
127.0.0.1:6379> GEORADIUSBYMEMBER sz beizhan 20 km count 3 asc
1) "beizhan"
127.0.0.1:6379> GEORADIUS sz 113.821705 22.638172 23 km withdist count 3 asc
1) 1) "jichang"
   2) "0.0001"
2) 1) "beizhan"
   2) "22.1018"

4.8 PubSub

​ Redis的list可以做为简单的消息队列使用,但是它不支持多播。Redis提供了PubSub,一个简单的消息队列,支持多播。它的用法比较简单,就是订阅和发布。PubSub指令:

  • SUBSCRIBE: 订阅,支持模式匹配,支持批量订阅。
  • PUBLISH:发布。

PubSub缺点:

​ PubSub的消息不会持久化,如果没有消费者,生产者发布的消息会丢掉。

5. Redis进阶篇

5.1 持久化

​ Redis提供了两种持久化机制:一种是RBD(Redis Database),即快照;另一种是AOF(Append Only File)日志追加。RBD是一次全量备份,AOF日志是连续的增量备份。RBD是内存数据的二进制序列化形式,在存储上非常紧凑,而AOF日志记录的是内存数据修改的指令记录文本。AOF日志会慢慢变大,所以需要定期进行AOF重写,给AOF日志进行瘦身。

5.1.1 RBD

​ Redis使用操作系统的多进程COW(Copy On Write,写时复制)机制来实现快照持久化。Redis在持久化时fork一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,和父进程共享内存里面的代码段和数据段,当父进程接收到客户端请求需要修改数据时,会利用COW机制将需要修改的页复制一份进行修改,不会改变原来的页,所以子进程看到的还是它自己产生的那一瞬间的数据(这就是叫这种持久化机制为”快照“的原因),可以安心的遍历数据进行持久化。

5.1.2 AOF

​ AOF日志存储的是Redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录。Redis恢复的时候,只需要根据AOF日志”重放“就行了。

​ Redis在长期运行过程中,AOF日志会越变越大,所以需要对AOF日志进行瘦身。Redis提供了bgrewirteaof指令用于对AOF日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列Redis的操作指令,序列化到一个新的AOF日志文件中。序列化完毕后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的AOF日志文件,瘦身工作就完成了。

​ 当Redis对AOF日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘。Redis提供了不同的刷盘策略:

  • 定时刷盘,时间间隔可配。(如果需要开启持久化,一般采用这种方式。)
  • 来一个指令就刷盘一次。(会影响性能)
  • 永不主动刷盘,让操作系统决定何时刷盘。(很不安全)

5.1.3 混合持久化

​ 将RDB文件的内容和增量的AOF日志文件存在一起,这里的AOF日志不是全量的日志,而是自快照持久化开始到持久化结束的这段时间发生的增量AOF日志。Redis重启的时候,先加载RBD的内容,再重放增量AOF日志。

5.2 管道

Redis的管道并不是Redis服务器提供的一个技术,而是客户端提供的。原理就是客户端连续发送多个请求发送,最后等待服务器返回结果,节省等待每个请求返回响应的时间。服务端还是按正常顺序处理每个请求。

5.3 事务

​ Redis也支持事务,相关指令有:

  • MULTI:指示事务的开始,相当于关系型数据库里的begin
  • EXEC:执行事务,相当于关系型数据库里的commit
  • DISCARD:丢弃事务,相当于关系型数据库里的rollback

简单事务使用示例:

# === EXEC 使用示例
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR students
QUEUED
127.0.0.1:6379(TX)> INCR students
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 1
2) (integer) 2
127.0.0.1:6379> get students
"2"
127.0.0.1:6379> 

# ==== DISCARD 使用示例
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR students1
QUEUED
127.0.0.1:6379(TX)> INCR students1
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> get students1
(nil)

​ Redis在开始一个事务时,所有的指令在EXEC之前不执行,而是缓存在服务器的一个事务队列中。如果服务器收到EXEC指令,就开始执行整个事务队列,并在执行完毕后一次性返回所有指令的运行结果;如果服务器收到DISCARD指令,则丢弃事务缓存队列中的所有指令。

Redis的事务是不支持原子性的,即使事务执行中间遇到了失败,后续指令也还是会继续执行。如下:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET name lily
QUEUED
127.0.0.1:6379(TX)> INCR name
QUEUED
127.0.0.1:6379(TX)> set after hello
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> GET after  # 即使第二个指令出错了,第三个指令仍然正常执行
"hello"

​ Redis可以使用WATCH指令配合事务来解决并发修改的问题(WATCH机制是一种乐观锁)。WATCH会在事务开始之前监视一个或多个变量,当事务开始执行时(发送了EXEC),Redis会检查被WATCH监视的变量是否被当前事务之外的请求修改过,如果有修改过,EXEC指令会返回nil,Redis事务执行返回失败。

127.0.0.1:6379> WATCH monitor
OK
127.0.0.1:6379> INCR monitor  # 在事务之前修改了 monitor
(integer) 1
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR monitor
QUEUED
127.0.0.1:6379(TX)> EXEC  # 事务执行失败
(nil)

# =====
127.0.0.1:6379> WATCH monitor1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> incr monitor1
QUEUED
127.0.0.1:6379(TX)> incr monitor1
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 1
2) (integer) 2

注意:Redis禁止在MULTIEXEC之间执行WATCH指令,而必须在MULTI之前检视变量。

5.4 淘汰策略

​ 当Redis内存使用超出配置参数“maxmemory”限制的内存后,Redis提供了几种可选策略(配置里的“maxmemory-policy”)来让用户决定如果处理。Redis配置文件里面可以看到这些策略:

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
  • volatile-lru:从设置了过期时间的key的集合中使用近似LRU算法进行淘汰。没有设置过期时间的key不会被淘汰。
  • allkeys-lru:从所有的key的集合中使用近似LRU算法进行淘。
  • volatile-lfu:从设置了过期时间的key的集合中使用近似LFU算法进行淘汰。
  • allkeys-lfu:从所有的key的集合中使用近似LFU算法进行淘。
  • volatile-random:从设置了过期时间的key的集合中随机淘汰key。
  • allkeys-random:从所有的key的集合中随机淘汰key。
  • volatile-ttl:从设置了过期时间的key的集合中根据剩余时间ttl的值进行淘汰,ttl越小的越先被淘汰。
  • noeviction:不做任何操作,只对写请求返回错误,读请求可以继续进行。

5.4.1 LRU

​ LRU,Least Recentlly Used,最近最少使用。LRU算法使用一个链表,维护被访问的key的顺序,每个key被访问的时候,就把key移到表头,所以链表的顺序就是key最近被访问的时间顺序。如果空间满了,就从表尾删除数据。

使用python的OrderedDict(字典+双向链表)实现简单的LRU算法:

from collections import OrderedDict


class LRUDict(OrderedDict):
    def __init__(self, capacity):
        super().__init__()
        self.capacity = capacity
        self.items = OrderedDict()

    def __setitem__(self, key, value):
        old_value = self.items.get(key)
        if old_value is not None:  # 如果key存在,移动到表头
            self.items.pop(key)
            self.items[key] = value
        elif len(self.items) < self.capacity:  # 如果key不存在,且空间够用,直接插入到表头
            self.items[key] = value
        else:
            self.items.popitem(last=False)  # 如果空间满了,删除最后一个元素,然后将key插入到表头
            self.items[key] = value

    def __getitem__(self, key):
        value = self.items.get(key)
        if value is not None:  # 每次访问key,都把key移动到表头
            self.items.pop(key)
            self.items[key] = value
        return value

    def __repr__(self):
        return repr(self.items)


d = LRUDict(10)
for i in range(15):
    d[i] = i
    print(d)

5.4.2 LFU

​ LFU,Least Frequently User,最近不经常使用。LFU算法是在给每个key统计访问次数。在每个key被访问的时候,计数加一,如果多个计数相同,则按访问时间排序。空间满的时候,从计数最小的开始删除。

6. Redis集群篇

6.1 主从同步

6.1.1 增量同步

​ Redis增量同步同步的是指令流,主节点将会修改数据的指令记录在内存buffer中,然后异步将buffer中的指令同步到从节点,从节点一边执行同步的指令流,一边向主节点反馈同步偏移位置。

​ Redis的复制内存buffer是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。如果网络不好,从节点同步太滞后,可能会造成从节点数据丢失。

6.1.2 全量同步(快照同步)

​ 快照同步首先在主库上进行一次bgsave将数据持久化到磁盘,然后再将快照文件的内容全部传送到从节点。从节点将快照文件全部接收完毕后,清空内存占用的数据,对快照文件执行一次全量加载,加载完成后再通知主节点继续进行增量同步。

6.1.3 无盘复制

​ 无盘复制是指服务器直接通过套接字将快照内容发送到从节点,主节点一边遍历内存,一边将内容发送到从节点。从节点先将收到的内容存入磁盘,然后进行一次性加载。

6.2 Sentinel(哨兵)模式

​ Redis Sentinel是Redis官方提供的一种高可用方案。Redis Sentinel本身可以单点部署,也可以集群部署。

​ Redis Sentinel负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端连接Redis集群时,会首先连接Sentinel,通过Sentinel查询主节点的地址,然后客户端直接去连接主节点进行数据交互。主节点故障时,客户端再向Sentinel获取新的主节点地址。

6.3 Cluster

​ Redis Cluster是Redis作者自己提供的去中心化的Redis集群化方案。Redis Cluster中每个节点负责存储一部分数据,节点之间通过特殊的二进制协议交互集群信息。Redis Cluster最少需要三个主节点,一般会为每个主节点配置一个(或多个)从节点,某个主节点故障时,集群会自动将该主节点的某个从节点提升为主节点。如果发生故障的主节点没有从节点,那么理论上整个集群将不可用,不过Redis提供了参数“cluster-require-full-coverage”可以允许部分节点故障,其它节点还可以继续提供对外访问。

​ Redis Cluster将所有的数据划分为16384个槽(solt),每个节点负责其中一部分槽位,槽位的信息存储在每个节点中。当Redis Cluster的客户端连接集群时,它会得到一份集群的槽位配置信息,这样当客户端需要查找某个key时,可以直接定位到目标节点。

6.3.1 槽定位算法

​ Cluster默认使用crc16算法进行hash再对16384取模来得到具体槽位。Cluster提供有hash tag允许用户使用指定的tag来定位槽。如果key包含{...}模式,则只使用“{”和“}”之间的字符串进行哈希以定位槽,例如key是"xxxxxsdfdf{hello}",或“fff222222{hello}”的,都只会使用“hello”来定位槽。

6.3.2 跳转

​ 当客户端向一个错误的节点发送指令,该节点发现key所在的槽位不归自己管理,这时它会向客户端发送一个特殊的跳转指令MOVED携带目标操作的节点地址,告诉客户端去连接这个节点。

6.3.3 迁移

​ Redis Cluster迁移的单位是槽,Redis一个槽一个槽进行迁移,当一个槽正在迁移时,这个槽就处于中间过渡状态。这个槽的源节点的状态为migrating,在目标节点的状态为importing,表示数据正在从源流向目标。

​ 迁移工具redis-trib首先会在源和目标节点设置好中间过渡状态,然后一次性获取源节点槽位的所有key,再挨个key进行迁移。每个key迁移过程是:从源节点获取内容,发送到目标节点,目标节点存入内存,源节点删除内容。注意,这里的迁移过程是同步的,在目标节点存入内存到源节点删除key之间,节点的主线程会处于阻塞状态,知道key被成功删除。所以集群环境下要避免大key的产生

6.3.4 节点下线

​ 因为Redis Cluster是去中心化的,一个节点认为某个节点失联了并不代表所有节点都认为它失联了。只有当大多数节点都认定某个节点失联了,集群才认为该节点需要进行主从切换来容错。

7. 参考文献

  1. 《Redis深度历险:核心原理和应用实践》

lvnux
25 声望4 粉丝