redis note
默认16个数据库,下标从0开始,也就是说到15为止。初始默认使用0号库。通过select 1
来切换到1号库。
对比memcache(多线程+锁🔒):
- 支持多数据类型
- 支持持久化
- 单线程+多路IO复用。redis通过这种方式,使得用单线程能实现出多线程的效果,这样效率更高。
什么叫多路IO复用呢?老师是这么解释的,去火车站买票,如果自己去买得排队,而且可能买到也可能买不到,买不到怎么办呢?等着吗?但是如果让黄牛去买,你只要告诉黄牛要买什么票就好了,你告诉他之后你就可以去干自己的事了,等他买到票他就通知你来拿,而其他人也一样,通过这个黄牛去买,然后就可以自己去干活了,这样让cpu一直运转,提升效率。
基本操作
keys *
// 列出所有key
set name tracy
// 设置key为name value为tracy
exists name
// 判断key=name是否存在 存在返回1 不存在返回0
type name
// 查看key类型
del age
// 删除key=age age不存在返回0 存在返回1
unlink age
// unlink也和del同样用于删除 但是unlink是异步删除 他告诉你删除完成 但是其实不是马上删除 而是后续慢慢删
expire age 10
// 设置key的过期时间(秒) 时间一到会自动删除该key
ttl age
// 查看设置了过期时间的key还剩多久时间(秒) 返回-1表示永不过期, -2表示已过期
dbsize
// 查看当前数据库的key的数量
原子性
比如加1操作INCR key
,所谓原子操作不是指的事务上的,而是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(切换到另一个线程)。在单线程中,能够在单条指令中完成的操作都可以认为是原子操作,因为中断只能发生于指令之间。在多线程中,不能被其他进程(线程)打断的操作就叫做原子操作。
redis单命令的原子性主要得益于redis的单线程。
比如java的i++
就不是原子操作,比如两个线程分别对i进行自增操作,互相之间是会干扰的。
数据类型
-
string
string是二进制安全的,意味着redis的string可以包含任何数据,比如图片或者序列化对象(可以转换成字符串也可以从字符串转换回来)。字符串value最多可以是512M。
内部结构实现上类似于java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。当字符串小于1m时,扩容都是加倍现有空间,如果超过1m,扩容一次只会多扩1m。
-
list
按照插入顺序排序,你可以添加一个元素到列表的头或者尾。底层实现为双向链表,对两端的操作性能很高,通过索性来操作中间节点性能较差。具体来说,数据结构是一个叫做快速链表的东西,在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有元素紧挨着一起存储,分配的是一块连续的内存。当数据量多了后,会有多个ziplist,然后将这些ziplist组织成双向链表。因为普通的链表,每个数据都需要附加两个指针(prev,next),会比较浪费空间。因此采用了这种连续空间配合链表的形式。这样满足快速插入和删除功能,又不会出现太大的空间冗余。
-
set
能自动去重。底层是一个value为null的hash表,所以添加,删除,查找的复杂度都是o(1)
-
hash
类似于map结构,key-value这种形式,所以特别适合存储对象。对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度短且个数少的时候,使用ziplist,否则使用hashtable。
-
zset
有序set,每个key通过添加score来进行排序。底层使用了两个数据结构,hash和跳跃表。hash用来关联value和score。跳跃表用来给value排序,根据score的范围获取元素列表。是一个类似下图的结构,分了好几层,找元素从最上层开始找,这样在找某些中间的元素的时候,可以跳着找到,而不至于像普通链表一样只能一个个找。
-
bitmaps
可以将value想像成是一个元素为0或者1的数组,数组的下标称为偏移量。两个bitmaps可以做一些与或非的操作,比如用偏移量来作为用户id,元素值为1则表示该用户访问过网站,每天都这样记录,然后通过与操作就能得到哪些用户在哪几天内都访问了。
在有些情况下能够极大的节约空间。比如有1亿用户,有5000活跃用户,然后需要记录活跃用户。
但是这里要存储的用户量很小,那么bitmap就不合适。
-
HyperLogLog
统计页面的访问量可以通过incr,incrby来实现。但是像独立访客,独立ip数等需要去重和计数的问题就可以用HyperLogLog,这种集合中不重复元素的个数称为基数问题。
在mysql中可以用distinct count计算不重复个数,也可以使用redis中的hash,set,bitmaps等数据结构来处理。上述方案很精确,但是带来的问题是数据不断增加,占用空间会越来越大,对于非常大的数据集就不适合了。
而Redis推出了HyperLogLog在降低一些精度的情况下来平衡存储空间。他的优点是在输入元素的数量或者提及非常大时,计算基数所需要的空间总是固定的,每个键只需要12kb内存,就可以计算接近2^64个不同元素的基数,因为他只会根据输入元素来计算基数,而不会存储输入元素本身,所以他不能像集合那样返回输入的各个元素。
-
Geospatial
用来存储经纬度的地理信息。他可以算出两个地点的距离,或者查询某地点返回内有哪些元素。
配置文件
The default image from redis does not have a redis.conf.
Here is the link for the image on dockerhub. https://hub.docker.com/_/redis/
You will have to copy it to image or have it mapped on the host using a volume mapping.
发布订阅
基本用法:
比如有两个redis客户端,其中一个定于ch1频道SUBSCRIBE ch1
,另外一个像ch1中发布消息PUBLISH ch1 HelloWorld
,第一个客户端就能收到HelloWorld
事务
事务的使用分两个阶段,组队阶段和执行阶段。用multi命令开启组队阶段,加下来输入的所有命令会被加入到队列中,等到exec命令来依次执行。如果想中断,那么可以在exec之前用discard。
如果在组队过程中,有任何命令出错,那么所有的命令都不会执行。
如果组队没问题,但是在执行中出错,那么只有出错的那条命令会失败,其他的正常执行。
事务冲突
-
悲观锁
每次操作数据的时候都上锁,操作完才释放锁,上锁期间别人无法读写数据。
之所以叫悲观,是想象成每次操作数据都同时有人会一起操作
-
乐观锁
操作数据的时候会被数据加上一个版本号,等操作完数据会更新其版本号。期间如果有人同时要操作数据,可以读取,但是在写入的时候发现自己的版本跟最新版本不一致就不让继续操作,类似抢火车票,只剩一张票大家都可以抢,但是只能有一个人付款成功。
乐观,就是每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是利用这种check-and-set的机制实现事务的。
watch key
,watch命令可以监视一个或多个key,如果在事务执行之前这个(或这些)key被改动了,那么事务将被打断(无法执行,返回nil)。
事务的三个特性
-
单独的隔离操作
事务中所有命令都会序列化,按顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
-
没有隔离级别的概念
队列中的命令提交之前都不会实际被执行
-
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
秒杀案例
通常会出现两个问题,一个是已经没库存了但是还被秒杀了,库存变成负值,(超卖问题),一个是过多的请求会造成连接超时的问题,因为redis同时处理的连接数是有限的。
连接超时可以通过连接池来解决。
超卖问题可以通过用watch来检测库存的key,然后用multi开启事务,在事务中去执行库存-1,以及将用户添加进秒杀成功的用户列表中的操作。然后exec执行事务。(乐观锁)
库存遗留问题
乐观锁会造成一个叫库存遗留的问题,你比如说库存比较大(比如500),然后秒杀开启大的时候,2000个请求一上来,最后会发现库存依然还有。回想一下乐观锁的过程,比较容易理解。想象一个极端情况,2000个请求每个人都抢到了,但是在设置库存-1的时候只会有1个人成功,剩下的人全部会做失败来处理。这样就实际就只卖出了一份。
用悲观锁可以解决,但是redis默认不能直接使用悲观锁。有一种另外的方式来实现悲观锁,就是需要上锁的那部分代码通过Lua脚本来执行。
持久化
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘。
这个时间间隔可以通过配置文件来修改。通常会将数据保存到一个叫dump.rdb的文件中,这个文件的生成位置可以通过配置文件来修改(好像默认是redis启动的位置,你在哪启动就在哪生成?)
更新过程举例:比如规则是30秒内有10个key发生了改变,则进行持久化,然后会重置时间间隔。比如,在持久化之后又有新的2个key发生了改变,那么新的key不会进行持久化。
-
几个命令/配置
-
命令save vs bgsave
save时只管保存,其他不管,全部阻塞。手动保存,不建议。
bgsave,redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
-
stop-writes-on-bgsave-error
当redis无法写入磁盘的话,直接关掉redis的写操作(比如磁盘满了),推荐yes,默认好像也是yes
-
rdbchecksum
检查数据完整性,如果数据损坏了就不进行持久化,这个操作大概要耗费10%的性能。推荐是yes
-
-
备份是如何执行的?
fork一个子进程将数据写入一个临时文件,等写入完成后将dump.rdb文件直接替换掉。整个过程中,主进程是不进行任何IO操作的。
写入临时文件而不直接写入dump.rdb能进一步保证数据的安全性,比如写入过程中redis突然挂掉,那么dump.rdb不会收到影响,还可以从这里恢复。
如果需要进行大规模数据的恢复,且对数据恢复的完整性不是非常敏感,RDB比AOF更加高效。
缺点是最后一次持久化后的数据可能丢失。比如上面时间间隔的例子来说,最后2个key发生了变化,但是redis挂掉了,那么这2个key就丢失了。然后,fork的时候,内存中的数据被克隆了一份,这样就需要2倍的空间。
Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量,环境变量,程序计算器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
AOF
Append Only File,以日志的形式来记录每个写操作,只允许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,也就是说redis重启的话,就根据日志文件的内容将写操作从头到尾执行一遍,以完成数据恢复工作。
aof默认不开启,需要去配置文件开启。如果rdb也同时开启,那么默认会读取aof的数据。
-
同步频率 appendfsync
always:始终同步,性能较差但是完整性好,everysec,每秒同步,如果宕机,本秒的数据可能丢失,no 不主动进行同步,把同步时机交给操作系统
-
rewrite 压缩
文件追加方式势必会使得文件越来越大,所以用重写机制来解决,当aof文件的大小超过设定的阈值时,redis会启动aof文件的内容压缩,只保留可以恢复数据的最小数据集。也即是比如对key设置了n次操作,压缩的时候,只需要他最后的那一次写入操作即可,其他的都可以舍弃。
优势:备份更稳健,丢失数据频率更低。
劣势:比rdb更占空间,因为你不光要存数据还有记录操作。恢复备份速度慢。每次读写都同步的话,有一定的性能压力。存在个别bug,造成恢复不能。
总结
官方推荐两个都开启,如果对数据不敏感,可以单独用rdb,不建议单独用aof,因为可能会出现bug。如果只做纯内存缓存,可以都不用。
主从复制
master负责写,slave负责读。读写分离,能分担压力,容灾恢复(一台从机挂掉后可以可以马上切换到其他从机)
info replication
可以查看当前是主服务器还是从服务器。
slaveof ip port
可以设置当前服务器的主服务器
常用三种做法:一主二仆,薪火相传,反客为主。
-
一主二仆
某个从服务器挂掉后重启,需要再次用slaveof来设置主服务器,设置后,会将主服务器中的数据从头开始复制。
主服务器挂掉后,从服务器依然会认他做大哥,等到主服务器重启后,依然是主服务器,并拥有之前的从服务器。
当从服务器连上主服务器之后,会向主服务器发送一个数据同步的请求,主服务器收到消息后,把主服务器数据进行持久化,然后把rdb文件发送给从服务器,从服务器开始读取并同步。而当主服务器有更改的时候,由主服务器主动向从服务器更新数据。
-
薪火相传
相当于是从主服务器开始一个个开始复制,主服务器复制到一个从服务器,然后这台从服务器复制到下一台从服务器,以此类推。相当于是从服务器下面还可以继续接从服务器,这样串联起来。但是和刚才的一主二仆一样,主服务器挂掉,其他人不会上位,依然是从服务器。
-
反客为主
slave no one
可以将一台从服务器变成主服务器。但是这是需要手动执行的,后面会讲到一种哨兵模式,是反客为主的自动版。
哨兵模式
-
配置哨兵
新建一个叫
sentinel.conf
的配置文件,名字不能写错。mymaster是取个别名。最后的quorum参数好像是说有多少个从机认为主机挂掉了才进行切换,比如这里是1,那么只要有一个从机认为主机挂掉了就进行切换。sentinel monitor mymaster 127.0.0.1 6379 1 sentinel monitor <master-group-name> <ip> <port> <quorum>
-
启动哨兵
用刚才的配置文件来启动哨兵。
redis-sentinel <sentinel.conf-path>
当主机挂掉后,哨兵会选择一个从机上位,之前的主机再重新启动后会成为新主机的从机。
复制延时,由于所有的写操作都是先在master上操作,然后同步到slave上,所以master同步到slave机器有一定的延迟,当系统很繁忙的时候,延迟会更严重,slave机器数量的增加也会使这个问题更加严重。
从机上位的规则,选择的优先级依次为
-
选择优先级靠前的
在配置文件中会有一个replica-priority的配置,设置了优先级。值越小的优先级越高。
-
选择同步率高的
比如主机有10条数据,从机1有10条,从机2有8条,那么选择从机1。
-
选择runid最小的
redis每次启动会随机生成一个40位的runid
集群
问题
容量不够,redis如何进行扩容?
并发写操作,redis如何分摊?
服务重启后ip可能是变化的,要怎么办?以前是采用的代理服务器的方式,就是客户端所有的请求都通过代理服务器来进行分发。如果代理服务器挂掉了那不就没法提供服务了吗?所以给代理服务器也加上从服务器。这样带来的问题是服务器变得多了,可能会带来管理上的问题。所以现在redis是采用去中心化的方式,也就是说集群的入口可以是任何一个节点,然后由这个节点来进行请求的分发,节点同时负责数据存储以及请求的分发。
搭建集群
比如搭建一个3主3从的集群,那么就是6台服务器,准备6个配置文件,配置文件还需要以下几个配置:
cluster-enabled yes // 打开集群模式
cluster-config-file nodes-6379.conf // 设置节点配置文件名
cluster-node-timeout 15000 // 设置节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
启动6台服务器。然后通过下面的命令将他们组合成一个集群。一个集群中至少要有3个主节点,--cluster-replicas 1
表示我们希望味集群中的每个主节点创建一个从节点。分配原则尽量保证每个主数据库运行在不同的ip,每个从库和主库不在一个ip上。
redis-cli --cluster create --cluster-replicas 1 <ip1:port1> ... <ip6:port6>
连接集群的时候,通过-c
命令来连接集群。后面的端口号用任何一个节点的端口号都行。
redis-cli -c -p 6379
插槽slots
一个redis集群包含16384个插槽(hash slot),数据库中的每个键都属于这16384个插槽的其中一个。
集群使用公式CRC16(key) % 16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16校验和。
集群中的每个节点负责处理一部分插槽,比如:
比如你连上集群后,进行一个set操作,但是他计算key后发现插槽值不在当前主库范围内,他会自动切换到对应的主库来进行set操作。
注意⚠️,使用集群后,当要同时设置多个kv,就不能这样使用mset k1 v1 k2 v2
,应该给每个key加上一个组名,比如向下面这样用大括号加上组名user。
mset k1{user} v1 k2{user} v2
几个常用命令
cluster keyslot k1 //查找k1的插槽值
cluster countkeysinslot 12779 // 查看插槽里有多少个数据,但是要注意⚠️这个只能查看当前库中的
cluster getkeysinslot 448 1 // 取出448插槽中的1个数据
故障恢复
如果一个主机挂掉了,那么从机会上位,等到之前的主机重启的时候会成为从机。
但是如果如果一个节点整个挂掉了呢?要根据配置cluster-require-full-coverage
来决定,如果为yes,那么整个集群都挂掉,如果为no,那么该插槽的数据全部不能用,也无法使用,但是其他的节点照常运行。
集群总结
好处:实现扩容,分摊压力,无中心化配置相对简单
不足:多键操作不支持,多键的redis事务是不被支持的,lua脚本不被支持。redis集群出现较晚,如果要迁移会比较麻烦。
可能会出现的问题
缓存穿透
通常来说,服务器收到请求会去查看缓存,如果有则返回,没有则去查数据库,数据库找到后然后会缓存到redis,但是,如果遭遇黑客攻击,他一直去查那种不存在的值,会导致数据库一直被访问,也就是redis的命中率不高的时候,服务器压力会变得很大。以下几种解决办法:
-
对空值缓存
如果一个查询返回空,我们仍然把这个空结果进行缓存,然后设置他的过期时间很短,比如1分钟,最长不超过5分钟。但是这只能算是一种临时的简单的方案。
-
设置可访问的名单(白名单)
使用bitmaps类型来制作白名单,名单的id作为偏移量。他的问题是每次都要去访问bitmaps,效率也不会太高。
-
采用布隆过滤器
其实也跟上面的白名单差不多,底层就是一个bitmaps。
-
进行实时监控
当发现redis的命中率开始急速降低,需要排查访问对象和访问数据,和运维配合,设置黑名单限制访问。
缓存击穿
会出现的现象:
- 数据库访问压力瞬时增加
- redis里面没有出现大量key过期
- redis正常运行
原因:一般是redis某个key过期了,但是大量访问这个key,比如一些新闻热点。
解决办法:
-
预先设置热门数据
在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
-
实时调整
监控到哪些热门数据,实时调整key的过期时长
-
使用锁
就是在缓存失效的时候(拿出来的值为空),不立即去查询数据库,而是用比如setnx这种去设置一个排他锁,设置成功后再去查询数据库,然后设置缓存,删除刚设置的排他锁。这样肯定能解决这个问题,但是效率肯定是要差一些的,因为在缓存设置好之前其他人就得等着。
缓存雪崩
现象:数据库压力过大,导致服务器变慢进而崩溃,redis大量的访问等待,然后redis也崩溃了。
原因:大量的key过期。
解决方案:
-
构建多级缓存架构
nginx缓存+redis缓存+其他缓存(ehcache等)
-
使用锁或者队列
保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。但是他不适用高并发情况。
-
设置过期标志更新缓存
就是在数据快要过期的时候,通知线程在后台去更新实际key的缓存
-
将缓存失效时间分散开
比如可以原来的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,很难引起机体失效的事件。
分布式锁
集群里面某个节点的锁其他节点当然是不认的,所以需要一个大家都认的共享的锁。具体做法:
使用setnx 来设置一个key,这样就算是上锁,如果成功会返回1,失败会返回0。事情做完后再直接删掉这个key,就当作是释放锁了
会遇到的问题
-
锁一直没有释放
可以通过设置过期时间,来自动释放
-
上锁后突然出现异常,导致无法设置过期时间了
这也就是所说的原子性问题,这个时候redis提供了一个方式来同时设置key和过期时间
set users 10 nx ex 12 // 设置12秒的过期时间
-
释放了错误的锁
比如有两台机器要操作同一个数据,那么肯定是依据:上锁>具体操作>释放锁这种顺序来执行。但是如果a在拿到锁之后,服务器卡顿了,然后又因为有过期时间,然后锁被释放了,这时候b拿到了锁,这时候a的服务器恢复了,开始继续执行,手动释放锁的操作,就会把b加上的锁给释放了。
办法是用uuid来上锁,防止误删。比如
set lock uuid nx ex 12 // 设置12秒的过期时间
每次上锁之前先生成一个uuid设置进去,然后等到要释放的时候,去比对当前锁的值(也就是uuid)是否跟服务器上生成的一致,一致则正常删除,不一致则不删除。
-
原子性问题
在上面的例子的基础上,还有一种情况,比如在a判断来uuid一致正打算要删除锁的时候,锁过期被自动释放了,然后此时b抢到了锁,而a继续开始删除锁,就会误删掉b到锁。
给出的解决办法是:判断uuid以及删除锁这段操作用lua来执行。
我有点没懂的是,难道lua来执行的时候,过期操作就不会在这期间自动执行了吗??
总结
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
-
互斥性
在任意时刻,只有一个客户端能持有锁
-
不会发生死锁
即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
-
解铃还须系铃人
加锁解锁必须是同一个客户端
-
加锁解锁必须具有原子性
redis6 新功能
-
acl
用户权限控制
-
IO多线程
IO多线程其实指的客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程,执行命令依然是单线程。
默认不开启,需要在配置中开启
io-threds-do-reads yes io-threads 4