NoSQL
NoSQL即Not Only SQL不仅仅是SQL,泛指非关系型的数据库
NoSQL数据库可以分为4大类:键值key-value存储、列存储、文档数据库、图形数据库
CAP原理
CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。CAP原则是NOSQL数据库的基石。
分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:
- 一致性C:在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
- 可用性A:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容忍性P:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
- 关系型数据库RDBMS:Oracle、MySQL就是CA
- CP系统最典型的就是分布式数据库,如Redis、HBase等
- AP高可用并允许分区,则需放弃一致性。例如抢购系统中,还有CouchDB之类的NoSQL数据库
BASE事务
分布式事务的特性BASE:基本可用BA、软状态S、最终一致性E
- 基本可用Basically Available。NoSQL允许分布式系统中某些部分出现故障,那么系统的其余部分依然可用。它不会像ACID那样,在系统出现故障时,进行强制拒绝,允许继续部分访问。
- 软状态Soft State。NoSQL在数据处理过程中,允许这个过程,存在数据状态暂时不一致的情况。但经过纠错处理,最终会一致的。
- 最终一致性Eventually Consistent。NoSQL的软状态允许数据处理过程的暂时不一致,但是最终处理结果将是一致的,说明NoSQL对数据处理过程可以有短暂的时间间隔,也允许分更细的步骤一个一个地处理,最好数据达到一致即可。这在互联网上进行分布式应用具有其明显的优势。
Redis
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、高性能的Key-Value的NoSQL数据库。可以用作数据库、缓存、消息中间件等
Redis通常被称为数据结构服务器,因为值value可以是字符串String、哈希Map、列表list、集合set和有序集合sorted set等类型
- 性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS
- 单进程单线程,是线程安全的,采用 IO 多路复用机制
- 丰富的数据类型,支持字符串strings、散列hashes、列表lists、集合sets、有序集合sorted sets等
- 支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载
- 主从复制,哨兵,高可用
- 可以用作分布式锁
- 可以作为消息中间件使用,支持发布订阅
为什么使用Redis
1、解决应用服务器的cpu和内存压力
2、在项目中使用Redis,主要考虑两个角度:性能和并发。
3、在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。
4、在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。
5、排行榜及相关问题。排行榜按照得分进行排序。zadd命令可以直接实现这个功能,而zrevrange命令可以用来按照得分来获取前100名的用户,zrank可以用来获取用户排名,非常直接而且操作容易。
6、计数的问题,比如点赞和转发数,通过原子递增保持计数;getset用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
Redis优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s
- 丰富的数据类型 – Redis支持二进制的Strings, Lists, Hashes,Sets及Ordered Sets数据类型操作
- 原子– Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来
- 丰富的特性 – Redis还支持publish/subscribe,通知,key过期等等特性。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
- 支持数据持久化,支持AOF和RDB两种持久化方式。
Redis缺点
- 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
- Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
redis与memcache比较
- Redis:多种结构 AE事件驱动 支持存儲 支持复制、单进程单线程[redis5+已经支持多线程]、Crash safe和Recovery slow,单机qps可以达到10W、适合小数据量高速读写访问。
- Memcached:字符串 libEvent 不支持存儲 多线程、不能枚举全数据、性能仍有提升空间
1、存储方式上:Memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分数据存在硬盘上,这样能保证数据的持久性。
2、数据支持类型上:Memcache 对数据类型的支持简单,只支持简单的 key-value,,而 Redis 支持五种数据类型。
3、使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、Value 的大小:Redis 可以达到 1GB,而 Memcache 只有 1MB
典型应用
1、会话缓存Session Cache。最常用的一种使用Redis的情景是会话缓存session cache。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化
2、全页缓存FPC。除基本的会话token之外,Redis还提供很简便的FPC平台
3、队列。Reids在内存存储提供list和set操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作
4、排行榜/计数器。Redis在内存中对数字进行递增或递减的操作实现的非常好。集合Set和有序集合Sorted Set也使得在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”
5、发布/订阅。可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统。
Redis6多线程
主线程还是单线程的,只是IO线程采用多线程方式。
Redis 6.0默认并没有开启多线程,可以在conf文件进行配置
- io-threads-do-reads yes
- io-threads 线程数
官方建议:4 核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数,尽量不超过8个。
开启多线程后,是否会存在线程并发安全问题?
不会,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。
AE事件模型
redis采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。多路指的是多个Socket连接,复用指的是复用一个线程。多路复用主要有三种技术:Select,Poll,Epoll。
Redis使用了一个称为A simple event-driven programming library的自制异步事件库,简称AE。Redis默认采用epoll。主要提供了对两种类型的事件驱动:1、IO事件(文件事件),包括有IO的读事件和写事件。2、定时器事件,包括有一次性定时器和循环定时器。
Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制 ,因为一般的系统调用系统函数的话会浪费一定的时间去移动和请求;
Redis是单线程的,如何提高多核CPU的利用率
可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果想使用多个CPU可以考虑一下分片shard。
redis应用体系结构
Sentinel/san’te’nao/是面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性
-
单机版Redis Server,无Sentinel。会有单点故障的问题无法解决,如果没有配置Redis数据持久化的话,Redis内部已经存储的数据也会丢失
-
主从同步Redis Server,单实例Sentinel
即在两台服务器上分别各启动一个Redis Server进程,一般情况下由master提供服务,slave只负责同步和备份。与此同时在额外启动一个Sentinel进程,监控两个Redis Server实例的可用性,以便在master宕机时及时把slave提升到master的角色继续提供服务,这样就实现了Redis Server的高可用。这基于一个高可用服务设计的依据,即单点故障本身就是个小概率事件,而多个单点同时故障(即master和slave同时挂掉),可以认为是基本不可能发生的事件
-
主从同步Redis Server,三实例Sentinel
把Redis Sentinel进程也额外启动一份,三个Sentinel进程同时为客户端提供服务发现的功能,引入了服务器3上搭建起一个Redis Sentinel进程,现在由三个Sentinel进程来管理两个Redis Server实例。对于客户端来说,它可以连接任何一个Redis Sentinel服务来获取当前Redis Server实例的基本信息。通常情况下会在Client端配置多个Redis Sentinel的链接地址,Client一旦发现某个地址连接不上,会去试图连接其他的Sentinel实例
-
Redis集群的数据分片redis-cluster
利用【集群+槽位】的方式完成,Redis集群没有使用一致性哈希,而是另外一种不同的分片形式,每个键概念上是被称为哈希槽hash slot。Redis集群有16384个哈希槽,使用键的CRC16编码对16384取模来计算一个指定键所属的哈希槽。 每个Redis集群中的节点都承担一个哈希槽的子集
-
Redis集群的主从模型Redis Cluster master-slave model
为了当部分节点失效时或者无法与大多数节点通信时仍能保持可用,Redis集群采用每个节点拥有1(主服务自身)到N个副本(N-1个附加的从服务器)的主从模型。假设集群拥有A,B,C三个节点,如果节点B失效集群将不能继续服务,因为不再有办法来服务在5501-11000范围内的哈希槽。 但是,如果当创建集群后为每个主服务器添加一个从服务器,这样最终的集群就由主服务器A,B,C和从服务器A1,B1,C1组成,如果B节点失效系统仍能继续服务。B1节点复制B节点,于是集群会选举B1节点作为新的主服务器,并继续正确的运转。
redis.windows.conf
- daemonize no默认情况下redis不是作为守护进程运行的,如果想让它在后台运行,就把它改成yes。当redis作为守护进程运行的时候,它会写一个pid到/var/run/redis.pid文件里面
- port 6379监听端口号,默认为6379,如果设为0,redis将不在socket上监听任何客户端连接。
- tcp-backlog 511设置TCP监听的最大容纳数量。在高并发的环境下,需要把这个值调高以避免客户端连接缓慢的问题。Linux内核会把这个值缩小成/proc/sys/net/core/somaxconn对应的值,所以要修改这两个值才能达到你的预期。
- bind 192.168.1.100 10.0.0.1默认情况下,redis在server上所有有效的网络接口上监听客户端连接。如果只想让它在一个网络接口上监听,那就绑定一个IP或者多个IP。多个IP用空格隔开
- timeout 0指定在一个client空闲多少秒之后关闭连接(0就是不管它)
- loglevel notice定义日志级别。可以是:debug适用于开发或测试阶段、verbose类似debug信息但是内容更少些、notice适用于生产环境、warning仅仅一些重要的消息被记录
基本操作中的问题
问题1:假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
数据类型
Redis内部使用一个redisObject对象来表示所有的key和value,redisObject 最主要的信息为:1、type 表示一个value对象具体是何种数据类型,2、encoding是不同数据类型在Redis内部的存储方式。如type=string表示value存储的是一个普通字符串,那么encoding可以是raw或者int。
Redis数据类型实际上有8种,常用的是String、Hash、List、Set、Zset共5种
字串类型
string类型是Redis 最基本的数据类型,string 类型的值最大能存储512MB,不仅仅可以是字符串,还可以是数字
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。string类型是Redis最基本的数据类型,一个键最大能存储512MB。
应用场景:
-
缓存:将数据以字符串方式存储
-
计数器功能:比如视频播放次数,点赞次数。
-
共享session:数据共享的功能
具体存储结构:
Redis构建了一个叫做简单动态字符串Simple Dynamic String,简称SDS
Hash哈希表
Redis hash 是一个 string 类型的 field 和 value 的映射表,Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。每个 hash 可以存储 2^32 -1 键值对(40多亿)
字典。键值对集合,即编程语言中的Map类型。适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。
应用常见:存储、读取、修改对象属性,也可以用Hash做表数据缓存
具体的存储实现:
在 hash 的内部包含了两个hashtable,一般情况下只是用一个
- 负载因子是1,扩容比为1
- 当hashtable 中元素逐渐变少时,Redis会进行缩容来减少空间占用,并且缩容不会受bgsave的影响,缩容条件是元素个数少于数组长度的10%。
List列表
Redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边),最多可以存储2^32 -1个元素
List 应用场景非常多,也是 Redis 最重要的数据结构之一,比如 Twitter 的关注列表,粉丝列表都可以用 List 结构来实现。
应用场景:1、最新消息排行榜。2、消息队列以完成多程序之间的消息交换
底层实现采用的是双向链表的快速列表,插入和删除的效率很高,可以用来当消息队列用。链表查询的效率又由ziplist压缩列表来进行弥补。quickList是一个ziplist组成的双向无环链表。每个节点使用ziplist来保存数据。本质上来说,quicklist里面保存着一个一个小的ziplist。quickList类似于B树结构可以快速定位
Set集合
Redis的Set是string类型的无序集合。集合是通过值为空的哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。哈希表实现,元素不重复,最大的优势在于可以进行进行集合的交、并、差操作。
底层数据结构以intset或者hashtable来存储。intset可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)。set的底层存储intset和hashtable是存在编码转换的,使用intset存储必须满足两个条件,否则使用hashtable,条件为:1、集合对象保存的所有元素都是整数值。2、集合对象保存的元素数量不超过512个
intset内部其实是一个数组,而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的
应用场景:1、利用交集求共同好友。2、利用唯一性,可以统计访问网站的所有独立IP。3、好友推荐时根据tag求交集,大于某个临界阈值就可以推荐
SortedSet有序集合
Redis zset和set一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数score却可以重复。
适用于:排行榜;带权重的消息队列。可以用于一个大型在线游戏的积分排行榜
内部是以ziplist或者skiplist来实现。
只有同时满足2个条件是使用的是ziplist,其他时候则是使用skiplist。
条件是1、有序集合保存的元素数量小于128个。2、有序集合保存的所有元素的长度小于64字节。
当ziplist作为存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表结点来保存,
第一个节点保存元素的成员,第二个元素保存元素的分值。
当使用skiplist作为存储结构时,使用skiplist按序保存元素分值,使用dict来保存元素和分值的对应关系
跳跃表是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。在查找、插入和删除这些字典操作上效率可比拟于平衡二叉树(如红黑树)
HyperLogLog
Redis在2.8.9版本添加了HyperLogLog结构。HyperLogLog是用来做基数统计的算法,
HyperLogLog的优点是在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
Pub/Sub发布/订阅
Redis发布订阅pub/sub是一种消息通信模式:发送者pub发送消息,订阅者sub接收消息。Redis客户端可以订阅任意数量的频道。
Redis事务
Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位,一个事务要么都执行,要么都不执行。
事务失败处理:
- 语法错误(编译器错误),在开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败,k1、k2保留原值
- Redis类型错误(运行时错误),在开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值。
带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis 事务的原理是先将属于一个事务的命令发送给 Redis,然后依次执行这些命令。
Redis中多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。
为什么redis事务不具备原子性
单个Redis命令的执行是原子性的,但Redis没有在事务上增加任何维持原子性的机制,所以Redis事务的执行并不是原子性的。
Redis事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
Redis事务相关命令
DISCARD取消事务,放弃执行事务块内的所有命令
EXEC执行所有事务块内的命令,必须与MULTI命令成对使用
MULTI标记一个事务块的开始
WATCH命令用于监视一个或多个 key ,如果在事务执行之前这个或这些key 被其他命令所改动,那么事务将被打断
UNWATCH取消 WATCH 命令对所有 key 的监视。
Redis持久化
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
Redis持久化机制
Redis 提供2种方式进行持久化。RDB是Redis默认的持久化方式。
- RDB持久化:原理是将 Reids 在内存中的数据库记录定时 dump 到磁盘上的 RDB 持久化。
- AOF即append only file持久化:原理是将 Redis 的操作日志以追加的方式写入文件。
当 Redis 重启的时候,它会优先使用 AOF 文件来还原数据集,因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存。
RDB:Redis Database,就是快照snapshots。
默认Redis是会以快照RDB的形式将数据持久化到磁盘的一个二进制文件dump.rdb。可以配置Redis的持久化策略,例如数据集中每N秒钟有超过M次更新,就将数据写入磁盘;或者你可以手工调用命令SAVE或BGSAVE。
工作原理简单说一下:当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处是可以copy-on-write。
RDB 的优点是:这种文件非常适合用于备份:如可以在最近的 24 小时内,每小时备份一次,并且在每个月的每一天也备份一个 RDB 文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适合灾难恢复。
RDB 的缺点是:如果需要尽量避免在服务器故障时丢失数据,那么RDB不合适。
AOF:Append Only File。
快照模式并不十分健壮,当系统停止或者无意中Redis被kill掉,最后写入Redis的数据就会丢失。这对某些应用也许不是大问题,但对于要求高可靠性的应用来说,Redis就不是一个合适的选择。Append-only文件模式是另一种选择。可以在配置文件中打开AOF模式
Redis中提供了3种同步策略,即每秒同步、每修改同步和不同步。事实上每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。
AOF 可以做到全程持久化,只需要在配置中开启 appendonly yes。这样 Redis 每执行一个修改数据的命令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,恢复到 Redis 关闭前的最后时刻。
使用AOF的优点:会让Redis变得非常耐久。可以设置不同的 Fsync 策略,AOF的默认策略是每秒钟 Fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。
缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 Fsync 策略,AOF 的速度可能会慢于 RDB。
RDB优点
- RDB 是紧凑的二进制文件,比较适合备份,全量复制等场景
- RDB 恢复数据远快于 AOF
RDB缺点
- RDB 无法实现实时或者秒级持久化
- 新老版本无法兼容 RDB 格式
AOF优点
- 可以更好地保护数据不丢失
- appen-only 模式写入性能比较高
- 适合做灾难性的误删除紧急恢复
AOF缺点:
- 对于同一份文件,AOF 文件要比 RDB 快照大
- AOF 开启后,会对写的 QPS 有所影响,相对于 RDB 来说 写 QPS 要下降
- 数据库恢复比较慢, 不合适做冷备。
数据库备份和灾难恢复:定时生成 RDB 快照非常便于进行数据库备份,并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快。当然Redis支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。
持久化配置
snapshot
RDB是Redis用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是Snapshot快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里
- save 900 1在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照
- save 300 10在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照
- save 60 1000在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
AOF
- appendfsync always每次有数据修改发生时都会写入AOF文件
- appendfsync everysec 每秒钟同步一次,该策略为AOF的缺省策略。在性能和持久化方面作了很好的折中
- appendfsync no从不同步。高效但是数据不会被持久化。
持久化机制建议
- 更新频繁,一致性要求比较高,AOF策略为主
- 更新不频繁,可以容忍少量数据丢失或错误,snapshot策略为主
Redis缓存
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。Redis提供了6种缓存失效策略。redis采用的是定期删除+惰性删除策略。
#####Redis缓存失效策略
- FIFO ,first in first out ,最先进入缓存的数据在缓存空间不够情况下(超出最大元素限制时)会被首先清理出去
- LFU , Less Frequently Used ,一直以来最少被使用的元素会被被清理掉。这就要求缓存的元素有一个hit 属性,在缓存空间不够得情况下,hit 值最小的将会被清出缓存。
- LRU ,Least Recently Used ,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
- noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )。
- allkeys-lru:所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
- allkeys-random: 所有key通用; 随机删除一部分 key。
- volatile-lru:只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
- volatile-random:只限于设置了 expire 的部分; 随机删除一部分 key。
- volatile-ttl:只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。
Redis 4.0加入了 LFU(least frequency use)淘汰策略,包括 volatile-lfu 和 allkeys-lfu,通过统计访问频率,将访问频率最少,即最不经常使用的 KV 淘汰。
定时过期策略
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期策略
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期策略
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis中同时使用了惰性过期和定期过期两种过期策略。
所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。
注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 redis 是每隔 100ms 随机抽取一些 key 来检查和删除的。
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?答案是:走内存淘汰机制。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置maxmemory-policy volatile-lru1
该配置就是配内存淘汰策略的
- volatile-lru从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction驱逐:禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件prerequisites; 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
缓存更新
面试问题1:通过expire可以设置key的过期时间,那么对过期的数据怎么处理呢?
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
1、MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
2、Redis的内存淘汰策略有哪些?
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
总结:Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
3、Redis的内存用完了会发生什么?
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
4、Redis如何做内存优化?
可以好好利用Hash、list、sorted set、set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表hashes,散列表使用的内存非常小,所以应该尽可能的将数据模型抽象到一个散列表里面。比如web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面
5、生产环境中的 redis 是怎么部署的?
redis cluster,10 台机器,5 台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?
商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
我们公司一般会有基础架构的 team 负责缓存集群的运维。
Redis缓存异常方案
缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。
从而形成一系列连锁反应,造成整个系统崩溃。
解决办法
大多数系统设计者考虑用加锁( 最多的解决方案)或者线程池的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
还有一个简单方案就时讲缓存失效时间分散开。
1、加锁排队。mutex互斥锁解决,Redis的SETNX去set一个mutex key,当操作返回成功时,再进行加载数据库的操作并回设缓存,否则,就重试整个get缓存的方法
2、数据预热。缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key
3、双层缓存策略。C1为原始缓存,C2为拷贝缓存,C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期
4、定时更新缓存策略。实效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存
5、设置不同的过期时间,让缓存失效的时间点尽量均匀
缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求。
缓存雪崩是因为大面积的缓存失效,打崩了DB。
缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。
防止缓存穿透
1、缓存空对象。简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
2、布隆过滤器
优势:占用内存空间很小,位存储;性能特别高,使用key的hash判断key存不存在。
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
解决办法
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决冲突。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
缓存降级
降级的情况,就是缓存失效或者缓存服务挂掉的情况下,也不去访问数据库。直接访问内存部分数据缓存或者直接返回默认数据。
对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。
降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
如果不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
解决思路:
- 直接写个缓存刷新页面,上线时手工操作下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
热点数据和冷数据
缓存数据的规则:读多写少,非敏感数据,数据量在可接受的范围内。数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
redis的主从结构
主从结构一是可以进行冗余备份,二是可以实现读写分离
主从复制
冗余备份,还可以称为主从复制、数据冗余、数据备份,可以实现容灾快速恢复。持久化保证了即使redis服务重启也会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,如果通过redis的主从复制机制就可以避免这种单点故障
搭建一个主叫做redis0,两个从,分别叫做redis1和redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。
1、一个Master可以有多个Slave,不仅主服务器可以有从服务器,从服务器也可以有自己的从服务器
2、复制在Master端是非阻塞模式的,这意味着即便是多个Slave执行首次同步时,Master依然可以提供查询服务
3、复制在Slave端也是非阻塞模式的:如果在redis.conf做了设置,Slave在执行首次同步的时候仍可使用旧数据集提供查询;也可以配置为当Master与Slave失去联系时,让Slave返回客户端一个错误提示
4、当Slave要删掉旧的数据集,并重新加载新版数据时,Slave会阻塞连接请求
Redis慢查询
慢查询就是指系统执行命令之后,计算统计每条指令执行的时间,如果执行时间超过设置的阈值,就说明该命令为慢指令。
Redis 慢查询配置参数为:
- slowlog-log-slower-than:设置慢查询定义阈值,超过这个阈值就是慢查询。单位为微妙,默认为 10000
- slowlog-max-len:慢查询的日志列表的最大长度,当慢查询日志列表处于最大长度的时候,最早插入的一个命令将从列表中移除。
针对主从复制问题中比较主流的解决方案就是哨兵。
哨兵模式,一个哨兵集群和一组主从架构组成。比主从更好的是当我们的主节点宕机以后,哨兵会主动选举出一个主节点继续向外提供服务。
总结
Redis缓存
-
使用场景
- 降低后端负载
- 加速请求响应
- 大量写合并为批量写
-
Redis缓存策略
- LRU、LFU、FIFO
- 超时剔除
- 主动更新
-
缓存粒度控制
- 通用性:全量属性更好
- 占用空间:部分属性更好
- 代码维护:表面上全量属性最好
-
缓存穿透问题
- 现象:请求访问redis,redis没有缓存,然后再访问mysql,但是mysql中也没有,下次在请求的时候,redis中也没有,这样就是缓存穿透的问题
- 解决方法
- 缓存空对象
- 布隆过滤器拦截
-
无底洞问题:更多的集器不代表更好的性能
- 命令行的优化,减少慢查询,比如:keys *,hgetall bigkey
- 减少网络通信次数
- 降低接入成本
-
热点key的重建
-
减少重缓存的次数
-
数据尽可能一致
-
减少潜在危险
-
解决方案
- 互斥锁:获取缓存的时候加锁,重建缓存完成之后再解锁
- 永不过期:逻辑上设置键是永不过期的
-
SpringBoot-redis编程使用
Spring Data
Spring Data: Spring 的一个子项目。用于简化数据库访问,支持NoSQL和关系数据库存储。其主要目标是使数据库的访问变得方便快捷。
Spring Data 项目所支持NoSQL存储:
- MongoDB(文档数据库)
- Neo4j (图形数据库)
- Redis(键/值存储)
- Hbase(列族数据库)
Spring Data 项目所支持的关系数据存储技术:
- JDBC
- JPA(Hibernate)
JPA Spring Data: 致力于减少数据访问层(DAO)的开发量。开发者唯一要做的,就只是声明持久层的接口,其他都交给Spring Data JPA 来帮你完成!
JPA Spring Data的使用
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
添加核心配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 设置表的自动创建模式,有5种不同的值,update表示自动同步修改表结构,实际上能增不能减
show-sql: true # 在控制台上显示sql语句
定义实体类,在类上添加JPA注解声明类和表种列之间的对应关系
@Data
@Entity//用于声明实体类
@Table(name = "t_users")//声明对应的表名称
public class User implements Serializable {
@Id //用于声明标识属性
@GeneratedValue(strategy = GenerationType.IDENTITY) //声明主键的生成策略
private Long id;
@Column(name = "username",unique = true,nullable = false,length = 20)//username属性对应的列声明
private String username;
@Column(length = 20,nullable = false)
private String password;
@Temporal(TemporalType.DATE) //使用日期模板表示该属性只包含年月日
@Column(columnDefinition = "timestamp default current_timestamp") //使用MySQL的本地方言创建对应的列
private LocalDate birth;
@Column(columnDefinition = "boolean default 1")
private Boolean sex;
@ManyToOne(optional = false,fetch = FetchType.EAGER)
@JoinColumn(name = "role_id")
private Role role=new Role();
}
定义数据访问接口,只需要继承只需要继承JpaRepository接口即可
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
}
测试
@SpringBootTest
class Redis02ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
}
@Test
void testCreate(){
User user=new User();
user.setUsername("猴子1");
user.setPassword("666666");
userRepository.save(user);
System.out.println(user.getId());
}
}
Spring data Redis
Spring-data-redis是spring大家族的一部分,提供了在spring应用中通过简单的配置访问redis服务,对redis底层开发包(Jedis, lettuce and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。
- 连接池自动管理,提供了一个高度封装的“RedisTemplate”类
- 针对客户端中大量api进行了归类封装,将同一类型操作封装为operation接口
- ValueOperations:简单K-V操作
- SetOperations:set类型数据操作
- ZSetOperations:zset类型数据操作
- HashOperations:针对map类型的数据操作
- ListOperations:针对list类型的数据操作
通过RedisTemplate编程使用
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
redis的配置
spring:
redis:
host: localhost
port: 6379
lettuce:
pool:
max-active: 10
min-idle: 2