Redis 是 REmote DIctionary Server的缩写。
本文主要讲解redis基本概念、集群、过期和内存机制、事务、数据类型、应用等,希望大家阅读后有所收获。
Redis本质上是一个Key-Value类型的内存型数据库。因为是纯内存操作,Redis的性能非常出色,读写可达10万/S,是已知性能最快的Key-Value DB。
Redis-client 在操作的时候,会产生具有不同事件类型的 Socket。
在服务端,有一个 I/O 多路复用
程序,将客户端传递过来的Socket置入队列之中。然后,file event dispatcher
依次去队列中取,转发到不同的event handler
中。
需要说明的是,这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库,大家可以自行去了解。
如果想进一步提高CPU利用率,可以在一个机器上部署多个Redis实例(Redis客户端可以通过一致性哈希来处理多个Redis实例),也可以使用数据分片。
Redis采用的是基于内存的单进程单线程模型,由C语言编写,官方说QPS可以达到100000+。
纵轴QPS,横轴连接数。
更多关于为什么说Redis是单线程的以及Redis为什么这么快
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
TPS 80k/s,QPS 100k/s。
Redis支持丰富的数据类型,主要有:字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets) , bitmaps, hyperloglogs 和 地理空间(geospatial)。
注意在 list、Sets、 Sorted Sets和Hashes 为空时删除 key,并在用户试图添加元素而key不存在时创建空元素的 list、Sets、 Sorted Sets和Hashes,是 Redis 的职责。
String
是最简单Redis类型,就是一个key对应一个value的pair。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器(注:memcache的数据仅保存在内存中,服务器重启后,数据将丢失)。
String能表示3种类型:字符串、整数和浮点数。根据场景可自动转型,并且根据需要选取底层的实现方式。
String value内部存储结构:
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操作完成时,其他客户端不会在同一时间执行任何命令。
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:
Redis是单线程处理请求,迁移和访问的请求在相同线程内进行,所以不会存在并发性问题。
Redis Hash一般用于存储对象。
以下是一个简单示例:
> 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对象的键值对的操作。
按插入顺序排序的字符串元素的集合,数据结构是双向链表(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定界符。
> 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中的倒数第二个元素,以此类推。
Set是不重复且无序的字符串元素的集合,类似java中的hashset,增删查的时间复杂度都为O(1),可以求交集、并集、差集等。
Set以intset或hashtable来存储。hashtable中的value永远为null;当set中只包含整数型的元素时,则采用intset。
intset的内部结构:
> 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
类似Set,但Sorted Set中的每个字符串元素关联到一个double类型的score浮动数值,集合中的元素按 Score 进行升序排序。
Sorted Set中添加,删除和更新元素的操作时间复杂度是(O(log(N))).
内部结构以ziplist或skiplist+hashtable来实现
注意:有序集合的成员是唯一的,但分数(score)却可以重复。
所以它是可以进行有序搜索的元素集合(例如取出前面10个或者后面10个元素)。
Sorted Set可以做排行榜应用如求 TOP N 、范围查找等。
在redis-shell
里面SortedSet称为zset
WITHSCORES
来输出分数可以参考一篇很不错的文章,点击这里
通过特殊的命令,你可以将 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的内存空间就能存下。
底层是字节数组来存放,可扩容。
> 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
普通用户使用Redis bitmap时需要注意
首先说下基数的概念:比如数据集 {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 不能像集合那样,返回输入的各个元素。
HyperLogLog被用于估计一个 set 中元素数量的概率性的数据结构。
> PFADD runoobkey "redis"
1) (integer) 1
> PFADD runoobkey "mongodb"
1) (integer) 1
> PFADD runoobkey "mysql"
1) (integer) 1
> PFCOUNT runoobkey
(integer) 3
Redis 3.2 推出了GEO , 这个功能可以储存用户给定的地理位置, 并对这些信息进行操作。GEO 的数据结构总共有六个命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash,这里着重讲解几个。
Geo被用于储存地理位置
关于前面几种数据类型和实现原理总结如下:
名称 | 原理 | 时间复杂度 |
---|---|---|
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)) |
Redis主要提供了两种持久化机制RDB和AOF:
round-db
默认开启。RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储到磁盘。
Redis具体做法是fork一个子进程,将父进程的数据复制到内存然后写入临时文件,持久化过程结束后将此临时文件替换旧的快照文件。执行完毕后子进程退出。
注意:RDB方式在复制数据过程中会耗费大量内存,甚至在内存不足时会阻塞服务器运行直到结束复制,所以会引起大量IO,性能影响大。
还需要留意的是最后一次持久化的数据可能会丢失。
全名 append only file
AOF以redis协议来记录每次对服务器写的操作,追加的方式保存到持久化文件。当服务器重启的时候会replay
(重放)这些命令来恢复原始的数据。
AOF主要分为两种方式:
Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。具体来说,就是当日志文件大到一定程度的时候,会fork出一个进程来遍历父进程内存中的数据,每条记录对应一条set语句,写到临时文件中,然后再替换到旧的日志文件(类似rdb的操作方式)。默认触发条件是当AOF文件大小是上次重写后大小的一倍且文件大于64M。
开启持久化缓存机制,对性能会有一定的影响。所以如果你只希望你的数据在服务器运行的时候存在可以不使用任何持久化方式.
可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
一般来说, 如果想达到足以媲美PostgreSQL的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
有很多用户都只使用AOF持久化,但并不推荐这种方式:因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快。
用RDB恢复内存状态会丢失很多数据,重放AOP日志又很慢。Redis4.0推出了混合持久化来解决这个问题。将 RDB 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
Redis 3.0以后,提供了完整的Sharding(分片)、replication(主备感知能力)、failover(故障转移)新特性。
将不同范围的key映射到不同实例,缺点是需要维护映射关系
4个Redis实例时的一个例子如下:
crc32(key:foobar)=93024922
93024922 % 4 = 2
所以 key foobar 会被存储到第2个Redis实例。
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节点,这在避免跨节点访问相关数据时很重要。
客户端分区
由客户端指定数据读写时映射的Redis实例。我们已经在生产环境中使用了改造过的实现了一致性哈希的客户端。
代理分区
客户端请求走代理,代理根据分区规则请求相应实例最后把结果返回给客户端,可以参见Twemproxy。twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。也就是说,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。
由于使用了中间件,twemproxy可以通过共享与后端系统的连接,降低客户端直接连接后端服务器的连接数量。同时,它也提供sharding功能,支持后端服务器集群水平扩展。统一运维管理也带来了方便。
当然,也是由于使用了中间件代理,相比客户端直连服务器方式,性能上会有所损耗,实测结果大约降低了20%左右。
查询路由
客户端请求发送到任一Redis节点,Redis节点将客户端请求重定向到正确的节点。
Redis集群是自动分片和高可用的首选方案,是客户端分区和查询路由的结合使用。
Redis十分轻量级(单实例1M内存),为应对未来扩容,可以初始启动较多实例。只有一台服务器时也可以让Redis以分布式的方式运行:在同一台服务器上启动多个实例。
当数据增长需要更多的Redis服务器资源时,只需将Redis实例从一台服务器迁移到另外的服务器,不需再重新分区。
Redis复制技术可以做到极短或者不停机,操作流程如下:
Redis 集群没有使用一致性hash, 而是引入了 Hash Slot 的概念,即将所有数据划分为16384个分片(slot),每个节点会对应一部分slot,每个key都会根据分布算法映射到16384个slot中的一个,分布算法如下:
Slot_ID = crc16(key) % 16384
当一个Client访问的key不在对应节点的slots中,Redis会返回给client一个moved
命令来告知正确的路由信息,Client据此重新发起请求。同时,Client会根据每次请求来缓存本地的路由缓存信息,以便下次能直接路由到正确的节点。
举个例子,比如当前集群有3个节点,那么:
分片迁移:分片迁移的触发和过程控制由外部系统完成,Redis只提供迁移过程中需要的原语支持,主要包含两种:
可参考:
一致性哈希算法是分布式系统中常用的算法,他解决了普通求余类Hash算法伸缩性差的问题,可保证在增减节点的情况下尽量有多的请求命中原来路由到的服务器,减少受影响的数据量。
2^32
次方个桶的空间中,即0~(2^32)-1
的数字空间中,节点机器名和数据key都用此算法来hashRedis 集群是一个提供在多个Redis节点间共享数据的程序集,他通过sharding分区来提供一定程度的可用性。当某个节点宕机或者不可达时可继续服务。
Redis 集群的优势:
还可参考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主从复制分为全量同步和增量同步。Master-Slave刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。两种同步方式如下:
断点续传
在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秒,主服务器接受写操作
注意:
故障发现
节点间两两通过TCP保持连接,周期性进行PING、PONG交互,若对方的PONG响应超时未收到,则将其状态置为PFAIL,并传播给其他节点。
故障确认
当集群中有一半以上的节点对某一个PFAIL状态进行了确认,则将该节点状态改为FAIL,确认其发生故障。
Slave选举
当有一个master挂掉了,则其slave重新竞选出一个新的master。主要根据各个slave最后一次同步master信息的时间,越新表示slave的数据越新,竞选的优先级越高,就更有可能选中。选举成功之后将消息广播给其他节点。
手动故障转移
在某些时候比如Redis节点需要升级时,可通过设置目标Master节点为Slave再升级。流程如下:
当Redis集群中任意master挂掉且当该master没有slave或集群中有超过半数以上master挂掉时,整个Redis集群服务就不可用了。
Redis 并不能保证数据的强一致性,也就是说集群在特定的条件下可能会丢失写操作,原因有:
每个节点内部都将集群的配置信息存储在ClusterState中,通过自增的epoch变量来使集群配置在各个节点间保持一致。
Redis 哨兵用于管理多个 Redis 节点,主要任务如下:
虽然 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(哨兵) 进程。
Redis 哨兵 failover机制 使用 Raft 算法来选举领头(leader) 哨兵 , 从而确保在一个epoch
里, 只有一个leader 哨兵产生。更高的epoch总是优于较小的epoch, 因此每个哨兵都会主动使用更新的epoch来更新自己的配置。
Raft算法主要用于分布式系统的系统容错和选leader,Redis哨兵使用其核心原则如下:
当因为master处于ODOWN
状态和哨兵接收到从大多数已知的哨兵实例发来的授权时会开启failover过程,这时候会开启一次新的master选举,该过程主要会读slave以下信息进行评估:
较小
运行ID。当 Lua 脚本的运行时间超过指定时限时, Redis 就会返回 -BUSY 错误。
当出现这种情况时, 哨兵在尝试执行failover之前会先向服务器发送一个 SCRIPT KILL
命令。 如果正在执行的是一个只读脚本的话就会被杀死, 然后回到正常状态;如果还是错误,就会执行failover。
Redis 只能存 5G 数据,可是你写了 10G,那会删 5G 的数据。怎么删的?
数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,为什么?
回答:Redis 采用的是定期删除+惰性删除策略。
定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。
随机抽取Key
检查是否有过期的,有过期就删除。因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。但采用定期删除+惰性删除,就一切安好?
NO。如果定期删除没抽取到、程序也没请求过期key时,Redis的内存占用会越来越大。此时就要采用内存淘汰机制。
在 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(不删除) 基本上一致。
传统事务可以一次执行多个命令, 并且带有以下两个重要的保证:
Redis事务不同于传统事务
但请注意,Redis事务和传统事务是不同的,即使我们的事务中某个命令操作失败,我们也无法在这一组命令中让整个状态回滚到操作之前。具体查看7.3 事务错误
和7.4 事务回滚
原理
Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。
Redis事务的原理是先将属于一个事务的若干命令发送给Redis,然后再让Redis依次执行这些命令。
事务执行流程
一个Redis事务从使用MULTI
命令开始到执行EXEC
命令开启事务会经历以下三个阶段:
EXEC与事务
EXEC
命令负责触发并执行事务中的所有命令:
严格保证事务命令顺序
除此之外,Redis的事务还能保证一个事务内的命令依次执行而不被其他命令插入。试想客户端A需要执行几条命令,同时客户端B发送了一条命令,如果不使用事务,则客户端B的命令可能会插入到客户端A的几条命令中执行。如果不希望发生这种情况,也可以使用事务。详细请看7.5 Redis事务期间不响应其他请求
DISCARD放弃事务
当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出
脚本与事务
Redis中的脚本也是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。
当使用 AOF 方式做持久化的时候, Redis 会使用 write(2) 命令将事务落盘。
如果 Redis 服务器因为某些原因被杀死或硬件故障,那么可能只有部分事务命令会被成功写入到磁盘。
如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。使用redis-check-aof
程序可以修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。
使用Redis事务时可能遇到两种错误:
事务在执行 EXEC 之前,入队的命令出错(语法错误、内存不足等)
命令可能在 EXEC 调用之后失败
举个例子,事务中的命令可能处理了错误类型的key,比如将列表命令用在了字符串键上面。
这种情况并没有对它们进行特别处理:即使事务中某些命令在执行时出错, 其他命令仍会继续执行 —— Redis 不会停止执行事务中的命令。。
注意: Redis 事务不支持回滚(roll back)!!!
与传统事务不同,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在事务执行期间,不再响应其他客户端请求,才能保证一个客户端上的事务完整按序执行.
以下是一个Redis事务的例子:
MULTI
开始一个事务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"
第三章中讲了Redis的数据类型,可以感受到Redis底层实现是根据数据量和数据类型不同而不同的,换句话说Redis致力于对每种数据类型做特定的优化。
Redis可设阈值,当不超过这个阈值时对这些数据集合采用内存压缩技术进行编码,可节省5-10倍内存空间。
32位系统启动redis,每个key的指针更小,但是最大内存为4G。
当Hash元素非常少时,Redis将数据encode为一个O(N)的数据结构,你可以认为这是一个带有长度属性的线性数组。因为元素非常少和限行数组局部性原理的原因,所以此时使用HGET
和HSET
命令的复杂度仍然是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”,分成两部分:
HSET object:12 34 somevalue
这种优化方式可以将内存节约一个数量级。
Redis内存分配需要注意以下几点:
maxmemory
设置最大内存或者在启动后通过 CONFIG SET
设置。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命令来取消监控。
这个乐观锁实现流程如下:
需要注意WATCH的失效时机:
当 EXEC 被调用时, 不管事务是否成功执行, 对所有key的watch都会被删除
客户端断开连接
使用无参数的 UNWATCH 命令可以手动取消对所有键的watch
Redis 使用 WATCH 命令可以创建本来没有的原子操作。
下面是一个可以原子地弹出有序集合中score最小的元素的实例:
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC
程序只要重复执行这段代码, 直到 EXEC 的返回值不是nil-reply回复即可。
实现一个hsetNX函数,仅当字段存在时才赋值。
为了避免竞态条件我们使用watch和事务来完成这一功能(伪代码):
WATCH key
isFieldExists = HEXISTS key, field
if isFieldExists is 1
MULTI
HSET key, field, value
EXEC
else
UNWATCH
return isFieldExists
在代码中会判断要赋值的字段是否存在,如果字段不存在的话就不执行事务中的命令**,但需要使用UNWATCH命令来保证下一个事务的执行不会受到影响。**
Redis为单进程单线程模式、采用队列模式将并发访问变成串行访问。且多client对redis的链接并不存在竞争关系。
setnx key value
setnx key value
,当key不存在时,将 key 的值设为 value ,返回1。del key
,释放锁,如果setnx返回0表示获取锁失败只需具备4个特性就可以实现一个最低保障的分布式锁。
但是这种方式在多个Redis实例 master-slave架构 master挂掉的情况下会出问题:
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。为了取到锁,客户端应该执行以下操作:
关于Redis分布式锁更多内容请点击这里
如知乎每个问题的被浏览器次数
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
在list里面一边进,一边出即可
左放右取
# 一直往list左边放
lpush key value
# key这个list有元素时,直接弹出
# 没有元素时被阻塞,直到等待超时或发现可弹出元素为止
# 这里我们设置超时时间为10s
brpop key value 10
右放左取
rpush key value
blpop key value 10
新浪/Twitter用户消息列表(list)
lpush msg::li 100
lpush msg::li 200
# 下标从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)
# 返回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 官方提供的 benchmark 基准测试结果,x轴是连接数,y轴是qps。
官方文档中,影响Redis性能的关键因素如下:
更多关于Redis 性能的信息请查看How fast is Redis?
Redis中文网站
redis原理总结
扫盲,为什么分布式一定要有Redis?
Redis主从复制原理总结
jedisLock—redis分布式锁实现
Redis的n种妙用,不仅仅是缓存
redis集群与hash一致性
redis的事务和watch