[Redis]
NoSQL
NoSQL(Not Only SQL),泛指非关系型数据库
不依赖业务逻辑方式存储,而以简单的“key-value”模式存储,大大增加了数据库的扩展能力。
用于高并发和海量数据
Redis概述
Redis 使用的是单线程 + 多路 IO 复用技术:
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用 select 和 poll 函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
串行 vs 多线程 + 锁(memcached) vs 单线程 + 多路 IO 复用 (Redis)
(与 Memcache 三点不同:支持多数据类型,支持持久化,单线程 + 多路 IO 复用) 。
Redis五大数据类型
Redis 键
1 | set k1 xxx |
String
概述
- String 是 Redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
- String 类型是二进制安全的。意味着 Redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化的对象。
- String 类型是 Redis 最基本的数据类型,一个 Redis 中字符串 value 最多可以是 512M。
常用命令
1 | set key value 添加键值对 |
List列表
单键多值:Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
1 | lpush/rpush key value 从左/右插入一个或多个值 |
Set列表
Redis set 对外提供的功能与 list 类似,是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。
Redis 的 Set 是 string 类型的无序集合。它底层其实是一个 value 为 null 的 hash 表,所以添加,删除,查找的复杂度都是 O (1)。
1 | sadd key value1 value2 将一个或多个元素加入到集合中,已经存在的元素将被忽略 |
Redis 哈希(Hash)
Redis hash 是一个键值对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
类似 Java 里面的 Map<String,Object>。
用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,如果用普通的 key/value 结构来存储。
通过 key (用户 ID) + field (属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
1 | hset key field value 给key集合中的field键赋值value |
Redis 有序集合 Zset(Sorted set)
Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的,所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
发布和订阅
发布订阅(pub/sub)是消息通信模式:发送者发送消息,订阅者接收消息
redis客户端可以订阅任意数量频道
新数据类型
Bitmaps
1 | getbit key offset 获取位图指定索引的值 |
HyperLogLog
1 | pfadd 元素添加 |
Geographic
地理信息缩写,元素的二维坐标。
Redis事务
定义
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序的执行。事务在执行的过程中,不会被其他客户端发来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
Redis事务三大特性:
- 单独的隔离操作(不会被打断)
- 没有隔离级别
- 不保证原子性
主要的三个命令
命令 | 功能 |
---|---|
multi | 组队阶段,还未执行 |
exec | 执行阶段,将multi的队列放进 exec中 |
discard | 放弃multi在队列中的值 |
事务的错误处理
组队的时候失败,即执行的时候也是失败
组队的时候成功,执行的会出错。但个别指令如果出错,只有这个指令出错执行不了
事务冲突
悲观锁
不能同时进行多人,执行的时候先上锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
乐观锁
通过版本号一致与否,即给数据加上版本,同步更新数据以及加上版本号。不会上锁,判断版本号,可以多人操作,类似生活中的抢票。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的
在执行multi之前,执行命令watch
具体格式如下
1 | watch key1 [key2] |
持久化操作
RDB
RDB(Redis DataBase),在指定的时间间隔内
将内存中的数据集快照
写入磁盘
具体的备份流程如下:
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
RDB的缺点是最后一次持久化后的数据可能丢失。
数据如果有变化的,会在/usr/local/bin目录下生成一个dum.rdb的文件
关于fork进程
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
AOF
AOF(append only file)以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
重写的机制:
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename
redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作
no-appendfsync-on-rewrite:
缓存,yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
磁盘,no,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
什么时候重写:
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
重写流程:
bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
主进程fork出子进程执行重写操作,保证主进程不会阻塞。
子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
使用新的AOF文件覆盖旧的AOF文件,完成AOF重写
总结
优点:
备份机制更稳健,丢失数据概率更低
可读的日志文本,通过操作AOF稳健,可以处理误操作
缺点:
比起RDB占用更多的磁盘空间。
恢复备份速度要慢。
每次读写都同步的话,有一定的性能压力。
存在个别Bug,造成恢复不能
总结
- RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
- AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.
- Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
- 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
- 同时开启两种持久化方式.在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
- RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
Redis 主从复制
主机数据更新后根据配置和策略, 自动同步到备机的 master/slaver 机制,Master 以写为主,Slave 以读为主,主从复制节点间数据是全量的。
作用:
- 读写分离,性能扩展
- 容灾快速恢复
复制原理
Slave 启动成功连接到 master 后会发送一个 sync 命令;
Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步。
全量复制:
slave 服务器在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:
Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步。
但是只要是重新连接 master,一次完全同步(全量复制) 将被自动执行。
情况1:一主两仆
主机挂掉,执行shutdown
从机info replication还是显示其主机是挂掉的哪个
如果从机挂掉,执行shutdown
主机开始写数据,从机在开启的时候,恢复数据的时候是从主机从头开始追加的
情况2:薪火相传
上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。
从机的大哥是另一台从机的意思
用 slaveof <ip><port>
,中途变更转向:会清除之前的数据,重新建立拷贝最新的
风险是一旦某个slave宕机,后面的slave都没法备份
主机挂了,从机还是从机,无法写数据了
情况3:反客为主
当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改
可以使用命令:slaveof no one
将从机变为主机
哨兵模式 (sentinel)
主要是为了监控主机宕机之后,从机可以立马变为主机,就和上面的反客为主一样,不用手动设置
能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
再目录中新建一个文件sentinel.conf,文件格式不能出错.文件内容为
sentinel monitor mymaster 127.0.0.1 6379 1
代码的含义为 sentinel哨兵,监控,一个id(别名),ip加端口号
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
启动哨兵模式通过redis的bin目录下
命令如下:redis-sentinel /sentinel.conf
具体哪个从机会变成主机
其判定规则主要为
(顺序依次往下,优先级》偏移量》runid)
优先级在redis.conf中默认:slave-priority 100,值越小优先级越高
偏移量是指获得原主机数据最全的,也就是数据越多,变主机的机会越大
每个redis实例启动后都会随机生成一个40位的runid
在这里也有个缺点就是复制会有延时
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
集群
容量不够,并发写操作等问题
通过引入集群,也就是可以多个主机可以操作
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。
集群的定义:
Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求
集群操作
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,
集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口 。
- 查询集群中的值,
CLUSTER KEYSLOT k1
- 查询卡槽中key的数量,
CLUSTER COUNTKEYSINSLOT 12706
- 查询指定卡槽返回key的数量,
CLUSTER GETKEYSINSLOT 5474 2
如果在集群中录入值,录一个值,会根据计算进入到某个主从的卡槽值
如果传输多个值,要使用到分组的技术,在用mset 同时设置多个值的时候,需要把这些key放到同一个组中,不然会报错。可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去
故障恢复
在主节点回复后,原来主机变成从机
应用问题解决
缓存穿透
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库
通俗的来说:访问的数据缓存找不到,一直转而发送到数据库
解决方案
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
- 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
- 设置可访问的名单(白名单):使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
- 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。 - 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
总结如下:
设置空值缓存,而且设置超时时间
通过bitmap的位运算进行存储,数据量比较小
实时监控,将其禁止访问
缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
也就是一个key过期,一直访问数据库
解决方案:
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
- 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
- 实时调整:现场监控哪些数据热门,实时调整key的过期时长
- 使用锁:先判断值是否为空再让他进来与否
总结如下:
设置热门的key,加大时长过期
实时监控调整
缓存雪崩
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key正常访问
解决方案:
- 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
- 用锁或队列:
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况 - 设置过期标志更新缓存:
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。 - 将缓存失效时间分散开:
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
总结如下:
- 设置多个级别的缓存架构,时间来得及缓冲
- 使用锁的机制
- 设置一过期时间标志来通知
- 将过期时间分散,比如5分钟、5.01分钟等
分布式锁
由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问
也就是在这个机器上了锁,另外一个机器也要可以识别到这个锁,也就是共享锁,都是同一把锁
解决方案如下:
基于数据库实现分布式锁
基于缓存(Redis等)
基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
性能:redis最高
可靠性:zookeeper最高
这里,我们就基于redis实现分布式锁
setnx 上锁,通过del 解释
锁一直没有释放,可以通过设置过期时间来自动释放
但是如果上锁之后就断电了
解决方法为
可以边上锁边设置过期时间,通过命令set users 10 nx ex 12
,nx为上锁,ex为过期时间
ttl查看过期时间还有多久
分布式锁总结
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
同一时间只有一个人有锁,而且开锁解锁都是同一个人,不会死锁
互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁和解锁必须具有原子性
UUID防止误删
上面代码操作可能会出现,在解锁的时候刚好设置的时间结束了
而导致锁解错了
为此应该多一个判断是否是你的锁,虽然是共享锁,都是一样的,但是可以上锁之后在设置时间,还要给每个用户的这把锁都来一个uuid
解决
1 |
|
lua脚本保证原子性
问题又来了
如果在判断它的uuid相等之后,正准备解锁,发现又误解他人锁
所以引入lua脚本保证它的原子性
总结如下:
- 加锁
1 | // 1. 从redis中获取锁,set k1 v1 px 20000 nx |
- lua释放锁
1 | // 2. 释放锁 del |
- 重试
1 | Thread.sleep(500); |
Redis6.0新功能
ACL
ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。
在Redis 5版本之前,Redis 安全规则只有密码控制 还有通过rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等。Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制:
- 接入权限:用户名和密码
- 可以执行的命令
- 可以操作的 KEY
1 | acl list命令展现用户权限列表 |
IO多线程
IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程
Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题
另外,多线程IO默认也是不开启的,需要再配置文件中配置
1 | io-threads-do-reads yes |