Redis学习-知识梳理

Redis学习-知识梳理

0x00 系列文章目录

  1. Redis学习-安装和启动
  2. Redis学习-知识梳理
  3. Redis学习-FAQ

0x01 摘要

Redis 是 REmote DIctionary Server的缩写。

本文主要讲解redis基本概念、集群、过期和内存机制、事务、数据类型、应用等,希望大家阅读后有所收获。

0x02 Redis是什么

Redis学习-知识梳理_第1张图片

2.1 简介

Redis本质上是一个Key-Value类型的内存型数据库。因为是纯内存操作,Redis的性能非常出色,读写可达10万/S,是已知性能最快的Key-Value DB。

2.2 Redis架构

2.3 Redis的线程模型

2.3.1 Redis线程模型简介

Redis学习-知识梳理_第2张图片

Redis-client 在操作的时候,会产生具有不同事件类型的 Socket。

在服务端,有一个 I/O 多路复用程序,将客户端传递过来的Socket置入队列之中。然后,file event dispatcher 依次去队列中取,转发到不同的event handler中。

需要说明的是,这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库,大家可以自行去了解。

2.3.2 提高CPU利用率方法

如果想进一步提高CPU利用率,可以在一个机器上部署多个Redis实例(Redis客户端可以通过一致性哈希来处理多个Redis实例),也可以使用数据分片。

2.3.3 Redis单线程速度快原因

Redis采用的是基于内存的单进程单线程模型,由C语言编写,官方说QPS可以达到100000+。
Redis学习-知识梳理_第3张图片
纵轴QPS,横轴连接数。

  • 基于内存,读写速度分别达到10万/20万,是已知性能最快的Key-Value DB ;
  • kv时间复杂度为(1)
  • 数据结构简单,且针对不同数据类型做了专门的优化
  • 处理请求的线程模型为单线程,避免CPU上下文切换和线程竞争(如加锁解锁)
  • 线程使用 IO 多路复用模型

更多关于为什么说Redis是单线程的以及Redis为什么这么快

2.3.4 IO多路复用

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

2.3.5 Redis单线程的好处

  • 代码逻辑简单,设计模型清晰
  • 没有多线程竞争带来的锁问题、进程切换小号

2.4 Redis单点吞吐量

TPS 80k/s,QPS 100k/s。

0x03 Redis数据类型

Redis支持丰富的数据类型,主要有:字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets) , bitmaps, hyperloglogs 和 地理空间(geospatial)。

注意在 list、Sets、 Sorted Sets和Hashes 为空时删除 key,并在用户试图添加元素而key不存在时创建空元素的 list、Sets、 Sorted Sets和Hashes,是 Redis 的职责。

3.1 String

3.1.1 概念

String是最简单Redis类型,就是一个key对应一个value的pair。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器(注:memcache的数据仅保存在内存中,服务器重启后,数据将丢失)。

String能表示3种类型:字符串、整数和浮点数。根据场景可自动转型,并且根据需要选取底层的实现方式。

String value内部存储结构:

  • int 存放整型数据
  • sds 存放字节/字符串和浮点型数据。

看具体底层type
Redis学习-知识梳理_第4张图片

3.3.2 用途

  • 利用INCR命令簇(INCR, DECR, INCRBY)来把字符串当作原子计数器使用。
  • 使用APPEND命令在字符串后添加内容。
  • 将字符串作为GETRANGE 和 SETRANGE的随机访问向量。
  • 在小空间里编码大量数据,或者使用 GETBIT 和 SETBIT创建一个Redis支持的Bloom过滤器。

3.3.3 命令

  • set:设置指定key的value
  • get:获取指定key的value
  • strlen key
    返回底层字节数组长度。中文取决于redis client字符编码。
    Redis学习-知识梳理_第5张图片
    这个就是所谓二进制安全,即数据都要转为字节数组。
  • 更多命令请点击这里

3.3.4 示例

String类型最简单的操作如下:

> set mykey somevalue
OK
> get mykey
"somevalue"

可以用String类型做原子类型计数器,例子如下:

> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

上文中INCR 命令将字符串值解析成整型,将其加一,最后将结果保存为新的字符串值,类似的命令有INCRBY, DECR 和 DECRBY。实际上他们在内部就是同一个命令,只是看上去有点儿不同。

INCR是原子操作意味着什么呢?就是说即使多个客户端对同一个key发出INCR命令,也决不会导致竞争的情况。如下情况永远不可能发生:『客户端1和客户端2同时读出“10”,他们俩都对其加到11,然后将新值设置为11』。最终的值一定是12,read-increment-set操作完成时,其他客户端不会在同一时间执行任何命令。

3.2 Hash

3.2.1 概念

Redis Hash类型类似Java中的HashMap,他是一个拥有指定key名字的,由field和value组成的键值对映射表,注意field和value都是String。Hash 可以存储2^32 - 1 个键值对(40多亿)。值得注意的是,小的(100个左右字段) hash 被用特殊方式编码,非常节约内存,所以你可以在一个小型的 Redis实例中存储上百万的对象。

Hash主要由hashtable和ziplist两种承载方式实现。也List相同,对于数据量较小的map,Hash底层采用ziplist。注意,和list的ziplist实现不同的是,map对应的ziplist的entry个数总是2的整数倍,奇数存放key,偶数存放value

hashtable内部结构主要分为三层,自底向上分别是dictEntry、dictht、dict:

  • dictEntry:管理一个key-value对,同时保留同一个桶中相邻元素的指针,维护哈希桶的内部链
  • dictht:维护哈希表的所有桶链
  • dict:当dictht需要扩容/缩容时,用于管理dictht的迁移

Redis是单线程处理请求,迁移和访问的请求在相同线程内进行,所以不会存在并发性问题。

3.2.2 用途

Redis Hash一般用于存储对象。

3.2.3 命令

  • hset:可向hash中某key对应的单个field设置value
  • hmset:可向hash中某key对应的多个field设置value
  • hget:可从hash中某key对应的单个field获取value
  • hmget:可从hash中某key对应的多个field获取value
  • hgetall:可从hash中获取某key对应的所有field的value
  • 更多命令请点击这里

3.2.4 示例

以下是一个简单示例:

> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"

上面这个示例展示了对一个key为"user:1000"的hash对象的键值对的操作。

3.3 List

3.3.1 概念

按插入顺序排序的字符串元素的集合,数据结构是双向链表(linked list)。这意味着在一个list中有数百万个元素,在头部或尾部添加一个元素的操作,其时间复杂度也是常数级别O(1)的。但是,如果要用索引进行随机访问,那么效率会比较低O(N)。Redis list 使用链表的主要考虑就是能快速插入数据。

list类型的value对象内部以linkedlist或ziplist承载。当list的元素个数和单个元素的长度较小时,redis会采用ziplist实现以减少内存占用,否则采用linkedlist结构

ziplist的内部结构
所有内容被放置在连续的内存中。其中zlbytes表示ziplist的总长度,zltail指向最末元素,zllen表示元素个数,entry表示元素自身内容,zlend作为ziplist定界符。

3.3.2 用途

  • List 可以做简单的消息队列MQ(利用LPUSH和RPOP或是阻塞的BRPOPLPUSH和BRPOP,详情点击这里 )。
  • 利用 lrange 顺序范围访问命令做基于 Redis 的高性能分页功能。

3.3.3 命令

  • lpush:可向list的左边(头部)添加一个新元素
  • rpush:可向list的右边(尾部)添加一个新元素
  • lrange:可从list中取出一定范围的元素
  • pop:从list中删除元素并同时返回删除的值。可以lpop或rpop
  • ltrim:截取list从左边开始指定长度的元素
  • 更多命令请点击这里

3.3.4 示例

> rpush mylist A B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
> rpop mylist
"B"
> rpop mylist
"A"
> rpop mylist
"first"
> rpop mylist
(nil)

注意:LRANGE 带有两个索引,一定范围的第一个和最后一个元素。这两个索引都可以为负来告知Redis从尾部开始计数,因此-1表示最后一个元素,-2表示list中的倒数第二个元素,以此类推。

3.4 Set

3.4.1 概念

Set是不重复且无序的字符串元素的集合,类似java中的hashset,增删查的时间复杂度都为O(1),可以求交集、并集、差集等。

Set以intset或hashtable来存储。hashtable中的value永远为null;当set中只包含整数型的元素时,则采用intset。

intset的内部结构:

  • intset核心是一个字节数组,从小到大有序地存放set的元素
  • 由于元素有序排列,所以此时set的获取操作采用二分查找方式实现,复杂度O(log(N))。
  • 进行插入时,首先通过二分查找得到本次插入的位置,再对元素进行扩容,再将预计插入位置之后的所有元素向右移动一个位置,最后插入元素,插入复杂度为O(N)。删除类似。

3.4.2 用途

  • 存储唯一值如身份证号
  • 标签系统,如可以将拥有某tag的所有对象放入该tag为key的set,也可以反过来把某对象拥有的tag放入该对象为key的set

3.4.3 命令

  • sadd:添加若干元素到指定key的set中
  • smembers:输出指定key的set的所有元素
  • sismember:判断指定元素是否在指定key对应的set中
  • sinter:求若干key对应的set的元素交集
  • spop:随机返回一个set中的元素并删除该元素
  • 更多命令请点击这里

3.4.4 示例

> sadd myset a b c
(integer) 3
> smembers myset
1. c
2. a
3. b
> sismember myset c
(integer) 1
> sismember myset d
(integer) 0

3.5 Sorted Set

3.5.1 概念

类似Set,但Sorted Set中的每个字符串元素关联到一个double类型的score浮动数值,集合中的元素按 Score 进行升序排序。

Sorted Set中添加,删除和更新元素的操作时间复杂度是(O(log(N))).

内部结构以ziplist或skiplist+hashtable来实现
注意:有序集合的成员是唯一的,但分数(score)却可以重复。
所以它是可以进行有序搜索的元素集合(例如取出前面10个或者后面10个元素)。

3.5.2 用途

Sorted Set可以做排行榜应用如求 TOP N 、范围查找等。

3.5.3 命令

redis-shell里面SortedSet称为zset

  • zadd:添加若干元素到指定key的set中
  • zrange:输出指定key的指定范围内的元素,可以跟WITHSCORES来输出分数
  • zrank:获取指定key元素的排序序号
  • 更多命令请点击这里

3.6 Bitmap

可以参考一篇很不错的文章,点击这里

3.6.1 概念

通过特殊的命令,你可以将 String 值当作一系列 bits 处理:可以设置和清除单独的 bits,数出所有设为 1 的 bits 的数量,找到最前的被设为 1 或 0 的 bit,等等。
其实Redis中的Bitmap并不是一个真正的数据类型,而是一个在String类型上定义的面向bit的操作的集合。由于String类型是二进制安全的,并且它们的最大长度为512 MB,因此它们可以用2 ^ 32个不同的bit表示。

BIt操作可以分为两种,一种是对单个位进行操作比如某个bit位为1或0或是获取这个bit的值;还有一种是对bit组进行操作,比如计算给定范围内的bit数(如人口统计)。

BitMap的其中一个最大的好处就是存储数据时通常可以节约大量的空间。比如有40亿条自增的用户ID,我们可以用Bitmap结构,512MB的内存空间就能存下。

底层是字节数组来存放,可扩容。

3.6.2 用途

  • 各种实时分析
  • 存储与对象ID相关联的节省空间但又高性能的boolean数据
  • 计算活跃用户,点击这里

3.6.3 命令

  • setbit key 10 1:将指定key中数字10的代表的bit设置为1
  • getbit key 10:获取指定key的数字10代表的bit
  • bitcount key :统计指定key的bitmap中的bit为1的数目
  • bitop:可以对多个bitmap做操作,以下示例结果都放在destkey
    • BITOP AND destkey srckey1 srckey2 srckey3 … srckeyN
    • BITOP OR destkey srckey1 srckey2 srckey3 … srckeyN
    • BITOP XOR destkey srckey1 srckey2 srckey3 … srckeyN
    • BITOP NOT destkey srckey

3.6.4 示例

> setbit key 10 1
(integer) 1
> getbit key 10
(integer) 1
> getbit key 11
(integer) 0
> setbit key 100 1
(integer) 0
> bitcount key
(integer) 2

3.6.5 注意事项

普通用户使用Redis bitmap时需要注意

3.7 HyperLogLog

3.7.1 概念

首先说下基数的概念:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。

所谓基数估计,就是在误差可接受的范围内,快速计算基数。

HyperLogLog是被用于估计一个 set 中元素数量的概率性的数据结构。Redis HyperLogLog 是用来做基数统计的算法。HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

3.7.2 用途

HyperLogLog被用于估计一个 set 中元素数量的概率性的数据结构。

3.7.3 命令

  • pfadd:添加若干元素到HyperLogLog中
  • pfcount:返回给定HyperLogLog的基数估计值
  • pfmerge:将多个HyperLogLog合并为一个

3.7.4 示例

> PFADD runoobkey "redis"
1) (integer) 1
> PFADD runoobkey "mongodb"
1) (integer) 1
> PFADD runoobkey "mysql"
1) (integer) 1
> PFCOUNT runoobkey
(integer) 3

3.8 Geo

3.8.1 概念

Redis 3.2 推出了GEO , 这个功能可以储存用户给定的地理位置, 并对这些信息进行操作。GEO 的数据结构总共有六个命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash,这里着重讲解几个。

3.8.2 用途

Geo被用于储存地理位置

3.8.3 命令

  • geoadd:将给定的空间元素(纬度、经度、名字)添加到指定的键里面。 这些数据会以有序集合的形式被储存在键里面, 从而使得像 GEORADIUS 和 GEORADIUSBYMEMBER 这样的命令可以在之后通过位置查询取得这些元素。
  • geopos:返回给定HyperLogLog的基数估计值
  • geodist:将多个HyperLogLog合并为一个
  • georadius
  • georadiusbymember
  • gethash

3.8.4 示例

3.9 小结

关于前面几种数据类型和实现原理总结如下:

名称 原理 时间复杂度
String int(整形数据) sds(字符、浮点) O(1)
Hash ziplist(连续内存,奇entry放key,偶放value) hashtable(类似java) 查找O(1)
List ziplist(连续内存,元素少时用) linkedlist(双向链表) poo push O(1),index O(N)
Set intset(只包含整数时用;字节数组) hashtable(value为null) intset时,获取时用二分查找O(log(N));插入O(N)
HashTable时,O(N)
SortedSet ziplist;跳表+hashtable O(log(N))

0x04 Redis持久化

4.1 持久化介绍

Redis主要提供了两种持久化机制RDB和AOF:

4.1.1 RDB

round-db
默认开启。RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储到磁盘。

Redis具体做法是fork一个子进程,将父进程的数据复制到内存然后写入临时文件,持久化过程结束后将此临时文件替换旧的快照文件。执行完毕后子进程退出。

注意:RDB方式在复制数据过程中会耗费大量内存,甚至在内存不足时会阻塞服务器运行直到结束复制,所以会引起大量IO,性能影响大。

还需要留意的是最后一次持久化的数据可能会丢失。

4.1.2 AOF

全名 append only file
AOF以redis协议来记录每次对服务器写的操作,追加的方式保存到持久化文件。当服务器重启的时候会replay(重放)这些命令来恢复原始的数据。

AOF主要分为两种方式:

  • 来一个写操作写一个本地
  • 每秒定时写本地

Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。具体来说,就是当日志文件大到一定程度的时候,会fork出一个进程来遍历父进程内存中的数据,每条记录对应一条set语句,写到临时文件中,然后再替换到旧的日志文件(类似rdb的操作方式)。默认触发条件是当AOF文件大小是上次重写后大小的一倍且文件大于64M。

4.1.3 不开启持久化

开启持久化缓存机制,对性能会有一定的影响。所以如果你只希望你的数据在服务器运行的时候存在可以不使用任何持久化方式.

4.1.4 同时开启两种持久化

可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

4.2 如何选择合适的持久化方式?

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。

  • 有很多用户都只使用AOF持久化,但并不推荐这种方式:因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快。

  • 用RDB恢复内存状态会丢失很多数据,重放AOP日志又很慢。Redis4.0推出了混合持久化来解决这个问题。将 RDB 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

0x05 Redis集群

Redis 3.0以后,提供了完整的Sharding(分片)、replication(主备感知能力)、failover(故障转移)新特性。

5.1 Partition分区

5.1.1 分区方案

5.1.1.1 Range分区

将不同范围的key映射到不同实例,缺点是需要维护映射关系

5.1.1.2 Partition分区
  1. 使用partitioner (如 crc32 )将key转换为数字。
  2. 将散列值对Redis实例总数取模,结果就是该key对应的实例。

4个Redis实例时的一个例子如下:
crc32(key:foobar)=93024922
93024922 % 4 = 2
所以 key foobar 会被存储到第2个Redis实例。

5.1.1.3 Redis Sharding
  • Redis Sharding
    Redis Sharding是Redis Cluster之前业界普遍使用的Redis集群方案。其主要思想是采用MURMUR_HASH一致性哈希算法,将key和节点name同时hashing,然后进行映射匹配。相较于哈希求模映射的好处是当增减节点时,不需rehash。一致性哈希只影响增减节点的相邻节点的key分配,影响较小。

    Jedis已支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool。

  • ShardedJedis的虚拟节点
    为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增减Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。

  • ShardedJedis的keyTagPattern
    ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。

5.1.2 分区实现

  • 客户端分区
    由客户端指定数据读写时映射的Redis实例。我们已经在生产环境中使用了改造过的实现了一致性哈希的客户端。

  • 代理分区
    客户端请求走代理,代理根据分区规则请求相应实例最后把结果返回给客户端,可以参见Twemproxy。twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。也就是说,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。

    由于使用了中间件,twemproxy可以通过共享与后端系统的连接,降低客户端直接连接后端服务器的连接数量。同时,它也提供sharding功能,支持后端服务器集群水平扩展。统一运维管理也带来了方便。

    当然,也是由于使用了中间件代理,相比客户端直连服务器方式,性能上会有所损耗,实测结果大约降低了20%左右。

  • 查询路由
    客户端请求发送到任一Redis节点,Redis节点将客户端请求重定向到正确的节点。

Redis集群是自动分片和高可用的首选方案,是客户端分区和查询路由的结合使用。

5.1.3 分区缺点

  • 不能用命令直接操作分区不同的key
  • 不同分区的多个key不能使用事务
  • 牢记分区的粒度是key,也就是说一个key的巨大set只能存在一个节点,不能分散存储
  • 分区时,RDB和AOF处理变得很复杂,必须综合各个节点的这些文件
  • 分区后,集群扩缩容变得很麻烦

5.1.4 预分区

Redis十分轻量级(单实例1M内存),为应对未来扩容,可以初始启动较多实例。只有一台服务器时也可以让Redis以分布式的方式运行:在同一台服务器上启动多个实例。

当数据增长需要更多的Redis服务器资源时,只需将Redis实例从一台服务器迁移到另外的服务器,不需再重新分区。

Redis复制技术可以做到极短或者不停机,操作流程如下:

  1. 在新服务器启动一个空Redis实例且将该实例配置为原实例的slave节点
  2. 暂停客户端,更新Redis实例IP配置
  3. 在新Redis实例中执行SLAVEOF NO ONE命令
  4. 更新配置后重启客户端
  5. 停止旧服务器上的Redis实例

5.1.5 Redis集群Slot

Redis 集群没有使用一致性hash, 而是引入了 Hash Slot 的概念,即将所有数据划分为16384个分片(slot),每个节点会对应一部分slot,每个key都会根据分布算法映射到16384个slot中的一个,分布算法如下:

Slot_ID = crc16(key) % 16384

当一个Client访问的key不在对应节点的slots中,Redis会返回给client一个moved命令来告知正确的路由信息,Client据此重新发起请求。同时,Client会根据每次请求来缓存本地的路由缓存信息,以便下次能直接路由到正确的节点。

举个例子,比如当前集群有3个节点,那么:

  • 节点 A 包含 0 到 5500号slot.
  • 节点 B 5501 - 11000
  • 节点 C 11001 - 16384
    这种结构很容易添加或者删除节点:
  • 添加个节点D, 只需从节点 A, B, C中得部分槽到D上.
  • 移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可
    注意:从一个节点将哈希槽移动到另一个节点并不会停止服务

分片迁移:分片迁移的触发和过程控制由外部系统完成,Redis只提供迁移过程中需要的原语支持,主要包含两种:

  • 节点迁移状态设置,即迁移前标记源、目标节点;
  • key迁移的原子化命令

5.1.6 一致性哈希

可参考:

  • Redis集群的一致性Hash及代码演示
  • 对一致性Hash算法,Java代码实现的深入研究
  • Redis 一致性hash算法
  • Redis分布式算法 — Consistent hashing(一致性哈希)
    分布式的存储系统如果采用普通的hash方法,将数据按规则映射到某个节点,如mod(key,n)(key是数据的key,n为分布式集群节点数)。当集群有节点增减时,所有数据映射都将失效。

一致性哈希算法是分布式系统中常用的算法,他解决了普通求余类Hash算法伸缩性差的问题,可保证在增减节点的情况下尽量有多的请求命中原来路由到的服务器,减少受影响的数据量。
Redis学习-知识梳理_第6张图片

  1. 环形Hash空间按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中,节点机器名和数据key都用此算法来hash
  2. 数据按hash值顺时针遇到的第一个机器放入。比如上图o3 o4存入了节点t2
  3. 有节点被下掉时,其上的数据按顺时针移入下一个节点
  4. 有节点添加时,算出该新节点hash位置,以该位置为起点逆时针往前直到前一个节点hash位置之间的数据都迁移到该新节点中。
  • 算法优点
    这种算法可保持单调性,在集群节点发生变化时减少了数据迁移量。
  • 算法缺点
    因为节点hash结果比较随机,所以可能造成节点上的数据并非均匀分配,设置可能某几个节点数据特别多,其他节点数据特别少的极端情况。
  • 虚拟节点
    为了数据更均匀存储到集群内节点,可将每个节点增加若干虚拟节点,比如Node1为Hash(192.168.1.1),Node1.1为Hash(192.168.1.1#1),Node1.2为Hash(192.168.1.1#2):
    Redis学习-知识梳理_第7张图片

5.2 MasterSlave与FailOver

Redis 集群是一个提供在多个Redis节点间共享数据的程序集,他通过sharding分区来提供一定程度的可用性。当某个节点宕机或者不可达时可继续服务。

Redis 集群的优势:

  • 自动分割数据到不同的节点上。
  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

5.2.1 主从复制

还可参考redis主从同步原理(浅谈)

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,Redis集群使用了Master Slave模型,每个节点都会有N个slave,而每个slave也可以拥有多个slave。这种Master-Slave结构可以增强扩展性即使用多个Slave来处理只读的请求(比如,繁重的排序操作可以放到从服务器去做),也可以只是用来做数据冗余。

例如A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。

然而如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点A1,B1,C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务。只有当B和B1 都失败后,集群才不可用。 示意图如下:
Redis学习-知识梳理_第8张图片
Redis主从复制分为全量同步和增量同步。Master-Slave刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。两种同步方式如下:

  • 增量同步
    当客户端将数据写入主节点后,主节点会返回response,然后将写操作赋值给他的从节点。请注意这个复制过程是非阻塞的,也就是说Master和Slave(当初始同步完成后,需要删除旧的数据集和加载新的数据集。这个短暂的时间内会阻塞请求)还能在此期间处理客户端请求。示意图如下:
    Redis学习-知识梳理_第9张图片
  • 全量同步
    Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份,如下图:
    Redis学习-知识梳理_第10张图片
    具体步骤如下:
  1. slave连接master,发送SYNC命令;
  2. master接收到SYNC命令后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  3. master BGSAVE执行完后,向所有slave发送快照文件,并在发送期间继续记录被执行的写命令;
  4. slave收到快照文件后丢弃所有旧数据,载入收到的快照RDB文件;
  5. master快照发送完毕后开始向slave发送缓冲区中的写命令;
  6. slave完成对快照的载入,开始接收命令请求,并执行来自master缓冲区的写命令;
  • 断点续传
    在master服务器内存中给每个slave服务器维护了一份同步日志和同步标识,每个slave在跟master进行同步时都会携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后,slave每隔(默认1s)主动尝试和master进行连接,如果slave携带的偏移量标识还在master服务器上的同步备份日志中,那么就从slave发送的偏移量开始继续上次的同步操作;否则(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内master服务器接收到大量的写操作),必须进行一次全量更新。

  • 不落盘复制
    在网络良好磁盘条件较差情况下可以使用此方式,Master数据不写RDB文件而是通过网络Socket直接发送给Slave

  • 限制有N个以上从服务器才允许写入
    可配置master连接N个以上slave才允许写操作。但由于Redis使用的是异步主从复制,不能完全确保slave确实收到了要写入的数据,所以还是有一定可能丢失数据。
    这一特性的工作原理如下:
    1)slave每秒钟ping一次master,确认处理的复制流数量
    2)master记住每个从服务器最近一次ping的时间
    3)可以配置最少要有N个服务器min-slaves-to-write有小于M秒的确认延迟min-slaves-max-lag
    4)如果有N个以上从服务器,并且确认延迟小于M秒,主服务器接受写操作

注意:

  • Slave中过期key的处理
    由master负责key的过期删除处理,然后将相关删除命令已数据同步的方式同步给slave服务器进行删除
    本地的key。
  • Master关闭持久化
  • 可以通过修改Master端的redis.config来避免在Master端执行持久化操作(Save)使得Master避免数据写入磁盘的消耗,由Slave端来执行持久化。但要避免Master挂掉重启,必须关掉自动重启,此时数据为空必须重新同步数据。

5.2.2 FailOver

  • 故障发现
    节点间两两通过TCP保持连接,周期性进行PING、PONG交互,若对方的PONG响应超时未收到,则将其状态置为PFAIL,并传播给其他节点。

  • 故障确认
    当集群中有一半以上的节点对某一个PFAIL状态进行了确认,则将该节点状态改为FAIL,确认其发生故障。

  • Slave选举
    当有一个master挂掉了,则其slave重新竞选出一个新的master。主要根据各个slave最后一次同步master信息的时间,越新表示slave的数据越新,竞选的优先级越高,就更有可能选中。选举成功之后将消息广播给其他节点。

  • 手动故障转移
    在某些时候比如Redis节点需要升级时,可通过设置目标Master节点为Slave再升级。流程如下:

  1. 客户端不再链接我们要淘汰的master
  2. master向slave发送需要复制的数据偏移量
  3. slave得到复制偏移量后开始故障转移,接着通知master进行配置切换
  4. 当客户端从旧的master上解锁后重新连接到新的master

5.2.3 集群不可用

当Redis集群中任意master挂掉且当该master没有slave或集群中有超过半数以上master挂掉时,整个Redis集群服务就不可用了。

5.3 集群一致性

Redis 并不能保证数据的强一致性,也就是说集群在特定的条件下可能会丢失写操作,原因有:

  • 集群master slave间异步复制
    这个过程总可能出现数据没有成功赋值给slave节点的情况。
  • 网络分区
    一个客户端与至少包括一个主节点在内的少数节点被孤立分区。示例:
    集群包含 A 、 B 、 C 3个master,分别有一个slave节点。当发生网络分区,一部分为节点 A 、C 、A1 、B1 和 C1 ,另一部为B 和客户端 。
    此时客户端然能够向主节点B正常写入。如果分区很快恢复,那么集群也会恢复;如果分区过长以至于B1被选举为新的master,那么客户端写入B中得数据便丢失了。

每个节点内部都将集群的配置信息存储在ClusterState中,通过自增的epoch变量来使集群配置在各个节点间保持一致。

5.4 Sentinel哨兵

5.4.1 哨兵的主要任务

Redis 哨兵用于管理多个 Redis 节点,主要任务如下:

  • 监控
    哨兵不断地检查Redis的各master和slave节点是否正常工作
  • 通知
    当被监控的某个 Redis 节点异常时, 哨兵通知管理员
  • 自动故障迁移(failover)
    当master失效时,哨兵开启failover进程,将备选的其中一个slave选举为master
  • 提供配置信息
    客户端可连接哨兵来获取master地址,在failover的时候哨兵会将新的master地址告知客户端

5.4.2 哨兵程序的本质

虽然 Redis Sentinel 释出为一个单独的可执行文件 redis-sentinel , 但实际上它只是一个运行在特殊模式下的 Redis 服务器, 你可以在启动一个普通 Redis 服务器时通过给定 –sentinel 选项来启动 Redis Sentinel 。

哨兵程序本身就是一种分布式协作的程序,通过多数投票来决定节点是否可用。哨兵与其他哨兵进行通信,互相检查可用性并进行信息交换。单个哨兵对Redis节点做出已经下线的判断成为主观下线(SDOWN),多个哨兵协商、认定的下线称客观下线(ODOWN)(只适用于master,slave下线不需要协商所以是主观下线)。master被发现处于ODOWN后,会被推举出的哨兵执行自动failover。

客户端可以将哨兵看作是一个只提供了订阅功能的 Redis 服务,订阅频道来获取相应事件,比如, 名为 +sdown 的频道就可以接收所有实例进入主观下线(SDOWN)的事件。当failover发生时,在选定slave变为master后,可以通过发布订阅功能将更新后的配置广播给其他哨兵更新配置。

哨兵的状态信息会持久化到磁盘中的配置文件中,也就是说可以安全重启哨兵程序。

为了防止分区,可通过min-slaves-to-write进行最小slave, 让主服务器在连接的从实例少于给定数量时停止执行写操作, 与此同时, 应该在每个运行 Redis 主服务器或从服务器的机器上运行 Redis Sentinel(哨兵) 进程。

5.4.2 Raft算法

Redis 哨兵 failover机制 使用 Raft 算法来选举领头(leader) 哨兵 , 从而确保在一个epoch里, 只有一个leader 哨兵产生。更高的epoch总是优于较小的epoch, 因此每个哨兵都会主动使用更新的epoch来更新自己的配置。

Raft算法主要用于分布式系统的系统容错和选leader,Redis哨兵使用其核心原则如下:

  1. 所有哨兵都可能被选为leader,且都有唯一的uid,不会因为重启而变更
  2. 每个哨兵都会要求其他哨兵选举自己为leader(由发现redis节点客观下线的哨兵率先发起选举)
  3. 每个哨兵只有一次选举的机会
  4. FIFO
  5. 一旦加入到系统了,则不会自动清除
  6. 选出leader的条件是 N/2 + 1个哨兵选择了自己
  7. 采用配置epoch,如果选举出现脑裂,则配置epoch会自增,进入下一次选举,所有哨兵都会处于统一配置epoch,以最新的为标准。

5.4.3 Slave选举机制

当因为master处于ODOWN状态和哨兵接收到从大多数已知的哨兵实例发来的授权时会开启failover过程,这时候会开启一次新的master选举,该过程主要会读slave以下信息进行评估:

  • 与master断开时间
  • slave优先级,数值越小代表优先级越高,在redis.conf配置
  • 已处理的复制偏移量。优先级相等的情况下比较此量值
  • 运行ID号。当上两项相同时,选择按字典顺序排列的较小运行ID。

5.4.4 -BUSY状态

当 Lua 脚本的运行时间超过指定时限时, Redis 就会返回 -BUSY 错误。

当出现这种情况时, 哨兵在尝试执行failover之前会先向服务器发送一个 SCRIPT KILL 命令。 如果正在执行的是一个只读脚本的话就会被杀死, 然后回到正常状态;如果还是错误,就会执行failover。

6 Redis 过期及内存淘汰机制

6.1 引子:

Redis 只能存 5G 数据,可是你写了 10G,那会删 5G 的数据。怎么删的?
数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,为什么?

回答:Redis 采用的是定期删除+惰性删除策略。

6.2 为什么不用定时删除策略

定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。

6.3 随机选key定期删除+惰性删除是如何工作

  • 随机选key定期删除
    Redis 默认每隔 100ms 随机抽取Key检查是否有过期的,有过期就删除。因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。
  • 惰性删除
    Redis在获取某个Key 时会先检查是否设置了过期时间,如果已设置且已过期就会删除。

但采用定期删除+惰性删除,就一切安好?

NO。如果定期删除没抽取到、程序也没请求过期key时,Redis的内存占用会越来越大。此时就要采用内存淘汰机制。

6.4 内存淘汰机制

在 redis.conf 中配内存淘汰策略的配置如下:

# maxmemory-policy noeviction
  • noeviction
    默认值。当内存不足以容纳新写入数据时,新写入操作会报错。
    不推荐使用。

  • allkeys-lru
    当内存不足以容纳新写入数据时,在key中移除最近最少使用的 Key(LRU)。
    推荐使用。

  • allkeys-random
    当内存不足以容纳新写入数据时,在key中随机移除某个 Key。
    不推荐使用。

  • volatile-lru
    当内存不足以容纳新写入数据时,在设置了过期时间的key中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
    不推荐。

  • volatile-random
    当内存不足以容纳新写入数据时,在设置了过期时间的key中,随机移除某个 Key。
    不推荐。

  • volatile-ttl
    当内存不足以容纳新写入数据时,在设置了过期时间的key中,有更早过期时间的 Key 优先移除。
    不推荐。

如果没有设置 expire 的 Key,那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。

0x07 Redis事务

7.1 概念

7.1.1 传统事务

传统事务可以一次执行多个命令, 并且带有以下两个重要的保证:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

7.1.2 Redis事务

  • Redis事务不同于传统事务
    但请注意,Redis事务和传统事务是不同的,即使我们的事务中某个命令操作失败,我们也无法在这一组命令中让整个状态回滚到操作之前。具体查看7.3 事务错误7.4 事务回滚

  • 原理
    Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。

    Redis事务的原理是先将属于一个事务的若干命令发送给Redis,然后再让Redis依次执行这些命令。

  • 事务执行流程
    一个Redis事务从使用MULTI命令开始到执行EXEC命令开启事务会经历以下三个阶段:

    1. 开始事务。
    2. 命令入队。
    3. 执行事务。
  • EXEC与事务
    EXEC命令负责触发并执行事务中的所有命令:

    • 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,则Redis会清空事务队列,那么事务中的所有命令都不会被执行。
    • 如果客户端成功在开启事务之后执行 EXEC命令 ,事务中的所有命令都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。
  • 严格保证事务命令顺序
    除此之外,Redis的事务还能保证一个事务内的命令依次执行而不被其他命令插入。试想客户端A需要执行几条命令,同时客户端B发送了一条命令,如果不使用事务,则客户端B的命令可能会插入到客户端A的几条命令中执行。如果不希望发生这种情况,也可以使用事务。详细请看7.5 Redis事务期间不响应其他请求

  • DISCARD放弃事务
    当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出

  • 脚本与事务
    Redis中的脚本也是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

7.2 事务与持久化

当使用 AOF 方式做持久化的时候, Redis 会使用 write(2) 命令将事务落盘。

如果 Redis 服务器因为某些原因被杀死或硬件故障,那么可能只有部分事务命令会被成功写入到磁盘。

如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。使用redis-check-aof程序可以修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。

7.3 事务错误

使用Redis事务时可能遇到两种错误:

  • 事务在执行 EXEC 之前,入队的命令出错(语法错误、内存不足等)

    • Redis 2.6.5 之前
      Redis 2.6.5之前的版本会忽略有语法错误的命令,然后执行事务中其他语法正确的命令。就此例而言,SET key value会被执行,EXEC命令会返回一个结果:1) OK。
    • Redis 2.6.5 开始
      从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。
  • 命令可能在 EXEC 调用之后失败
    举个例子,事务中的命令可能处理了错误类型的key,比如将列表命令用在了字符串键上面。

    这种情况并没有对它们进行特别处理:即使事务中某些命令在执行时出错, 其他命令仍会继续执行 —— Redis 不会停止执行事务中的命令。

7.4 事务回滚

注意: Redis 事务不支持回滚(roll back)!!!
与传统事务不同,Redis 在事务失败时不进行回滚,而是继续执行余下的命令。这么做的好处如下:

  • Redis 命令只会因为语法错误(未在加入事务队列时发现的)而失败,或命令对错误类型key执行。也就是说,这些失败是由编程错误造成的,应该在开发阶段被发现并修复,而不是出现在生产环境。
  • 因为不需支持回滚,所以 Redis 内部处理逻辑简单快捷。

7.5 Redis事务期间不响应其他请求

事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。

因为当多个client端同时发送命令时,redis处理命令的顺序是不确定的.
如A事务中有c,d,e三个命令,B事务中h,l两个命令,当两个客户端同时发送给redis时,redis可能先执行c,然后又执行了h,顺序可能变成 c->h->d->l->e.而期望的顺序是c->d->e->h->l.因此只有redis在事务执行期间,不再响应其他客户端请求,才能保证一个客户端上的事务完整按序执行.

7.6 实例

以下是一个Redis事务的例子:

  1. MULTI 开始一个事务
  2. 后序命令加入到事务队列中,
  3. 最后由 EXEC 命令触发事务, 执行事务队列中的所有命令

shell操作如下:

redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Journey to the West"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "Fiction" "Famous" "Required"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Journey to the West"
3) (integer) 3
4) 1) "Famous"
   2) "Required"
   3) "Fiction"

0x08 内存优化

第三章中讲了Redis的数据类型,可以感受到Redis底层实现是根据数据量和数据类型不同而不同的,换句话说Redis致力于对每种数据类型做特定的优化。

8.1 对集合类型优化

Redis可设阈值,当不超过这个阈值时对这些数据集合采用内存压缩技术进行编码,可节省5-10倍内存空间。

8.2 32位系统

32位系统启动redis,每个key的指针更小,但是最大内存为4G。

8.3 Hash高效存储键值对

当Hash元素非常少时,Redis将数据encode为一个O(N)的数据结构,你可以认为这是一个带有长度属性的线性数组。因为元素非常少和限行数组局部性原理的原因,所以此时使用HGETHSET命令的复杂度仍然是O(1):

当Hash包含的元素太多的时将被转换为正常的Hash,该阈值可以在redis.conf进行配置,示例:

hash-max-zipmap-entries 256
# 相应的最大键值长度设置:
hash-max-zipmap-value 1024

我们假设要缓存的对象使用数字后缀进行编码,如:
object:102393, object:1234, object:5

每次SET的时候,可以把key分为两部分,第一部分当做一个key,第二部当做field。比如“object:1234”,分成两部分:

  • a Key named object:12
  • a Field named 34
    我们使用如下命令:
HSET object:12 34 somevalue

这种优化方式可以将内存节约一个数量级。

8.4 内存分配

Redis内存分配需要注意以下几点:

  • 一般需要通过maxmemory设置最大内存或者在启动后通过 CONFIG SET设置。
  • 如果内存使用偶尔会达到N,那就应该将最大内存设为N。因为malloc()的性质决定,可能不用的key和要继续用的key再一个内存页上,不会立刻归还操作系统。但是这部分内存可以被新的key复用。

0x09 Redis应用

9.1 通过WATCH实现乐观锁

WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。

WATCH命令可以监控一个或多个键,一旦其中有一个键在执行 EXEC 之前被修改(或删除),之后的事务就不会执行, EXEC 命令会返回nil-reply来表示该事务已经失败。

监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)

下面是一个示例:

  • 没有乐观锁的代码:

    val = GET mykey
    val = val + 1
    SET mykey $val
    

    以上代码问题很明显,如果多线程同时这样操作,那val可能出现加少的情况。

  • 加上乐观锁:

    WATCH mykey
    val = GET mykey
    val = val + 1
    MULTI
    SET mykey $val
    EXEC
    

    如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。 用户程序需要不断重试这个操作, 直到没有发生碰撞为止。

    注意:执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用UNWATCH命令来取消监控。

这个乐观锁实现流程如下:

  1. 将本次事务涉及的所有key注册为观察模式
  2. 执行只读操作
  3. 根据只读操作的结果组装写操作命令,并发送到服务器端入队
  4. 发送原子化的批量执行命令EXEC试图执行连接的请求队列中的命令
  5. 如果前面注册为观察模式的key中有一个或多个在EXEC之前被修改过,则EXEC将直接失败,拒绝执行;否则顺序执行请求队列中的所有请求
  6. redis没有原生的悲观锁或者快照实现,但可通过乐观锁绕过。一旦两次读到的操作不一样,watch机制触发,拒绝了后续的EXEC执行

需要注意WATCH的失效时机:

  • 当 EXEC 被调用时, 不管事务是否成功执行, 对所有key的watch都会被删除

  • 客户端断开连接

  • 使用无参数的 UNWATCH 命令可以手动取消对所有键的watch

9.2 原子操作

Redis 使用 WATCH 命令可以创建本来没有的原子操作。

9.2.1 实例1

下面是一个可以原子地弹出有序集合中score最小的元素的实例:

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

程序只要重复执行这段代码, 直到 EXEC 的返回值不是nil-reply回复即可。

9.2.1 实例2

实现一个hsetNX函数,仅当字段存在时才赋值。

为了避免竞态条件我们使用watch和事务来完成这一功能(伪代码):

WATCH key  
 isFieldExists = HEXISTS key, field  
 if isFieldExists is 1  
 MULTI  
 HSET key, field, value  
 EXEC  
 else  
 UNWATCH  
 return isFieldExists

在代码中会判断要赋值的字段是否存在,如果字段不存在的话就不执行事务中的命令**,但需要使用UNWATCH命令来保证下一个事务的执行不会受到影响。**

9.3 Redis分布式锁

9.3.1 基本思想

Redis为单进程单线程模式、采用队列模式将并发访问变成串行访问。且多client对redis的链接并不存在竞争关系。

  1. setnx key value
    • setnx key value,当key不存在时,将 key 的值设为 value ,返回1。
    • 若给定的 key 已经存在,则setnx不做任何动作,返回0。
  2. 当setnx返回1时,表示获取锁成功。
  3. 获取锁的线程执行代码
  4. 做完操作以后del key,释放锁,如果setnx返回0表示获取锁失败

9.3.2 分布式锁特性

只需具备4个特性就可以实现一个最低保障的分布式锁。

  • 独享(相互排斥)
    在任意一个时刻,只有一个客户端持有锁
  • 无死锁
    即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取
  • 容错
    只要大部分Redis节点都活着,客户端就可以获取和释放锁
  • 设定锁和持有锁的为同一个线程

9.3.3 传统Redis分布式锁

大多人使用Redis实现分布式锁的原理:
Redis学习-知识梳理_第11张图片

  1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。比如该客户端崩溃,其他客户端发现这个超时也可以夺取锁,必须使用GETSET,详细流程如下:
    1. C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
    2. C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试;反之,如果已超时,C3通过下面的操作来尝试获得锁:
    3. GETSET lock.foo
    4. 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
    5. 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
  3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

但是这种方式在多个Redis实例 master-slave架构 master挂掉的情况下会出问题:

  1. 客户端A从master获取到锁
  2. 在master将锁同步到slave之前,master宕掉了
  3. slave节点被晋级为master节点
  4. 客户端B取得了同一个key,被客户端A获取到的锁。分布式锁机制失效了

9.3.4 Redlock算法

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。为了取到锁,客户端应该执行以下操作:

  1. 获取当前毫秒时间戳
  2. 使用相同的key和随机值依次尝试从N个实例获取锁。当尝试设置Redis锁时,客户端应该设置一个网络连接和响应超时时间,且该时间应小于锁的失效时间,避免因为阻塞过长时间而客户端还在等待响应。如果该Redis实例没有在指定时间内响应,客户端应该尝试另外的实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁耗时。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁耗时(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
  6. 当客户端无法取到锁时,应该在一个随机延迟后重试,防止多个客户端同时抢夺同一资源的锁,可能会导致脑裂,没有人会取到锁。同时,客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低(必要的重试),所以,理想情况一下,客户端应该并发地向所有Redis实例发送SET命令获取锁。

关于Redis分布式锁更多内容请点击这里

9.3.5 计数器

如知乎每个问题的被浏览器次数

Redis的n种妙用,不仅仅是缓存
set key 0
incr key // incr readcount::{帖子id} 每阅读一次
get key // get readcount::{帖子id} 获取阅读量
分布式全局唯一id(string)

分布式全局唯一id的实现方式有很多,这里只介绍用redis实现

Redis的n种妙用,不仅仅是缓存
每次获取userId的时候,对userId加1再获取,可以改进为如下形式

Redis的n种妙用,不仅仅是缓存
直接获取一段userId的最大值,缓存到本地慢慢累加,快到了userId的最大值时,再去获取一段,一个用户服务宕机了,也顶多一小段userId没有用到

set userId 0
incr usrId //返回1
incrby userId 1000 //返回10001

9.3.6 消息队列-使用list

在list里面一边进,一边出即可

  • 左放右取

    # 一直往list左边放
    lpush key value 
    # key这个list有元素时,直接弹出
    # 没有元素时被阻塞,直到等待超时或发现可弹出元素为止
    # 这里我们设置超时时间为10s
    brpop key value 10 
    

Redis学习-知识梳理_第12张图片

  • 右放左取

    rpush key value
    blpop key value 10
    
  • 新浪/Twitter用户消息列表(list)

    • 假如说小编li关注了2个微博a和b,a发了一条微博(编号为100)就执行如下命令
    lpush msg::li 100
    
    • b发了一条微博(编号为200)就执行如下命令:
    lpush msg::li 200
    
    • 假如想拿最近的10条消息就可以执行如下命令(最新的消息一定在list的最左边):
    # 下标从0开始,[start,stop]是闭区间,都包含
    lrange msg::li 0 9 
    
  • 抽奖活动-set

    # 参加抽奖活动
    sadd key {userId} 
    # 获取所有抽奖用户,大轮盘转起来
    smembers key 
    # 抽取count名中奖者,并从抽奖活动中移除
    spop key count 
    # 抽取count名中奖者,不从抽奖活动中移除
    srandmember key count
    
  • 实现点赞,签到,like等功能(set)

    # 1001用户给8001帖子点赞
    sadd like::8001 1001
    # 取消1001用户对8001帖子点赞
    srem like::8001 1001
    # 检查用户是否给8001帖子点过赞
    sismember like::8001 1001 
    # 获取8001帖子点赞的用户列表
    smembers like::8001 
    # 获取8001帖子点赞用户数
    scard like::8001 
    
  • 实现关注模型,可能认识的人(set)

    • seven关注的人
      sevenSub -> {qing, mic, james}
    • qing关注的人
      qingSub->{seven,jack,mic,james}
    • Mic关注的人
      MicSub->{seven,james,qing,jack,tom}
    # 返回sevenSub和qingSub的交集,即seven和qing的共同关注
    sinter sevenSub qingSub -> {mic,james}
    # 我关注的人也关注他,下面例子中我是mic,他是james
    # qing在micSub中返回1,否则返回0
    sismember micSub james
    sismember micSub qing
    sismember jamesSub qing
    # 我可能认识的人,下面例子中我是seven
    # 求qingSub和sevenSub的差集,并存在sevenMayKnow集合中
    sdiffstore sevenMayKnow qingSub sevenSub -> {seven,jack}
    
  • 电商商品筛选(set)
    每个商品入库的时候即会建立他的静态标签列表如,品牌,尺寸,处理器,内存

    # 将拯救者y700P-001和ThinkPad-T480这两个元素放到集合brand::lenovo
    sadd brand::lenovo 拯救者y700P-001 ThinkPad-T480
    sadd screenSize::15.6 拯救者y700P-001 机械革命Z2AIR
    sadd processor::i7 拯救者y700P-001 机械革命X8TIPlus
    # 获取品牌为联想,屏幕尺寸为15.6,并且处理器为i7的电脑品牌(sinter为获取集合的交集)
    sinter brand::lenovo screenSize::15.6 processor::i7 -> 拯救者y700P-001
    
  • 排行版(zset)
    redis的zset天生是用来做排行榜的、好友列表, 去重, 历史记录等业务需求

    # user1的用户分数为 10
    zadd ranking 10 user1
    # user2的用户分数为 20
    zadd ranking 20 user2
    # 取分数最高的3个用户
    zrevrange ranking 0 2 withscores`
    

Redis学习-知识梳理_第13张图片
以上是Redis 官方提供的 benchmark 基准测试结果,x轴是连接数,y轴是qps。

官方文档中,影响Redis性能的关键因素如下:

  • 网络带宽和延时是直接影响
  • CPU-因为Redis是单线程,所以对CPU要求很高
  • 尽量采用物理机而不是虚拟机,虚拟化有额外的性能开销
  • 连接数最好不用过多,30000连接数是100连接数时性能的一半
  • 可以使用pipelining将多个命令组成管道提升处理速度。管道,一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。

更多关于Redis 性能的信息请查看How fast is Redis?

0xFF 参考文档

  • Redis中文网站

  • redis原理总结

  • 扫盲,为什么分布式一定要有Redis?

  • Redis主从复制原理总结

  • jedisLock—redis分布式锁实现

  • Redis的n种妙用,不仅仅是缓存

  • redis集群与hash一致性

  • redis的事务和watch

你可能感兴趣的:(redis,redis)