redis-总结列表
基础
启动命令
redis-server kconfig/redis.conf | 通过指定的配置文件启动服务(kconfig/redis.conf是复制过来的) |
---|---|
redis-cli -p 6379 | 使用Redis客户端进行连接 |
ping | 测试连接 |
ps -ef|grep redis | 查看Redis是否启动 |
shutdown | 关闭Redis服务 |
exit | 退出 |
数据库相关命令
set key value | 存放键值 |
---|---|
get key | 取出键值 |
del key | 删除键值 |
move key db | 移动键值到指定的数据库 |
keys * | 查看所有的键值 |
select db | 切换数据库 |
dbsize | 查看数据库大小 |
flushdb | 清除当前数据库 |
flushall | 清除全部数据库的内容 |
RedisKey
expire key time | 设置键值的过期时间(秒) |
---|---|
ttl key | 查看键值剩余过期时间(-2表示没了) |
type key | 查看键值的类型 |
系统相关命令
--删 flushdb --清空当前选择的数据库 del mykey mykey2 --删除了两个 Keys --改 move mysetkey 1 --将当前数据库中的 mysetkey 键移入到 ID 为 1 的数据库中 rename mykey mykey1 --将 mykey 改名为 mykey1 renamenx oldkey newkey --如果 newkey 已经存在,则无效 expire mykey 100 --将该键的超时设置为 100 秒 persist mykey --将该 Key 的超时去掉,变成持久化的键 --查 keys my* --获取当前数据库中所有以my开头的key exists mykey --若不存在,返回0;存在返回1 select 0 --打开 ID 为 0 的数据库 ttl mykey --查看还有多少秒过期,-1表示永不过期,-2表示已过期 type mykey --返回mykey对应的值的类型
1、五种数据类型
字符串(String)、哈希(Map)、列表(List)、集合(Sets)和有序集合(Sorted sets)
三种特殊数据类型
geospatial 地理位置
geospatial 底层是zset,所以可以用zset的方法操作geospatial;
geoadd key longitude latitude member [longitude latitude member | 添加城市经纬度(两极无法直接添加) |
---|---|
geopos key member [member ...] | 获取城市的经纬度 |
geodist key member1 member2 [m|km|ft|mi] | 获取两个城市之间的距离 |
georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] | 某座标指定半径范围的坐标数 |
georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WIT | 某城市指定半径范围的城市 |
geohash key member [member ...] | 返回一个或多个11位字符的Geohash字符串 |
Hyperloglog
功能:基数,不重复元素的个数;
优点:占内存是固定的,2^64,不同的元素的计数,只需要费12kB内存;如果从内存的角度来比较的话Hyperloglog首选;0.81%错误率;如果有的场景不允许容错率则不能使用,可以使用set;
pfadd key element [element ...] | 添加一组元素 |
---|---|
pfcount key [key ...] | 一组元素中不重复的元素个数 |
pfmerge destkey sourcekey [sourcekey ...] | 合并多组元素->并集 |
BitMaps
位存储;一般存储两个状态的数据; 010100
setbit key offset value | 添加元素(例如:setbit sign 0 1=>登录第几天是否打卡 ) |
---|---|
getbit key offset | 获取指定offset的值 |
bitcount key [start end] | 计算有几个值为1的元素 |
一、String(字符串)
(1) 存储一些配置数据
(2) 缓存对象
(3) 数据统计
(4) 时间内限制请求次数
(5) 订单号(全局唯一)
(6) 分布式session
计数
由于Redis单线程的特点,我们不用考虑并发造成计数不准的问题,通过 incrby 命令,我们可以正确的得到我们想要的结果。
限制次数
比如登录次数校验,错误超过三次5分钟内就不让登录了,每次登录设置key自增一次,并设置该key的过期时间为5分钟后,每次登录检查一下该key的值来进行限制登录。
二、Hash(哈希)
(1) Redisson分布式锁
(2) 购物车列表
(3) 缓存对象
查询的时间复杂度是O(1),用于缓存一些信息。
三、List(列表)
(1) 消息队列
典型使用场景
一、栈
通过命令 lpush+lpop
二、队列
命令 lpush+rpop
三、有限集合
命令 lpush+ltrim
四、消息队列
命令 lpush+brpop
四、set(集合)
(1) 抽奖活动 存储某活动中中奖的用户ID ,因为有去重功能,可以保证同一个用户不会中奖两次。
(2) 点赞 保证一个用户只能点一个赞。key 可以是某某文章、微信朋友圈的文章id
(3) 好友人脉 key 可以是 用户id
利用集合的交并集特性,比如在社交领域,我们可以很方便的求出多个用户的共同好友,共同感兴趣的领域等。
五、zset(有序集合)
- 排行榜(商品销量,视频评分,用户游戏分数)
- 新闻热搜。
- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 支持丰富数据类型,支持string,list,set,sorted set,hash
- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
为什么Redis是单线程的
1.官方答案
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
Redis的高并发和快速原因
1.redis是基于内存的,内存的读写速度非常快;
2.redis是单线程的,省去了很多上下文切换线程的时间;
3.redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
2.性能指标
关于redis的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。
3.详细原因
1)不需要各种锁的性能消耗
Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除
一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
2)单线程多进程集群方案
单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。
所以单线程、多进程的集群不失为一个时髦的解决方案。
3)CPU消耗
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。
但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?
可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。
Redis单线程的优劣势
单进程单线程优势
- 代码更清晰,处理逻辑更简单
- 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
- 不存在多进程或者多线程导致的切换而消耗CPU
单进程单线程弊端
- 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
IO多路复用技术
redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。
多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。
Redis高并发快总结
1. Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
2. 再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
3. Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
4. 另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
5. 还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
Redis特点:
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据,同时还提供String,list,set,zset,hash等数据结构的存储。
Redis支持数据的备份,即master-slave模式的数据备份。
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
丰富的特性 – Redis还支持 publish/subscribe, 通知, 设置key有效期等等特性。
redis作用:
可以减轻数据库压力,查询内存比查询数据库效率高。
Redis应用:
token生成、session共享、分布式锁、自增id、验证码等。
比较重要的3个可执行文件:
redis-server:Redis服务器程序
redis-cli:Redis客户端程序,它是一个命令行操作工具。也可以使用telnet根据其纯文本协议操作。
redis-benchmark:Redis性能测试工具,测试Redis在你的系统及配置下的读写性能。
Redis支持的数据类型
Redis通过Key-Value的单值不同类型来区分, 以下是支持的类型:
Strings
Lists
Sets 求交集、并集
Sorted Set
hashes
为什么redis需要把所有数据放到内存中?
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。
如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
Redis是单进程单线程的
redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销
虚拟内存
当你的key很小而value很大时,使用VM的效果会比较好.因为这样节约的内存比较大.
当你的key不小时,可以考虑使用一些非常方法将很大的key变成很大的value,比如你可以考虑将key,value组合成一个新的value.
vm-max-threads这个参数,可以设置访问swap文件的线程数,设置最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的.可能会造成比较长时间的延迟,但是对数据完整性有很好的保证.
分布式
redis支持主从的模式。原则:Master会将数据同步到slave,而slave不会将数据同步到master。Slave启动时会连接master来同步数据。
这是一个典型的分布式读写分离模型。我们可以利用master来插入数据,slave提供检索服务。这样可以有效减少单个机器的并发访问数量
读写分离模型
通过增加Slave DB的数量,读的性能可以线性增长。为了避免Master DB的单点故障,集群一般都会采用两台Master DB做双机热备,所以整个集群的读和写的可用性都非常高。
读写分离架构的缺陷在于,不管是Master还是Slave,每个节点都必须保存完整的数据,如果在数据量很大的情况下,集群的扩展能力还是受限于单个节点的存储能力,而且对于Write-intensive类型的应用,读写分离架构并不适合。
数据分片模型
为了解决读写分离模型的缺陷,可以将数据分片模型应用进来。
可以将每个节点看成都是独立的master,然后通过业务实现数据分片。
结合上面两种模型,可以将每个master设计成由一个master和多个slave组成的模型。
1. 使用Redis有哪些好处?
(1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
(2) 支持丰富数据类型,支持string,list,set,sorted set,hash
(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
(4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
2. redis相比memcached有哪些优势?
(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
(2) redis的速度比memcached快很多
(3) redis可以持久化其数据
3. redis常见性能问题和解决方案:
(1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
(2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...
这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
4. MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
相关知识:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略:
voltile-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(驱逐):禁止驱逐数据
5. Memcache与Redis的区别都有哪些?
1)、存储方式
Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
Redis有部份存在硬盘上,这样能保证数据的持久性。
2)、数据支持类型
Memcache对数据类型支持相对简单。
Redis有复杂的数据类型。
3)、使用底层模型不同
它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。
Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4),value大小
redis最大可以达到1GB,而memcache只有1MB
6. Redis 常见的性能问题都有哪些?如何解决?
1).Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
2).Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
3).Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
4). Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
7, redis 最适合的场景
Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就会有疑问,似乎Redis更像一个加强版的Memcached,那么何时使用Memcached,何时使用Redis呢?
如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:
1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
2 、Redis支持数据的备份,即master-slave模式的数据备份。
3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
应用场景
1)、会话缓存(Session Cache)
最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。
2)、全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。
再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。
此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
3)、队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。
4),排行榜/计数器
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。
5)、发布/订阅
最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。
---------------------------------------------
2、架构设计
高可用有三种部署模式:主从模式,哨兵模式,集群模式。
1.主从模式:读写分离,备份,一个Master可以有多个Slaves。
2.哨兵sentinel:监控,自动转移,哨兵发现主服务器挂了后,就会从slave中重新选举一个主服务器。
3.集群:为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。
Redis 主从复制、哨兵和集群这三个有什么区别
主从模式:备份数据、负载均衡,一个Master可以有多个Slaves。
sentinel发现master挂了后,就会从slave中重新选举一个master。
cluster是为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器。
sentinel着眼于高可用,Cluster提高并发量。
主从模式
Master节点,负责读写操作,Slave节点,只负责读操作。
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点 (master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。 Master以写为主,Slave 以读为主。
默认情况下,每台Redis服务器都是主节点;
且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
Redis 复制(Replication)
Redis为了解决单点数据库问题,会把数据复制多个副本部署到其他节点上,通过复制,实现Redis的高可用性,实现对数据的冗余备份,保证数据和服务的高度可靠性。
①从数据库向主数据库发送sync(数据同步)命令。
②主数据库接收同步命令后,会保存快照,创建一个RDB文件。
③当主数据库执行完保持快照后,会向从数据库发送RDB文件,而从数据库会接收并载入该文件。
④主数据库将缓冲区的所有写命令发给从服务器执行。
⑤以上处理完之后,之后主数据库每执行一个写命令,都会将被执行的写命令发送给从数据库。
主从复制的作用主要包括:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务 的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写 少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复 制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较 大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有 内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
Slave 启动成功连接到 master 后会发送一个sync同步命令 ;
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行 完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步 ;
复制 REPLICATION 主从复制
# slaveof <masterip> <masterport> #masterip:主机地址 masterport:主机端口号
主从复制原理
(1) 全量同步
(2) 增量同步
(3) 部分同步
主从模式的优缺点
优点
-
做到读写分离,提高服务器性能。Salve可以分载Master的读操作压力,当然写服务依然必须由Master来完成;
-
当Master节点服务挂了,可以让Slave变成Master节点继续提供服务;
缺点
-
在主从模式中,一旦Master节点由于故障不能提供服务,需要人工将Slave节点晋升为Master节点,同时还要通知应用方更新Master节点地址。显然,多数业务场景都不能接受这种故障处理方式;
-
redis的Master节点和Slave节点中的数据是一样的,降低的内存的可用性,而且存储能力也有限。
-
主从复制写还都是在Master节点,所以写的压力并没有减少。
哨兵模式
哨兵模式
,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Master节点和Slave节点,并在被监视的Master节点进入下线状态时,自动将下线Master服务器
属下的某个Slave节点升级为新的Master节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,
sentinel是一种特殊的redis实例,它不存储数据,只对集群进行监控。
1、哨兵主要工作任务
任务1:每个哨兵节点每 10 秒会向Master节点和Slave节点发送 info 命令获取最拓扑结构图,哨兵配置时只要配置对Master节点的监控即可,通过向Master节点发送info,
获取Slave节点的信息,并当有新的Slave节点加入时可以马上感知到
任务2,每个哨兵节点每隔 2 秒会向redis 数据节点的指定频道上发送该哨兵节点对于Master节点的判断以及当前哨兵节点的信息,同时每个哨兵节点也会订阅该频道,来了解
其它哨兵节点的信息及对Master节点的判断,其实就是通过消息publish 和subscribe 来完成的;
任务3,每隔 1 秒每个哨兵会向Master节点、Slave节点及其余哨兵节点发送一次ping 命令做一次心跳检测,这个也是哨兵用来判断节点是否正常的重要依据
哨兵模式的优缺点
哨兵(sentinal)
哨兵是Redis集群架构中非常重要的一个组件,哨兵的出现主要是解决了主从复制出现故障时需要人为干预的问题。
1.Redis哨兵主要功能
(1)集群监控:负责监控Redis master和slave进程是否正常工作
(2)消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
(3)故障转移:如果master node挂掉了,会自动转移到slave node上
(4)配置中心:如果故障转移发生了,通知client客户端新的master地址
哨兵的优缺点
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有;
- 主从可以切换,故障可以转移,系统的可用性就会更好;
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮;
缺点
- 具有主从模式的缺点,每台机器上的数据是一样的,内存的可用性较低。
- 还要多维护一套哨兵模式,实现起来也变的更加复杂增加维护成本。
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
原理:基本上哪个哨兵节点最先判断出这个主节点客观下线,就会在各个哨兵节点中发起投票机制Raft算法(选举算法),最终被投为领导者的哨兵节点完成主从自动化切换的过程。
Cluster集群模式
Redis的集群模式本身没有使用一致性hash算法,而是使用slots插槽。
它实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。
一个Redis Cluster由多个Redis节点构成,节点组内部分为主备两类节点,对应master和slave节点。两者数据准实时一致,通过异步化的主备复制机制来保证。
一个节点组有且只有一个master节点,同时可以有0到多个slave节点,在这个节点组中只有master节点对用户提供些服务,读服务可以由master或者slave提供。如上图中,
包含三个master节点以及三个master对应的slave节点,一般一组集群至少要6个节点才能保证完整的高可用。
其中三个master会分配不同的slot(表示数据分片区间),当master出现故障时,slave会自动选举成为master顶替Master节点继续提供服务。
1、集群的一些特点
- redis cluster模式采用了无中心节点的方式来实现,每个Master节点都会与其它Master节点保持连接。节点间通过gossip协议交换彼此的信息,同时每个Master节点又有
一个或多个Slave节点;
-
客户端连接集群时,直接与redis集群的每个Master节点连接,根据hash算法取模将key存储在不同的哈希槽上;
-
在集群中采用数据分片的方式,将redis集群分为16384个哈希槽。如下图所示,这些哈希槽分别存储于三个Master节点中:
- Master1负责0~5460号哈希槽
- Master2负责5461~10922号哈希槽
- Master3负责10922~16383号哈希槽
2、Master节点故障处理方式
redis 集群中Master节点故障处理方式与哨兵模式较为相像,当约定时间内某节点无法与集群中的另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态,同时
将这个信息向整个集群广播。
如果一个节点收到某个节点失联的数量达到了集群的大多数时,那么将该节点标记为客观下线状态,并向集群广播下线节点的fail消息。然后立即对该故障节点进行主从切换。
等到原来的Master节点恢复后,会自动成为新Master节点的Slave节点。如果Master节点没有Slave节点,那么当它发生故障时,集群就将处于不可用状态。
3、扩容问题
在cluster中我们如何动态上线某个节点呢。当集群中加入某个节点时,哈希槽又是如何来进行分配的?当集群中加入新节点时,会与集群中的某个节点进行握手,该节点会把集群
内的其它节点信息通过gossip协议发送给新节点,新节点与这些节点完成握手后加入到集群中。
然后集群中的节点会各取一部分哈希槽分配给新节点,如下图:
- Master1负责1365-5460
- Master2负责6827-10922
- Master3负责12288-16383
- Master4负责0-1364,5461-6826,10923-12287
4、关于 gossip协议
有关gossip协议这里需要单独解释下
在整个redis cluster架构中,如果出现以下情况
- 新加入节点
- slot迁移
- 节点宕机
- slave选举成为master
5、gossip的优缺点
优点
: gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新有一定的延时,降低了压力; 去中心化、可扩展、
容错、一致性收敛、简单。 由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。
缺点
: 元数据更新有延时可能导致集群的一些操作会有一些滞后。 消息的延迟 , 消息冗余 。
-------------------------------------------
3、Redis持久化
RDB (默认方式)
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快 照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,持久化过程 都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。 这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
客户端也可以使用save或者bgsave命令通知redis做一次快照持久化
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
Save分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。
Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
AOF
所有命令都记录下来,history,恢复的时候就是把这个文件全部在执行一遍。以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件 但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件 的内容将写指令从前到后执行一次以完成数据的恢复工作;
appendonly yes //启用日志追加持久化方式
appendfsync always //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全持久化,不推荐使用。
appendfsync everysec //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐使用。
appendfsync no //完全依赖操作系统,性能最好,持久化没保证。
1、RDB的优点
(1)比起AOF,在数据量比较大的情况下,RDB的启动速度更快。
(2)RDB文件是一个很简洁的单文件,它保存了某个时间点的Redis数据,很适合用于做备份。
(3)RDB很适合用于灾备。单文件很方便就能传输到远程的服务器上。
(4)RDB的性能很好,需要进行持久化时,主进程会fork一个子进程出来,然后把持久化的工作交给子进程,自己不会有相关的I/O操作。
2、RDB缺点
(1)RDB容易造成数据的丢失。假设每5分钟保存一次快照,如果Redis因为某些原因不能正常工作,那么从上次产生快照到Redis出现问题这段时间的数据就会丢失了。
(2)RDB使用fork()产生子进程进行数据的持久化,如果数据比较大的话可能就会花费点时间,造成Redis停止服务几毫秒。
3、AOF优点
(1)该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,如果发生灾难,您只可能会丢失1秒的数据。
(2)AOF日志文件是一个纯追加的文件。就算服务器突然Crash,也不会出现日志的定位或者损坏问题。甚至如果因为某些原因(例如磁盘满了)命令只写了一半到日志文件里,我们也可以用redis-check-aof这个工具很简单的进行修复。
(3)当AOF文件太大时,Redis会自动在后台进行重写。重写很安全,因为重写是在一个新的文件上进行,同时Redis会继续往旧的文件追加数据。
4、AOF缺点
(1)在相同的数据集下,AOF文件的大小一般会比RDB文件大。
(2)在某些fsync策略下,AOF的速度会比RDB慢。通常fsync设置为每秒一次就能获得比较高的性能,而在禁止fsync的情况下速度可以达到RDB的水平。
RDB和AOF建议
(1)官方建议:是同时开启两种持久化策略。因为有时需要RDB快照是进行数据库备份,更快重启以及发生AOF引擎错误的解决办法。(换句话就是通过RDB来多备份一份数据总是好的)
(2) 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
(3)如果选择AOF,只要硬盘许可,应该尽量减少AOF rewrite的频率。因为一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。
总结
1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储 ;
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始 的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重 写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化 ;
4、同时开启两种持久化方式
- 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF 文件保存的数据集要比RDB文件保存的数据集要完整。
- RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有 AOF可能潜在的Bug,留着作为一个万一的手段。
5、性能建议
- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够 了,只保留 save 900 1 这条规则。
- 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自 己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产 生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite 的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重 写可以改到适当的数值。
- 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也 减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时宕掉,会丢失十几分钟的数据, 启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。
4、Redis虚拟内存
首先说明下redis的虚拟内存与操作系统虚拟内存不是一码事,但是思路和目的都是相同的。就是暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的内存空间。对于 redis这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis 服务器以外。另外的能够提高数据库容量的办法就是使用虚拟内存技术把那些不经常访问的数据交换到磁盘上。
vm-enabled yes 开启虚拟内存功能
vm-swap-file /tmp/redis.swap 交换出来value保存的文件路径/tmp/redis.swapvm-max-memory268435456 redis使用的最大内存上限(256MB),超过上限后 redis开始交换value到磁盘swap文件中。建议设置为系统空闲内存的60 %-80%
vm-page-size 32 每个redis页的大小32个字节
vm-pages 134217728 最多在文件中使用多少个页,交换文件的大小 =(vm-page-size* vm-pages)4 GB
vm-max-threads 8 用于执行value对象换入换出的工作线程数量。0 表示不使用工作线程
6、redis实现加锁的几种方法示例详解
1. redis加锁分类
redis能用的的加锁命令分表是INCR、SETNX、SET
2. 第一种锁命令INCR
这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。
然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。
1、 客户端A请求服务器获取key的值为1表示获取了锁
2、 客户端B也去请求服务器获取key的值为2表示获取锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
5、 客户端B执行代码完成,删除锁
1
2
|
$redis ->incr( $key ); $redis ->expire( $key , $ttl ); //设置生成时间为1秒 |
3. 第二种锁SETNX
这种加锁的思路是,如果 key 不存在,将 key 设置为 value
如果 key 已存在,则 SETNX 不做任何动作
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁
1
2
|
$redis ->setNX( $key , $value ); $redis ->expire( $key , $ttl ); |
4. 第三种锁SET
上面两种方法都有一个问题,会发现,都需要设置 key 过期。那么为什么要设置key过期呢?如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测。
但是借助 Expire 来设置就不是原子性操作了。所以还可以通过事务来确保原子性,但是还是有些问题,所以官方就引用了另外一个,使用 SET 命令本身已经从版本 2.6.12 开始包含了设置过期时间的功能。
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁
1
|
$redis ->set( $key , $value , array ( 'nx' , 'ex' => $ttl )); //ex表示秒 |
7、Redis的过期策略
Redis的内存回收主要围绕以下两个方面:
1.Redis过期策略:删除过期时间的key值
2.Redis淘汰策略:内存使用到达maxmemory上限时触发内存淘汰数据
Redis的过期策略和内存淘汰策略不是一件事
1、设置过期时间
- expire key time(以秒为单位)--这是最常用的方式
- setex(String key, int seconds, String value)--字符串独有的方式
1.定时过期
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
2.惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
3.定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
2.Redis淘汰策略:
Redis的内存淘汰策略,是指当内存使用达到maxmemory极限时,需要使用LAU淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
内存使用到达maxmemory上限时触发内存淘汰数据
Redis默认情况下就是使用LRU策略算法。
LRU算法(least RecentlyUsed),最近最少使用算法,也就是说默认删除最近最少使用的键。
缓存清理配置
maxmemory用来设置redis存放数据的最大的内存大小,一旦超出这个内存大小之后,就会立即使用LRU算法清理掉部分数据。
Redis数据淘汰策略
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
三种过期策略
- 定时删除
- 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
- 优点:保证内存被尽快释放
- 缺点:
- 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
- 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
- 没人用
- 惰性删除
- 含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
- 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
- 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
- 定期删除
- 含义:每隔一段时间执行一次删除过期key操作
- 优点:
- 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
- 定期删除过期key--处理"惰性删除"的缺点
- 缺点
- 在内存友好方面,不如"定时删除"
- 在CPU时间友好方面,不如"惰性删除"
- 难点
- 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)
Redis采用的过期策略
惰性删除+定期删除
- 惰性删除流程
- 在进行get或setnx等操作时,先检查key是否过期,
- 若过期,删除key,然后执行相应操作;
- 若没过期,直接执行相应操作
- 定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)
- 遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
- 检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体时下边的描述)
- 如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
- 随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
- 判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
- 检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体时下边的描述)
- 遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
RDB对过期key的处理
过期key对RDB没有任何影响
- 从内存数据库持久化数据到RDB文件
- 持久化key之前,会检查是否过期,过期的key不进入RDB文件
- 从RDB文件恢复数据到内存数据库
- 数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
AOF对过期key的处理
过期key对AOF没有任何影响
- 从内存数据库持久化数据到AOF文件:
- 当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
- 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
- AOF重写
- 重写时,会先判断key是否过期,已过期的key不会重写到aof文件
Redis常见性能问题和解决方案
- Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
- 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…。这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
缓存带来的复杂度问题
常见的问题主要包括如下几点:
-
数据一致性
-
缓存穿透
-
缓存雪崩
-
缓存高可用
-
缓存热点
数据一致性
因为缓存属于持久化数据的一个副本,因此不可避免的会出现数据不一致问题,导致脏读或读不到数据的情况。
数据不一致,一般是因为网络不稳定或节点故障导致问题出现的常见 3 个场景以及解决方案:
缓存穿透
缓存一般是 Key-Value 方式存在,当某一个 Key 不存在时会查询数据库,假如这个 Key,一直不存在,则会频繁的请求数据库,对数据库造成访问压力。
主要解决方案:
-
对结果为空的数据也进行缓存,当此 Key 有数据后,清理缓存。
-
一定不存在的 Key,采用布隆过滤器,建立一个大的 Bitmap 中,查询时通过该 Bitmap 过滤。
缓存雪崩
缓存高可用
缓存是否高可用,需要根据实际的场景而定,并不是所有业务都要求缓存高可用,需要结合具体业务,具体情况进行方案设计,例如临界点是否对后端的数据库造成影响。
主要解决方案:
-
分布式:实现数据的海量缓存。
-
复制:实现缓存数据节点的高可用。
缓存热点
一些特别热点的数据,高并发访问同一份缓存数据,导致缓存服务器压力过大
解决:复制多份缓存副本,把请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力
支持,使用multi开启事务,使用exec提交事务。
Redis 通过 MULTI 、EXEC、 DISCARD 和 WATCH 四个命令来实现事务功能。
MULTI :标记一个事务块的开始。
EXEC: 执行所有事务块内的命令。
DISCARD :取消事务,放弃执行事务块内的所有命令。
WATCH key [key ...] :监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
注意:
(1)当遇到执行错误时,redis放过这种错误,保证事务执行完成。(所以说redis的事务并不是保证原子性)
(2)与mysql中事务不同,在redis事务遇到执行错误的时候,不会进行回滚,而是简单的放过了,并保证其他的命令正常执行。这个区别在实现业务的时候,需要自己保证逻辑符合预期。
(3)当事务的执行过程中,如果redis意外的挂了。很遗憾只有部分命令执行了,后面的也就被丢弃了。当然如果我们使用的append-only file方式持久化,redis会用单个write操作写入整个事务内容。即是是这种方式还是有可能只部分写入了事务到磁盘。发生部分写入事务的情况 下,redis重启时会检测到这种情况,然后失败退出。可以使用redis-check-aof工具进行修复,修复会删除部分写入的事务内容。修复完后就 能够重新启动了。
脑裂导致数据丢失
脑裂 也就是说,某个 Master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启
选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 Master ,也就是所谓的脑裂。
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave
挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
解决脑裂问题
Redis 已经提供了两个配置项来限制Master库的请求处理,分别是 min-slaves-to-write
和 min-slaves-max-lag
。 (2.8以后改为min-replicas-to-write 和 min-replicas-max-lag )
min-slaves-to-write 1
min-slaves-max-lag 10
如上两个配置:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒,如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就
不会再接收任何请求了。
这样一配置的话,就算你的Master库是假故障,那它在假故障期间也无法响应哨兵心跳,也不能和Slave库进行同步,自然也就无法和Slave库进行 ACK 确认了。原Master库就
会被限制接收客户端请求,客户端也就不能在原Master库中写入新数据了。
当然这个配置做不到让数据一点也不丢失,而是让数据尽可能的少丢失。
------------------------
缓存穿透是指查询一个缓存和数据库不存在的数据
。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
缓存穿透是指查询一个一不存在的数据。例如:从缓存redis没有命中,需要从mysql数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于 是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒 杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了 缓存穿透。
解决思路:
解决方案
1) 验证拦截
接口层进行校验,如鉴定用户权限,对ID之类的字段做基础的校验,如id<=0的字段直接拦截。
2) 布隆过滤器
我们可以提前将真实正确的商品id,添加到过滤器当中,每次再进行查询时,先确认要查询的id是否在过滤器当中,如果不在,则说明id为非法id,则不需要进行后续的查询步骤了。
布隆过滤器是一种比较独特数据结构,有一定的误差。布隆过滤器的特点就是 如果它说不存在那肯定不存在,如果它说存在,那数据有可能实际不存在
。
它最大的优点就是性能高,空间占用率及小。
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则 丢弃,从而避免了对底层存储系统的查询压力;
如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。
可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。
解决方案:
(一)利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
(二)采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
(三)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
3) 缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
缓存击穿
是指缓存中没有但数据库中有的数据
,并且某一个key非常热点
,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key缓存时间到期,持续的大并发就穿破缓存,直接请求数据库,导致压垮数据库。
1、原理
缓存击穿:指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。”
缓存击穿看着有点像缓存雪崩,其实它两区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是在于击穿针对某一热点key缓存,雪奔则是很多key。
2、解决方法
解决方案就有两种:
使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
“永不过期”。是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
缓存雪崩
数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
1.缓存的高可用性
缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
2.缓存降级
可以利用ehcache等本地缓存(暂时支持),但主要还是对源服务访问进行限流、资源隔离(熔断)、降级等。
当访问量剧增、服务出现问题仍然需要保证服务还是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级,这里会涉及到运维的配合。
降级的最终目的是保证核心服务可用,即使是有损的。
3.Redis备份和快速预热
1)Redis数据备份和恢复
2)快速缓存预热
4.提前演练
最后,建议还是在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,对高可用提前预演,提前发现问题。
解决方案:
(一)给缓存的失效时间,加上一个随机值,避免集体失效。
(二)使用互斥锁,但是该方案吞吐量明显下降了。
(三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
I 从缓存A读数据库,有则直接返回
II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
III 更新线程同时更新缓存A和缓存B。
15、缓存并发
这里的并发指的是多个redis的client同时set key引起的并发问题。其实redis自身就是单线程操作,多个client并发操作,按照先到先执行的原则,先到的先执行,其余的阻塞。当然,另外的解决方案是把redis.set操作放在队列中使其串行化,必须的一个一个执行。
16、缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。
这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
目的就是在系统上线前,将数据加载到缓存中。
以上就是缓存雪崩、预热、降级等的介绍。
Redis的数据同步方式
无论是主备还是主从都牵扯到数据同步的问题,这也分2种情况:
- 同步方式:当主机收到客户端写操作后,以同步方式把数据同步到从机上,当从机也成功写入后,主机才返回给客户端成功,也称数据强一致性。 很显然这种方式性能会降低不少,当从机很多时,可以不用每台都同步,主机同步某一台从机后,从机再把数据分发同步到其他从机上,这样提高主机性能分担同步压力。 在redis中是支持这杨配置的,一台master,一台slave,同时这台salve又作为其他slave的master。
- 异步方式:主机接收到写操作后,直接返回成功,然后在后台用异步方式把数据同步到从机上。 这种同步性能比较好,但无法保证数据的完整性,比如在异步同步过程中主机突然宕机了,也称这种方式为数据弱一致性。
Redis主从同步采用的是异步方式,因此会有少量丢数据的危险。还有种弱一致性的特例叫最终一致性,这块详细内容可参见CAP原理及一致性模型。
分布式
缓存数据量不断增加时,单机内存不够使用,需要把数据切分不同部分,分布到多台服务器上。 可在客户端对数据进行分片,数据分片算法详见一致性Hash详解、虚拟桶分片。
集群:至少部署两台Redis服务器构成一个小的集群,主要有2个目的:
- 高可用性:在主机挂掉后,自动故障转移,使前端服务对用户无影响。
- 读写分离:将主机读压力分流到从机上。
可在客户端组件上实现负载均衡,根据不同服务器的运行情况,分担不同比例的读请求压力。
23、redis和数据库双写一致性问题
1、旁路缓存模式
一般我们使用缓存,都是旁路缓存模式,它的特点就是读的时候插入缓存,写的时候删除缓存
。
2、删除缓存呢,还是更新缓存?
分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
回答:首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
监听数据binlog更新缓存,保障不丢数据
Redis的作者在Redis5.0中,放出一个新的数据结构,Stream。Redis Stream 的内部,其实也是一个队列,每一个不同的key,对应的是不同的队列,每个队列的元素,也就是消息,都有一个msgid,并且需要保证msgid是严格递增的。在Stream当中,消息是默认持久化的,即便是Redis重启,也能够读取到消息。那么,stream是如何做到多播的呢?其实非常的简单,与其他队列系统相似,Redis对不同的消费者,也有消费者Group这样的概念,不同的消费组,可以消费同一个消息,对于不同的消费组,都维护一个Idx下标,表示这一个消费群组消费到了哪里,每次进行消费,都会更新一下这个下标,往后面一位进行偏移。
①、save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave(这个命令下面会介绍,手动触发RDB持久化的命令)
默认如下配置:
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存 save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存 save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。可以直接一个空字符串来实现停用:save ""
②、stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了
③、rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
④、rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
⑤、dbfilename :设置快照的文件名,默认是 dump.rdb
⑥、dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。默认是和当前配置文件保存在同一目录。
也就是说通过在配置文件中配置的 save 方式,当实际操作满足该配置形式时就会进行 RDB 持久化,将当前的内存快照保存在 dir 配置的目录中,文件名由配置的 dbfilename 决定。
32、恢复数据
将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可,redis就会自动加载文件数据至内存了。Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。
获取 redis 的安装目录可以使用 config get dir 命令
33、设置主从关系
②、选择6380端口和6381端口,执行命令:SLAVEOF 127.0.0.1 6379
②、全量复制
通过执行 SLAVEOF 127.0.0.1 6379,如果主节点 6379 以前还存在一些 key,那么执行命令之后,从节点会将以前的信息也都复制过来吗?
答案也是肯定的,这里我就不贴测试结果了。
③、主从读写分离
主节点能够执行写命令,从节点能够执行写命令吗?这里的原因是在配置文件 6381redis.conf 中对于 slave-read-only 的配置
34、用Redis作为限流组件的核心的原理,将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间。
比如某接口设置相同IP10秒
内请求5次
,超过5次不让访问该接口。
1. 第一次该IP地址存入redis的时候,key值为IP地址,value值为1,设置key值过期时间为10秒。
2. 第二次该IP地址存入redis时,如果key没有过期,那么更新value为2。
3. 以此类推当value已经为5时,如果下次该IP地址在存入redis同时key还没有过期,那么该Ip就不能访问了。
4. 当10秒后,该key值过期,那么该IP地址再进来,value又从1开始,过期时间还是10秒,这样反反复复。
38、Redisson实现分布式锁
1、互斥
在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
2、防止死锁
在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式非常有必要设置锁的有效时间
,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
3、性能
对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。
所以在锁的设计时,需要考虑两点。
1、锁的颗粒度要尽量小
。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
2、锁的范围尽量要小
。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
4、重入
我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。
1、加锁机制
线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
2、watch dog自动延期机制
这个比较难理解,找了些许资料感觉也并没有解释的很清楚。这里我自己的理解就是:
在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,你也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生。
3、可重入加锁机制
Redisson可以实现可重入加锁机制的原因,我觉得跟两点有关:
1、Redis存储锁的数据类型是 Hash类型
2、Hash数据类型的key值包含了当前线程信息。
5、Redis分布式锁的缺点
Redis分布式锁会有个缺陷,就是在Redis哨兵模式下:
客户端1
对某个master节点
写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。
这时客户端2
来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
缺陷
在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。
使用缓存,可以降低耗时,提供系统吞吐性能。但是,使用缓存,会存在数据一致性的问题。
1、几种缓存使用模式
- Cache-Aside Pattern,旁路缓存模式
- Read-Through/Write-Through(读写穿透)
- Write- behind (异步缓存写入)
RedisTemplate和StringRedisTemplate的区别:
两者的关系是StringRedisTemplate继承RedisTemplate。
两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的(StringRedisSerializer)。
RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。(JdkSerializationRedisSerializer)
缓存热Key
1、原理
在Redis中,我们把访问频率高的key,称为热点key。如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
2、解决方法
如何解决热key问题?
Redis集群扩容:增加分片副本,均衡读流量;
对热key进行hash散列,比如将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量;
使用二级缓存,即JVM本地缓存,减少Redis的读请求。
六、缓存容量内存考虑
1、评估容量,合理利用
如果我们使用的是Redis,而Redis的内存是比较昂贵的,我们不要什么数据都往Redis里面塞,一般Redis只缓存查询比较频繁的数据。同时,我们要合理评估Redis的容量,也避免频繁set覆盖,导致设置了过期时间的key失效。
如果我们使用的是本地缓存,如guava的本地缓存,也要评估下容量。避免容量不够。
2、Redis的八种内存淘汰机制
为了避免Redis内存不够用,Redis用8种内存淘汰策略保护自己~
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据。
allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
3、不同的业务场景,Redis选择适合的数据结构**
排行榜适合用zset
缓存用户信息一般用hash
消息队列,文章列表适用用list
用户标签、社交需求一般用set
计数器、分布式锁等一般用String类型
twemproxy explore
twemproxy,也叫nutcraker。是一个twtter开源的一个redis和memcache代理服务器。 redis作为一个高效的缓存服务器,非常具有应用价值。但是当使用比较多的时候,就希望可以通过某种方式 统一进行管理。避免每个应用每个客户端管理连接的松散性。同时在一定程度上变得可以控制。 搜索了不少的开源代理项目,知乎实现的python分片客户端。node的代理中间层,还有各种restfull的开源代理。
-
RedBrige
- C + epoll实现的一个小的webserver
- redis自身执行lua脚本的功能来执行redis命令
- 访问时在url中带上lua脚本文件的名字,直接调用执行该lua脚本
- 本质是通过http协议远程执行lua脚本
-
Webdis
- libevent, http-parser...实现的小型web服务器
- C 语言实现,通过unix-socket,TCP调用redis命令。
- 访问方法:
/cmd/key/arg0,arg1,...
实质是对redis命令的简单变换
-
redis-proxy
- 使用node写的redis代理层。
- 支持主从节点的失败处理(可以仔细研究)
- 测试后发现性能为原生的1/3
-
twemproxy
-
支持失败节点自动删除
- 可以设置重新连接该节点的时间
- 可以设置连接多少次之后删除该节点
- 该方式适合作为cache存储
-
支持设置HashTag
- 通过HashTag可以自己设定将两个KEYhash到同一个实例上去。
-
减少与redis的直接连接数
- 保持与redis的长连接
- 可设置代理与后台每个redis连接的数目
-
自动分片到后端多个redis实例上
- 多种hash算法(部分还没有研究明白)
- 可以设置后端实例的权重
-
避免单点问题
- 可以平行部署多个代理层.client自动选择可用的一个
-
支持redis pipelining request
-
支持状态监控
- 可设置状态监控ip和端口,访问ip和端口可以得到一个json格式的状态信息串
- 可设置监控信息刷新间隔时间
-
高吞吐量
- 连接复用,内存复用。
- 将多个连接请求,组成reids pipelining统一向redis请求。
-
Redis 的 Java 客户端
Redis 官方推荐的 Java 客户端有Jedis、lettuce 和 Redisson。
概况对比
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。而Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。
共同点:都提供了基于 Redis 操作的 Java API,只是封装程度,具体实现稍有不同。
不同点:
-
1.1、Jedis
是 Redis 的 Java 实现的客户端。支持基本的数据类型如:String、Hash、List、Set、Sorted Set。
特点:使用阻塞的 I/O,方法调用同步,程序流需要等到 socket 处理完 I/O 才能执行,不支持异步操作。Jedis 客户端实例不是线程安全的,需要通过连接池来使用 Jedis。
-
1.1、Redisson
优点点:分布式锁,分布式集合,可通过 Redis 支持延迟队列。
-
1.3、 Lettuce
用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
基于 Netty 框架的事件驱动的通信层,其方法调用是异步的。Lettuce 的 API 是线程安全的,所以可以操作单个 Lettuce 连接来完成各种操作。
1. Jedis
Jedis 是老牌的 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持,其官方网址是:http://tool.oschina.net/uploads/apidocs/redis/clients/jedis/Jedis.html。
优点:
- 支持全面的 Redis 操作特性(可以理解为API比较全面)。
缺点:
- 使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步;
- Jedis 客户端实例不是线程安全的,所以需要通过连接池来使用 Jedis。
2. lettuce
lettuce ([ˈletɪs]),是一种可扩展的线程安全的 Redis 客户端,支持异步模式。如果避免阻塞和事务操作,如BLPOP和MULTI/EXEC,多个线程就可以共享一个连接。lettuce 底层基于 Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。lettuce 的官网地址是:https://lettuce.io/
优点:
- 支持同步异步通信模式;
- Lettuce 的 API 是线程安全的,如果不是执行阻塞和事务操作,如BLPOP和MULTI/EXEC,多个线程就可以共享一个连接。
3. Redisson
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。其中包括( BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson 提供了使用Redis 的最简单和最便捷的方法。Redisson 的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。Redisson的官方网址是:https://redisson.org/
优点:
- 使用者对 Redis 的关注分离,可以类比 Spring 框架,这些框架搭建了应用程序的基础框架和功能,提升开发效率,让开发者有更多的时间来关注业务逻辑;
- 提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列等。
缺点:
- Redisson 对字符串的操作支持比较差。
4. 使用建议
结论:lettuce + Redisson
---------------------------
- 本地缓存:不需要序列化,速度快,缓存的数量与大小受限于本机内存
- 分布式缓存:需要序列化,速度相较于本地缓存较慢,但是理论上缓存的数量与大小无限(因为缓存机器可以不断扩展)
2、本地缓存
- Google guava cache:当下最好用的本地缓存
- Ehcache:spring默认集成的一个缓存,以spring cache的底层缓存实现类形式去操作缓存的话,非常方便,但是欠缺灵活,如果想要灵活使用,还是要单独使用Ehcache
- Oscache:最经典简单的页面缓存
3、分布式缓存
- memcached:分布式缓存的标配
- Redis:新一代的分布式缓存,有替代memcached的趋势
3.1、memcached
- 经典的一致性hash算法
- 基于slab的内存模型有效防止内存碎片的产生(但同时也需要估计好启动参数,否则会浪费很多的内存)
- 集群中机器之间互不通信(相较于Jboss cache等集群中机器之间的相互通信的缓存,速度更快<--因为少了同步更新缓存的开销,且更适合于大型分布式系统中使用)
- 使用方便(这一点是相较于Redis在构建客户端的时候而言的,尽管redis的使用也不困难)
- 很专一(专做缓存,这一点也是相较于Redis而言的)
3.2、Redis
- 可以存储复杂的数据结构(5种)
- strings-->即简单的key-value,就是memcached可以存储的唯一的一种形式,接下来的四种是memcached不能直接存储的四种格式(当然理论上可以先将下面的一些数据结构中的东西封装成对象,然后存入memcached,但是不推荐将大对象存入memcached,因为memcached的单一value的最大存储为1M,可能即使采用了压缩算法也不够,即使够,可能存取的效率也不高,而redis的value最大为1G)
- hashs-->看做hashTable
- lists-->看做LinkedList
- sets-->看做hashSet,事实上底层是一个hashTable
- sorted sets-->底层是一个skipList
- 有两种方式可以对缓存数据进行持久化
- RDB
- AOF
- 事件调度
- 发布订阅等
4、集成缓存
专指spring cache,spring cache自己继承了ehcache作为了缓存的实现类,我们也可以使用guava cache、memcached、redis自己来实现spring cache的底层。当然,spring cache可以根据实现类来将缓存存在本地还是存在远程机器上。
5、页面缓存
在使用jsp的时候,我们会将一些复杂的页面使用Oscache进行页面缓存,使用非常简单,就是几个标签的事儿;但是,现在一般的企业,前台都会使用velocity、freemaker这两种模板引擎,本身速度就已经很快了,页面缓存使用的也就很少了。
总结:
- 在实际生产中,我们通常会使用guava cache做本地缓存+redis做分布式缓存+spring cache就集成缓存(底层使用redis来实现)的形式
- guava cache使用在更快的获取缓存数据,同时缓存的数据量并不大的情况
- spring cache集成缓存是为了简单便捷的去使用缓存(以注解的方式即可),使用redis做其实现类是为了可以存更多的数据在机器上
- redis缓存单独使用是为了弥补spring cache集成缓存的不灵活
- 就我个人而言,如果需要使用分布式缓存,那么首先redis是必选的,因为在实际开发中,我们会缓存各种各样的数据类型,在使用了redis的同时,memcached就完全可以舍弃了,但是现在还有很多公司在同时使用memcached和redis两种缓存。
在本系列接下来的介绍中,会介绍在分布式情况下guava cache、memcached、redis、spring cache的使用与原理。
------------------------------------
缓存的分类
CDN 缓存
CDN(Content Delivery Network 内容分发网络)的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中。
在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
应用场景:主要缓存静态资源,例如图片,视频。
反向代理缓存
反向代理位于应用服务器机房,处理所有对 Web 服务器的请求。
如果用户请求的页面在代理服务器上有缓冲的话,代理服务器直接将缓冲内容发送给用户。
如果没有缓冲则先向 Web 服务器发出请求,取回数据,本地缓存后再发送给用户。通过降低向 Web 服务器的请求数,从而降低了 Web 服务器的负载。
应用场景:一般只缓存体积较小静态文件资源,如 css、js、图片。
本地应用缓存
指的是在应用中的缓存组件,其最大的优点是应用和 Cache 是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等。
在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适。
同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
应用场景:缓存字典等常用数据。
Ehcache
基本介绍:Ehcache 是一种基于标准的开源缓存,可提高性能,卸载数据库并简化可伸缩性。
它是使用最广泛的基于 Java 的缓存,因为它功能强大,经过验证,功能齐全,并与其他流行的库和框架集成。
Ehcache 可以从进程内缓存扩展到使用 TB 级缓存的混合进程内/进程外部署。
Ehcache是一种广泛使用的开源Java分布式缓存。
主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
Spring 提供了对缓存功能的抽象:即允许绑定不同的缓存解决方案(如Ehcache),但本身不直接提供缓存功能的实现。它支持注解方式使用缓存,非常方便。
存数据过期策略
Ehcache 过期数据淘汰机制:即懒淘汰机制,每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做 TTL 比较来判断是否过期。
Guava Cache
基本介绍:Guava Cache 是 Google 开源的 Java 重用工具集库 Guava 里的一款缓存工具。
- 当下最常用最简单的本地缓存
- 线程安全的本地缓存
- 类似于ConcurrentHashMap(或者说成就是一个ConcurrentHashMap,只是在其上多添加了一些功能)
分布式缓存
memcached
Memcached 是一个高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的 Hash 表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。
简单的说就是将数据调用到内存中,然后从内存中读取,从而大大提高读取速度。
- 协议简单(文本协议、二进制协议)
- 基于libevent的事件处理,libevent封装了Linux的epoll模型的时间处理功能。
- slab存储模型
- 集群中服务器间互不通信(在大集群的情况下,其性能远超其他同步更新缓存的缓存器,当然小集群下,memcached的性能也十分优秀)
缓存数据过期策略:LRU(最近最少使用)到期失效策略,在 Memcached 内存储数据项时,可以指定它在缓存的失效时间,默认为永久。
当 Memcached 服务器用完分配的内存时,失效的数据被首先替换,然后是最近未使用的数据。
数据淘汰内部实现:懒淘汰机制为每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做 TTL 比较来判断是否过期。
分布式集群实现:服务端并没有 “ 分布式 ” 功能。每个服务器都是完全独立和隔离的服务。 Memcached 的分布式,是由客户端程序实现的。
Redis
Redis 是一个远程内存数据库(非关系型数据库),性能强劲,具有复制特性以及解决问题而生的独一无二的数据模型。
它可以存储键值对与 5 种不同类型的值之间的映射,可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能。
Redis 还可以使用客户端分片来扩展写性能,内置了 复制(replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的磁盘持久化(persistence)。
并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。
主要的特性
- 快速
- 简单
- 多种缓存策略
- 缓存数据有两级:内存和磁盘,因此无需担心容量问题
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可以通过RMI、可插入API等方式进行分布式缓存
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理器实例,以及一个实例的多个缓存区域
- 提供Hibernate的缓存实现
- 可以单独使用,一般在第三方库中被用到的比较多(如mybatis、shiro等)ehcache 对分布式支持不够好,多个节点不能同步,通常和redis一块使用
ehcache 和 redis 比较
ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
redis是通过socket访问到缓存服务,效率比Ehcache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。
如果是单个应用或者对缓存访问要求很高的应用,用ehcache。
如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。
ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但是涉及到缓存恢复,大数据缓存,则不合适。
Redis 与 Memcached 的比较如下图:
Redis和Memcache区别,优缺点对比
1、 Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等。
2、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
3、虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value 交换到磁盘
4、过期策略–memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定,例如expire name 10
5、分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从
6、存储数据安全–memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化)
7、灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复
8、Redis支持数据的备份,即master-slave模式的数据备份。
关于redis和memcache的不同,下面罗列了一些相关说法,供记录:
redis和memecache的不同在于[2]:
1、存储方式:
memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小
redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(笔者注:有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。
2、数据支持类型:
redis在数据支持上要比memecache多的多。
3、使用底层模型不同:
新版本的redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、运行环境不同:
redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统 环境上的优化,虽然后来微软有一个小组为其写了补丁。
Redis优势
下面对 Redis 的优势进行了简单总结:
- 性能极高:Redis 基于内存实现数据存储,它的读取速度是 110000次/s,写速度是 81000次/s;
- 多用途工具: Redis 有很多的用途,比如可以用作缓存、消息队列、搭建 Redis 集群等;
- 命令提示功能:Redis 客户端拥有强大的命令提示功能,使用起来非常的方便,降低了学习门槛;
- 可移植性:Redis 使用用标准 C语言编写的,能够在大多数操作系统上运行,比如 Linux,Mac,Solaris 等。
Redis应用场景
Redis 用来缓存一些经常被访问的热点数据、或者需要耗费大量资源的内容,通过把这些内容放到 Redis 中,可以让应用程序快速地读取它们。例如,网站的首页需要经常被访问,并且在创建首页的过程中会消耗的较多的资源,此时就可以使用 Redis 将整个首页缓存起来,从而降低网站的压力,减少页面访问的延迟时间。
我们知道,数据库的存储方式大体可分为两大类,基于磁盘存储和基于内存存储。磁盘存储的数据库,因为磁头机械运动以及系统调用等因素导致读写效率较低。Redis 基于内存来实现数据存取,相对于磁盘来说,其读写速度要高出好几个数量级。下表将 Redis 数据库与其他常用数据库做了简单对比:
名称 | 类型 | 数据存储选项 | 附加功能 |
---|---|---|---|
Redis | 基于内存存储的键值非关系型数据库 | 字符串、列表、散列、有序集合、无序集合 | 发布与订阅、主从复制、持久化存储等 |
Memcached | 基于内存存储的键值缓存型数据库 | 键值之间的映射 | 为提升性能构建了多线程服务器 |
MySQL | 基于磁盘的关系型数据库 | 每个数据库可以包含多个表,每个表可以包含多条记录; 支持第三方扩展。 |
支持 ACID 性质、主从复制和主主复制 |
MongoDB | 基于磁盘存储的非关系文档型数据库 | 每个数据库可以包含多个集合,每个集合可以插入多个文档 | 支持聚合操作、主从复制、分片和空间索引 |
memcached-键值存储
redis-键值存储
RocksDB-键值存储
KeyDB-键值存储
DynamoDB-键值存储
levelDB-键值存储
etcd-键值存储
一、redis与memcached对比
1、数据类型
redis数据类型丰富,⽀持set+liset等类型
memcache⽀持简单数据类型,需要客户端⾃⼰处理复杂对象
2、持久性
redis⽀持数据落地持久化存储
memcache不⽀持数据持久存储
3、分布式存储
redis⽀持master-slave复制模式
memcache可以使⽤⼀致性hash做分布式
4、value大小限制
memcache是⼀个内存缓存,key的长度⼩于250字符,单个item存储要小于1M,不适合虚拟机使用
5、数据一致性不同
redis使用的是单线程模式,保证顺序性
memcache需要使⽤cas保证数据⼀致性。CAS(Check+and+Set)是⼀个确保并发⼀致性的机制,属于“乐观锁”范畴;原理很简单:拿版本号,操作,对⽐版本号
如果一致九操作不一致就放弃操作
6、cpu的使用率
redis单线程模型只能使⽤⼀个cpu,可以开启多个redis进程
1.Redis中,并不是所有的数据都⼀直存储在内存中的,这是和Memcached相⽐⼀个最⼤的区别。
2.Redis不仅仅⽀持简单的k%2Fv类型的数据,同时还提供list,set,hash等数据结构的存储。
3.Redis⽀持数据的备份,即master-slave模式的数据备份。
4.Redis⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进⾏使⽤。
redis和memecache的不同在于:
存储方式
memecache+把数据全部存在内存之中,断电后会挂掉,数据不能超过内存⼤小
redis有部份存在硬盘上,这样能保证数据的持久性。
数据类型
redis比memecache类型多
使⽤底层模型不同:
新版本的redis直接⾃⼰构建了VM+机制,因为⼀般的系统调⽤系统函数的话,会浪费⼀定的时间去移动和请求。
运行环境不同
redis⽬前官⽅只⽀持LINUX+上去⾏,从⽽省去了对于其它系统的⽀持,
----------------
1、HBase、Redis、MongoDB、Couchbase、LevelDB主流 NoSQL 数据库的对比
HBase
HBash 的数据存储是基于列(ColumnFamily)的,且非常松散—— 不同于传统的关系型数据库(RDBMS),HBase 允许表下某行某列值为空时不做任何存储(也不占位),减少了空间占用也提高了读性能。
属于CP类型
优势
1. 存储容量大,一个表可以容纳上亿行,上百万列;
2. 可通过版本进行检索,能搜到所需的历史版本数据;
3. 负载高时,可通过简单的添加机器来实现水平切分扩展,跟Hadoop的无缝集成保障了其数据可靠性(HDFS)和海量数据分析的高性能(MapReduce);
4. 在第3点的基础上可有效避免单点故障的发生。
缺点
1. 基于Java语言实现及Hadoop架构意味着其API更适用于Java项目;
2. node开发环境下所需依赖项较多、配置麻烦(或不知如何配置,如持久化配置),缺乏文档;
3. 占用内存很大,且鉴于建立在为批量分析而优化的HDFS上,导致读取性能不高;
4. API相比其它 NoSql 的相对笨拙。
适用场景
1. bigtable类型的数据存储;
2. 对数据有版本查询需求;
3. 应对超大数据量要求扩展简单的需求。
---------------------------Redis 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。目前由VMware主持开发工作。
优势
1. 非常丰富的数据结构;
2. Redis提供了事务的功能,可以保证一串 命令的原子性,中间不会被任何操作打断;
3. 数据存在内存中,读写非常的高速,可以达到10w/s的频率。
缺点
1. Redis3.0后才出来官方的集群方案,但仍存在一些架构上的问题(出处);
2. 持久化功能体验不佳——通过快照方法实现的话,需要每隔一段时间将整个数据库的数据写到磁盘上,代价非常高;而aof方法只追踪变化的数据,类似于mysql的binlog方法,但追加log可能过大,同时所有操作均要重新执行一遍,恢复速度慢;
3. 由于是内存数据库,所以,单台机器,存储的数据量,跟机器本身的内存大小。虽然redis本身有key过期策略,但是还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
适用场景
适用于数据变化快且数据库大小可遇见(适合内存容量)的应用程序。
---------------------------
MongoDB
MongoDB 是一个高性能,开源,无模式的文档型数据库,开发语言是C++。它在许多场景下可用于替代传统的关系型数据库或键/值存储方式。
在 MongoDB 中,文档是对数据的抽象,它的表现形式就是我们常说的 BSON(Binary JSON )。
MongoDB 比较灵活,可以设置成 strong consistent (CP类型)或者 eventual consistent(AP类型)。
BSON 是一个轻量级的二进制数据格式。MongoDB 能够使用 BSON,并将 BSON 作为数据的存储存放在磁盘中。
BSON 是为效率而设计的,它只需要使用很少的空间,同时其编码和解码都是非常快速的。即使在最坏的情况下,BSON格式也比JSON格式再最好的情况下存储效率高。
优势
1. 强大的自动化 shading 功能(更多戳这里);
2. 全索引支持,查询非常高效;
3. 面向文档(BSON)存储,数据模式简单而强大。
4. 支持动态查询,查询指令也使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
5. 支持 javascript 表达式查询,可在服务器端执行任意的 javascript函数。
缺点
1. 单个文档大小限制为16M,32位系统上,不支持大于2.5G的数据;
2. 对内存要求比较大,至少要保证热数据(索引,数据及系统其它开销)都能装进内存;
3. 非事务机制,无法保证事件的原子性。
适用场景
1. 适用于实时的插入、更新与查询的需求,并具备应用程序实时数据存储所需的复制及高度伸缩性;
2. 非常适合文档化格式的存储及查询;
3. 高伸缩性的场景:MongoDB 非常适合由数十或者数百台服务器组成的数据库。
4. 对性能的关注超过对功能的要求。
NoSQL 数据库在以下的这几种情况下比较适用:
- 数据模型比较简单;
- 对灵活性要求很强的系统;
- 对数据库性能要求较高;
- 不需要高度的数据一致性;
- 对于给定 key,比较容易映射复杂值的环境。
MongoDB
市面上的数据库软件众多,我们为什么要选择 MongoDB 呢?换句话说就是 MongDB 有什么优势呢?下面列举了几点原因。
1) 面向文档
由于 MongoDB 是 NoSQL 类型的数据库,它不是像关系类型数据库那样以固定的格式存储数据,而是将数据存储在文档中,这使 MongoDB 非常灵活,可以适应实际的商业环境和需求;
2) 临时查询
MongoDB 支持按字段、范围和正则表达式查询并返回文档中的数据;
3) 索引
可以创建索引以提高 MongoDB 中的搜索性能,文档中的任何字段都可以建立索引;
4) 复制
MongoDB 支持高可用性的副本集。副本集由两个或多个 MongoDB 数据库实例组成,每个副本集成员可以随时充当主副本或辅助副本的角色,主副本是与客户端交互并执行所有读/写操作的主服务器。辅助副本使用内置复制维护主副本种的数据。当主副本失败时,副本集将自动切换到辅助副本,然后将辅助副本作为主服务器;
5) 负载平衡
MongoDB 可以在多台服务器上运行,以平衡负载或复制数据,以便在硬件出现故障时保持系统正常运行。
适用场景
MongoDB 的主要目标是在键/值存储方式和传统的 RDBMS(关系型数据库)系统之间架起一座桥梁,它集两者的优势于一身。根据官方网站的描述,MongoDB 适用于以下场景。
1) 网站数据
MongoDB 非常适合实时的插入、更新与查询数据,并具备网站实时存储数据所需的复制及高度伸缩的特性;
2) 缓存
由于性能很高,MongoDB 也适合作为信息基础设施的缓存层,在系统重启之后,由 MongoDB 搭建的持久化缓存层可以避免下层的数据源过载;
3) 庞大的、低价值的数据
使用传统的关系型数据库存取大量数据时,数据库的运行效率往往并不尽人意,而 MongoDB 的出现使这个问题迎刃而解,MongoDB 非常适合庞大数据的存储;
4) 高伸缩性的场景
MongoDB 内置了 MapReduce 引擎,因此非常适合由数十或数百台服务器组成的数据库;
5) 用于对象及 JSON 数据的存储
MongoDB 的 BSON 数据格式非常适合文档化格式的存储及查询。
---------------------------
Couchbase
本文之所以没有介绍 CouchDB 或 Membase,是因为它们合并了。合并之后的公司基于 Membase 与 CouchDB 开发了一款新产品,新产品的名字叫做 Couchbase。
Couchbase 跟 MongoDB 一样都是面向文档的数据库,不过在往 Couchbase 插入数据前,需要先建立 bucket —— 可以把它理解为“库”或“表”。
Couchbase 明显属于 CP 类型。
couchbase 的精髓就在于依赖内存最大化降低硬盘I/O对吞吐量的负面影响,所以其读写速度非常快,可以达到亚毫秒级的响应。
couchbase在对数据进行增删时会先体现在内存中,而不会立刻体现在硬盘上,从内存的修改到硬盘的修改这一步骤是由 couchbase 自动完成,等待执行的硬盘操作会以write queue的形式排队等待执行,也正是通过这个方法,硬盘的I/O效率在 write queue 满之前是不会影响 couchbase 的吞吐效率的。
优势
1. 高并发性,高灵活性,高拓展性,容错性好;
2. 以 vBucket 的概念实现更理想化的自动分片以及动态扩容(了解更多);
缺点
1. Couchbase 的存储方式为 Key/Value,但 Value 的类型很为单一,不支持数组。另外也不会自动创建doc id,需要为每一文档指定一个用于存储的 Document Indentifer;
2. 各种组件拼接而成,都是c++实现,导致复杂度过高,遇到奇怪的性能问题排查比较困难,(中文)文档比较欠缺;
3. 采用缓存全部key的策略,需要大量内存。节点宕机时 failover 过程有不可用时间,并且有部分数据丢失的可能,在高负载系统上有假死现象;
4. 逐渐倾向于闭源,社区版本(免费,但不提供官方维护升级)和商业版本之间差距比较大。
适用场景
1. 适合对读写速度要求较高,但服务器负荷和内存花销可遇见的需求;
2. 需要支持 memcached 协议的需求。
---------------------------
LevelDB
LevelDB 是由谷歌重量级工程师(Jeff Dean 和 Sanjay Ghemawat)开发的开源项目,它是能处理十亿级别规模 key-value 型数据持久性存储的程序库,开发语言是C++。
除了持久性存储,LevelDB 还有一个特点是 —— 写性能远高于读性能(当然读性能也不差)。
优势
1. 操作接口简单,基本操作包括写记录,读记录和删除记录,也支持针对多条操作的原子批量操作;
2. 写入性能远强于读取性能,
3. 数据量增大后,读写性能下降趋平缓。
缺点
1. 随机读性能一般;
2. 对分布式事务的支持还不成熟。而且机器资源浪费率高。
适应场景
适用于对写入需求远大于读取需求的场景(大部分场景其实都是这样)。
------------------------------
------------------------------
参考:
https://zhuanlan.zhihu.com/p/364787132?utm_medium=social&utm_oi=799560701973762048
https://www.jianshu.com/p/4a6cc0c1c7f1
https://www.cnblogs.com/jiarui-zjb/p/12635496.html
https://baijiahao.baidu.com/s?id=1675905732497463340&wfr=spider&for=pc