文章目录
Redis将所有的数据都存放在内存中,所以读写速度很快。同时,Redis将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。
Redis典型的应用场景:缓存、排行榜(热门帖子的缓存)、计数器(某一个帖子的访问量)、社交网络(点赞,关注等)、消息队列等。
一、NoSQL数据库
1.NoSQL可以解决CPU及内存压力,还可以解决IO压力
- 在分布式系统中,怎么存session?方法1:存到cookie中,但是cookie是存到客户端的,不安全。方法2:进行session的复制,在每一个服务器中都存一个session,但是这样的话会浪费很多存储空间。方法3:存到nosql数据库中,这样既可以保证安全性,也能够节约服务器中的内存空间,因此nosql数据库可以缓解CPU和内存压力。
- 当数据库表很大的时候怎么提高查询速度呢?一种方法是,对数据库进行水平切分,垂直切分,读写分离等,但是这样会破坏一定的业务逻辑。而nosql可以直接作为缓存数据库,提高访问速度,减少IO的读操作,缓解IO压力。
2.NoSQL数据库是什么?有什么特点?
- NoSQL(Not Only SQL)泛指非关系型数据库,不依赖业务逻辑方式存储,采用 key-value 模式存储,大大的增加了数据库的扩展能力。
- 特点:不遵循SQL标准,不支持ACID,远超SQL的性能。易扩展,大数据量高性能,多样灵活。
- 适用场景:高并发,海量数据读写,对数据高可扩展性。典型场景:秒杀
- 不适用场景:需要事务支持,结构化查询存储
3.传统RDBMS vs NoSQL
- RDBMS
- 高度组织化结构化数据
- 结构化查询语言(SQL)
- 数据和关系都存储在单独的表中
- 数据操纵语言,数据定义语言
- 严格一致性
- 基础事务
- NoSQL
- 不仅仅是SQL
- 没有声明性查询语言
- 没有预定义的模式
- 键 - 值对存储,列存储,文档存储,图形数据库
- 最终一致性,而非ACID属性
4.分布式 vs 集群
- 分布式:不同的多台服务器上面部署不同的服务模块(工程),他们之间通过Rpc/Rmi之间通信和调用,对外提供服务和组内协作。
- 集群:不同的多台服务器上面部署相同的服务模块,通过分布式调度软件进行统一的调度,对外提供服务和访问。
二、Redis数据库概述
1. 什么是Redis?
- Redis是一个用c语言编写的开源的key-value分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库。支持多种数据类型,包括string(字符串),list(链表),set(集合),zset(有序集合)和hash(哈希)。Redis操作都是原子性的(有一个失败则都失败)。Redis支持各种方式的排序。Redis数据可以缓存在内存中,也可以周期性的写入到磁盘中(即持久化操作),在此基础上实现了主从(master-slave)同步。
2.Redis有什么特点?
- 支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载进行使用。
- 不仅仅支持key-value类型的数据,同时提供list,set,zset,hash等数据结构的存储。
- 支持数据备份,即master-slave模式的数据备份。
3.Redis应用场景?
- 可以配合关系型数据库做高速缓存,session共享
- 多样的数据结构存储持久化数(比如排行榜,时效性数据,计数器,秒杀等)
4.Redis安装
- MacOS可以用Homebrew直接安装Redis,参考链接:
- https://blog.csdn.net/realize_dream/article/details/106227622
- https://blog.csdn.net/zuoyixiao/article/details/105242134
5.启动Redis
redis-server redis.conf
启动redis服务端,使用不同的配置文件时,启动起来的redis不一样。redis-cli [-p 6379]
启动redis客户端,
6.Redis切换数据库命令
select [id]
id默认为0
7.基础知识和命令
- Redis默认有16个数据库,类似数组下标从0开始,初始默认使用0号库。
- 统一密码管理,16个库都是同样密码,要么都OK要么一个也连接不上。
- 默认端口是6379
select
命令切换数据库dbsize
查看当前数据库的key的数量flushdb
清空当前库flushall
清空全部库
8.Redis单线程+多路IO复用技术
- Redis6之后,采用多线程IO
- 使用I/O多路复用来监听多个socket链接客户端,这样就可以使用一个线程链接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作,而且单线程不会导致死锁问题的发生。
三、配置文件redis.conf
Redis的配置文件redis.config位于redis目录下,配置文件中内容大小写不敏感,且1k与1kb不一样。
config get 配置设置名
:获取配置文件中某选项的值。
config set 配置文件名
:设置配置文件中某选项的值。
- 默认情况下 bind = 127.0.0.1 只能接受本机的访问请求,不写的情况下,无限制接受任何ip地址的访问。生产环境肯定要写应用服务器的地址,服务器是需要远程访问的,所以需要将其注释掉。
- 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应。
- 设置密码:redis-cli命令进入客户端,config get requirepass查看密码,config set requirepass [密码]设置密码。
操作:
- 把bind 127.0.0.1 ::1注掉,支持远程访问,记得改回来。
- 把 pretected-mode yes改为no,即把保护模式改为no。
- 把daemonize no改为yes,后台启动。
- 把 # requirepass foobared注释去掉,设置密码。
四、Redis常用数据类型
1.key键操作
keys *
:查看当前库所有key(匹配:keys *1)exists key
:判断某个key是否存在type key
:查看你的key是什么类型del key
:删除指定的key数据unlink key
:根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。expire key [number]
:为key设置过期时间,时间以秒为单位,比如expire key 10表示十秒后过期。ttl key
:查看还有多少秒过期,-1表示永不过期,-2表示已过期
2.字符串(String)
字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。⚠️字符串最大长度为512M。
String的数据结构为动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
常用命令
set <key> <value>
:添加键值对get <key>
:查询对应键值append <key> <value>
:将给定的追加到原值的末尾strlen <key>
:获得值的长度setnx <key> <value>
:只有在key不存在时,设置key的值incr <key>
:将key中存储的数字值增1,只能对数字值操作,如果为空,新增值为1decr <key>
:将key中存储的数字值减1,只能对数字值操作,如果为空,新增值为-1incrby/decrby <key> <步长>
:将key中存储的数字值增减,自定义步长mset <key1> <value1> <key2> <value2>…
:同时设置一个或多个key-value对mget <key1> <key2> <key3>…
:同时获取一个或多个valuemsetnx <key1> <value1> <key2> <value2>…
:同时设置一个或多个key-value对,当且仅当所有给定key都不存在getrange <key> <起始位置> <结束位置>
:获得值的位置,类似Java中的substring,前包,后包setrange <key> <起始位置> <value>
:用覆写 所存储的字符串值,从<起始位置>开始(索引从0开始)setex <key> <过期时间> <value>
:设置键值的同时,设置过期时间,单位秒getset <key> <value>
:以新换旧,设置了新值同时获得旧值
3.列表(List)
单键多值,Redis列表是简单的字符串列表,按照插入顺序排序,底层实际是个双向链表。
常用命令
lpush/rpush <key> <value1> <value2> <value3>…
:从左边/右边插入一个或多个值。lpop/rpop <key>
:从左边/右边吐出一个值。值在键在,值光键亡。rpoplpush <key1> <key2>
:从列表右边吐出一个值,插到列表左边。lrange <key> <start> <stop>
:按照索引下标获得元素(从左到右),lrange key 0 -1表示取所有。lindex <key> <index>
:按照索引下标获得元素(从左往右)。llen <key>
:获得列表长度。linsert <key> before/after <value> <newvalue>
:在的前面/后面插入插入值。lrem <key> <n> <value>
:从左边删除n个value(从左到右)。lset <key> <index> <value>
:将列表key下标为index的值替换成value。
4.集合(Set)
set与list类似,也是一个列表的功能,特殊之处在于set是可以自动排重的。set是String类型的无序集合,它的底层其实是一个value为null的hash表,所以添加、删除和查找的复杂度都是O(1)。
set数据结构是dict字典,字典是用哈希表实现的。
常用命令
sadd <key> <value1> <value2>…
:将一个或多个member元素添加到集合key中,已经存在的member元素将被忽略。smembers <key>
:取出该集合的所有值。sismember <key> <value>
:判断集合是否含有该值,有1,没有0。scard <key>
:返回该集合的元素个数。srem <key> <value1> <value2>…
:删除集合中的某个元素。spop <key>
:随机从集合中吐出一个值。srandmember <key> <n>
:随机从该集合中取出n个值。不会从集合中删除。smove <source> <destination> value
:把集合中一个值从一个集合移动到另一个集合。sinter <key1> <key2>
:返回两个集合的交集元素。sunion <key1> <key2>
:返回两个集合的并集元素。sdiff <key1> <key2>
:返回两个集合的差集元素(key1中的,不包含key2中的)。
5.哈希(Hash)
hash是一个键值对集合,是一个string类型的field和value的映射表,hash表特别适合用于存储对象。类似于Java里面的Map<String,Object>。
hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
常用命令
hset <key> <field> <value>
:给集合中的键赋值hget <key> <field>
:从集合域中取出valuehmset <key1> <field1> <key2> <field2>…
:批量设置hash值hexists <key> <field>
:查看hash表key中,给定域field是否存在,存在1,不存在0hkeys <key>
:列出该hash集合的所有fieldhvals <key>
:列出该hash集合的所有valuehincrby <key> <field> <increment>
:为hash表key中的域field的值加上增量incrementhsetnx <key> <field> <value>
:将哈希表key中的域field的值设置为value,当且仅当域field不存在
6.有序集合(Zset)
zset与普通集合set非常相似,是一个没有重复元素的字符串集合。有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员时唯一的,但是评分可以是重复的。
zset底层使用了两个数据结构(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
常用命令
zadd <key> <score1> <value1> <score2> <value2>…
:将一个或多个member元素及其score值加入到有续集key当中。zrange <key> <start> <stop> [WITHSCORES]
:返回有序集合key中,下标在之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集。 zrangebyscore key min max [withscores] [limit offset count]
:返回有序集key中,score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。zrevrangebyscore key max min [withscores] [limit offset count]
:同上,改为从大到小排序。zincrby <key> <increment> <value>
:为元素的score加上增量。zrem <key> <value>
:删除该集合下,指定值的元素。zcount <key> <min> <max>
:统计该集合,分数区间内的元素个数。zrank <key> <value>
:返回该值在集合中的排名,从0开始。
五、Redis新数据类型
1.Bitmaps:可以实现对位的操作
bitmaps(位图)本身不是一种数据类型,实际上是字符串,但是可以对字符串的位进行操作。
bitmaps单独提供了一套命令,所以与string使用方法不太相同。可以把bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在bitmaps中叫做偏移量。
命令:
setbit <key> <offset> <value>
:设置bitmaps中某个偏移量的值(0或1)。⚠️注意:偏移量从0开始。getbit <key> <offset>
:获取bitmaps中某个偏移量的值,即某个key键的offset的值。bitcount <key> [start] [end]
:统计bitmaps中被设置为1的bit数。默认情况下,给定的bitmaps都会被进行计数,通过指定额外的start或end参数,可以让计数只在特定的位上进行。⚠️注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。bitop and(or/not/xor) <destkey> [key…]
:符合操作,可以做多个bitmaps的and(交集),or(并集),not(非),xor(异或)操作,并将结果保存在destkey中。
bitmaps与set对比:
- 当用bitmaps和set分别存储网站的活跃用户时,bitmap可以节省很多的内存空间。
- 但是如果网站每天独立访问用户很少(大量的僵尸用户),那么使用bitmaps就不太合适了。
2.HyperLogLog
HyperLogLog(超级日志),可以对数据统计,也可以对数据合并。采用一种基数算法,用于完成独立总数的统计。不精确的统计算法,标准误差为0.81%。
占据空间小,无论统计多少个数据,只占12K的内存空间,适用于输入元素的数量或者提及非常大时。但是,因为HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
命令:
pfadd <key> <element> [element …]
:添加指定元素到HyperLogLog中。操作成功返回1,操作失败返回0。pfcount <key> [key …]
:计算HyperLogLog的近似基数,可以计算多个。pfmerge <destkey> <sourcekey> [sourcekey …]
:将一个或多个HyperLogLog合并,并将合并后的结果存储在另一个HyperLogLog中。
3.Geospatial
GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
命令:
geoadd<key> <longitude> <latitude> <member> [longitude latitude member…]
:添加地理位置(经度,纬度,名称)。有效的经度从-180度到180度,有效的维度从-85.05112878度到85.05112878度,当坐标位置超出指定范围时,返回一个错误。geopos <key> <member> [member…]
:获得指定地区的坐标值。geodist <key> <member1> <member2> [m|km|ft|mi]
:获取两个位置之间的直线距离。单位:m表示米,[默认值];km表示千米;mi表示英里,ft表示英尺。georadius <key> <longitude> <latitude> radius m|km|ft|mi
:以给定的经纬度为中心,找出某一半径内的元素。参数为:经度 纬度 半径 单位。
六、Redis的发布和订阅
什么是发布和订阅?Redis的发布和订阅?发布订阅命令行实现?
- Redis发布订阅(pub/sub)是一种消息通信模式:发布者(pub)发送消息,订阅者(sub)接收消息。Redis客户端可以订阅任意数量的频道。
- 订阅命令
subscribe channel1 [channel2
…]
; - 发布命令
publish channel message
七、Spring Boot整合Redis
-
在pom.xml文件中引入redis相关依赖
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在application.properties文件中进行redis相关配置
#Redis服务器地址 spring.redis.host=127.0.0.1 #Redis服务器连接端口 spring.redis.port=6379 #Redis数据库索引(默认为0) spring.redis.database=0 #连接超时时间(毫秒) spring.redis.timeout=1800000 #连接池最大连接数(使用负值表示没有限制) spring.redis.lettuce.pool.max-active=20 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-wait=-1 #连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=5 #连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0
-
添加redis配置类,构造Spring Redis的核心组件RedisTemplate,这个组件是由Spring提供的
@Configuration public class RedisConfig { @Bean public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){ RedisTemplate<String,Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 设置key的序列化方式 template.setKeySerializer(RedisSerializer.string()); // 设置value的序列化方式 template.setValueSerializer(RedisSerializer.json()); // 设置hash的key的序列化方式 template.setHashKeySerializer(RedisSerializer.string()); // 设置hash的value的序列化方式 template.setHashValueSerializer(RedisSerializer.json()); template.afterPropertiesSet(); return template; } }
-
测试一下
@SpringBootTest class RedisSpringbootApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test public void testRedis(){ redisTemplate.opsForValue().set("name","Amy"); String name = (String) redisTemplate.opsForValue().get("name"); System.out.println(name); } }
补充:导入依赖时同时需要导入spring-boot-starter-web和spring-boot-starter-test,否则的话在运行测试函数的时候会报java.lang.IllegalStateException: Failed to load ApplicationContext
错误。为什么呢?
八、Redis事务
1.Redis事务定义
- Redis事务是一个单独的隔离操作:一次可以执行多个命令,本质是一组命令的集合,一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不允许加塞。
2.事务操作Multi、Exec、discard
- 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。
multi
:标记一个事务块的开始,在此之后的命令只是入队但没执行exec
:执行所有事务块内的命令,在此之前只是入队但没执行discard
:取消事务,放弃执行事务块内的所有命令watch key1 [key2
…]
:在执行multi之前,先执行 watch命令,可以监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断。unwatch
:取消watch命令对所有key的监视。如果在执行watch命令之后,exec命令或discard命令先被执行了的话,那么就不需要再执行unwatch了。
3.事务的错误处理
- 全体连坐:组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
- 冤头债主:如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
4.事务冲突的问题(乐观锁和悲观锁)
- 乐观锁(Optimistic Lock):每次去拿数据的时候都会认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,提交版本必须大于当前记录版本才能执行更新。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种 check-and-set机制实现事务的。
- 悲观锁(Pessimistic Lock):每次去拿数据的时候都会认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁。
5.Redis事务三特性
- *单独的隔离操作:*事务中的所有命令都会序列化,按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
- 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
九、持久化之RDB(Redis DataBase)
1.RDB简介
- 在指定的时间间隔内将内存中的数据集快照写入磁盘, 恢复时将快照文件直接读到内存里。
- Redis会单独创建(fork)一个子进程进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中主进程不进行任何IO操作,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后数据可能丢失。
2.fork
- fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一样,但是是一个全新的进程,并作为原进程的子进程。
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。“写时复制技术”。
3.RDB文件存储位置
- 在redis.config中配置RDB文件名称,默认为dump.rdb
dbfilename dump.rdb
- 在redis.config中配置RDB文件的保存路径,默认为Redis启动时命令行所在的目录下
dir ./
- 获取rdb文件目录的命令:
config get dir
4.如何触发RDB快照
-
配置文件中
snapshotting
快照配置 -
命令
save
:只管保存,其他不管,全部阻塞。bgsave
:在后台异步进行快照操作,快照同时还可以响应客户端请求。可以通过 lastsave 命令获取最后一次成功执行快照的时间。flushall
:也会产生dump.rdb文件,但里面是空的,无意义。
5.如何恢复rdb
- 关闭Redis
- 先把备份的文件拷贝到工作目录下
cp dump2.rdb dump.rdb
- 启动Redis,备份数据会直接加载
6.如何停止rdb
- 动态停止RDB:
redis-cli config set save ""
save后给空值,表示禁用保存策略
7.优劣
- 优势:
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
- 劣势:
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
8.小结
- RDB是一个非常紧凑的文件。
- RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。
- 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些。
- 数据丢失风险大。
- RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级不能响应客户端请求。
十、持久化之AOF(Append Of File)
1.AOF简介
- 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作)不记录,只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
- AOF默认不开启。
2.AOF持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区内;
- AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
- Redis服务重启时,会重写load加载AOF文件中的写操作达到数据恢复的目的。
3.AOF文件存储位置
- 可以在redis.config中配置AOF文件名称,默认为appendonly.aof
- AOF文件的保存路径同RDB的路径一致
4.AOF启动/修复/恢复
- AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要回复时再拷贝到Redis工作目录下,启动系统即加载。
- 正常恢复
- 修改默认的
applyonly no
,改为yes
- 将有数据的aof文件复制一份保存到对应目录(查看目录:
config get dir
) - 恢复:重启redis然后重新加载
- 修改默认的
- 异常恢复
- 修改默认的
applyonly no
,改为yes
- 如遇到aof文件损坏,通过
/usr/local/bin/redis-check-aof--fix appendonly.aof
进行恢复 - 备份被写坏的AOF文件
- 恢复:重启redis然后重新加载
- 修改默认的
5.AOF同步频率设置
- *appendfsync always:*始终同步,每次redis的写入都会立刻记入日志;性能较差但数据完整性比较好
- *appendfsync everysec:*每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失
- *appendfsync no:*redis不主动进行同步,把同步时机交给操作系统。
6.rewrite
- AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令
bgrewriteaof
进行重写 - 重写原理:AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中的数据,每条记录有一条的set语句。重写AOF文件的操作,并没有读取旧的AOF文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的AOF文件,这点和快照有些类似。
- 触发机制:Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64MB时触发。
- 重写流程:(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行;(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞;(3)子进程遍历redis内存中数据到临时文件,客户端的鞋请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失;(4)子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息;主进程把aof_rewrite_buf中的数据写入到新的AOF文件;(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
7.优劣
- 优势
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文件,通过操作AOF文件,可以处理误操作。
- 劣势
- 比起RDB占用更多的磁盘空间
- 恢复备份速度慢
- 每次读写都同步的话,有一定的性能压力
- 存在个别Bug,造成恢复存在一些问题
8.小结
- AOF文件是一个只进行追加的日志文件
- Redis可以再AOF文件体积变得过大时,自动地在后台对AOF进行重写
- AOF文件有序地保存了对数据路执行的所有写入操作,这些写入操作以redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松
- 对于相同的数据集来说,AOF文件的体积通常要大于RDB文件的体积
- 根据所使用的fsync策略,AOD的速度可能会慢于RDB
补充:
AOF和RDB同时开启,系统默认读取AOF的数据(数据不会存在丢失)
RDB与AOF用哪个好?官方推荐两个都启用。如果对数据不敏感,可以单独选用RDB。不建议单独用AOF,因为可能会出现Bug。如果只是做纯内存缓存,可以都不用。
十一、主从复制
1.简介
- 主从复制就是主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Salve以读为主。
- 读写分离,性能扩展,容灾快速恢复。
2.如何使用redis主从复制
- 拷贝多个redis.config文件,命名为[redis6379.conf、redis6380、redis6381…]
- include(写绝对路径)
- 开启daemonize yes
- Pid文件名字pidfile
- 指定端口port
- log文件名字
- dump.rdb名字dbfilename
- Appendonly 关掉或者换名字
- 启动多台redis服务器
- 示例:
redis-server redis6379.config
- 示例:
- 配从(库)不配主(库)
slaveof <ip> <port>
: 成为某个实例的从服务器- 在主机上写,在从机上可以读取数据,在从机上写数据报错
- 主机挂掉,重启就行,一切如初
- 从机重启需重设slaveof,从机每次与master断开,都需要重新连接,除非配置进redis.conf文件
info replication
: 打印主从复制的相关信息,即查看当前redis的属性,是从机还是主机。
- 常用3招
- 一主二仆:主机不配置,从机使用slaveof声明所属主机;主机可写可读,从机只可以读,不可以写;主机shutdown后,从机不会代替主机,还是从机;当主机重启后恢复到之前的状态,不需要再做其他任何修改,主机新增记录,从机可以读到数据,从机会自动与主机保持一致性;从机down后,再开机不会自动与主机连接,除非在配置文件中进行了相关配置。
- 薪火相传:上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为链条中下一个的master,可以有效减轻master的写压力,去中心化降低风险。中途变更转向会清楚之前的数据,重新拷贝最新的。风险是一旦某个slave宕机,后面的slave都没法备份。
- 反客为主:当一个master宕机后,从slave中选出一个作为主机,用
salveof no one
将从机变为主机。
3.复制原理
- Slave启动成功连接到master后会发送一个sync命令;
- Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步;
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中;
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步;
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。
4.哨兵模式(sentinel)
- 反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
- 调整为一主二仆模式,主:6379,仆:6380、6381
- 自定义的/myredis目录下新建sentinel.conf文件,名字绝不能错
- 配置哨兵,在sentinel.conf文件中填写内容
- sentinel monitor mymaster 127.0.0.1 6379 1
- 其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
- 启动哨兵
- 执行
redis-sentinel /myredis/sentinel.conf
- 执行
- 当主机挂掉,会根据优先级别:slave-priority,在从机中选举产生新的主机
- 原主机重启后会变成从机。
5.复制延迟
- 由于所有的写操作都是先在master上操作,然后同步更新到slave上,所以从master同步到slave机器会有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,slave及其数量的增加也会使这个问题更加严重。
十二、Redis应用问题及解决方案
1.缓存穿透
- 问题描述:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透问题。
- 例子:比如用一个不存在的用户id获取用户信息,不论是缓存还是数据库都没有该数据,如果黑客利用此漏洞进行攻击可能压垮数据库。
- 解决方案
- 非法请求限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在API入口处我们要判断请求参数是否合理,请求参数是否含有非法值,请求字段是否存在,如果判断出时恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 设置空值或者默认值:如果一个查询返回的数据为空(不管数据是否不存在),在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
- 采用布隆过滤器:使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:在写入数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询redis和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,redis自身也是支持布隆过滤器的。布隆过滤器实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数),可以用于检索一个元素是否在一个集合中。优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
2.缓存击穿
- 问题描述:key对应的数据在数据库中存在,但在redis中过期,此时若有大量并发请求,这鞋请求发现缓存过期就会直接访问数据库,从而导致数据库瞬间被压垮。
- 例子:热点数据缓存过期的瞬间,大量请求直接访问数据库,导致数据库崩掉。
- 解决方案:
- 不给热点数据设置过期时间,由后台异步更新缓存
- 在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。
- 互斥锁方案:(redis中使用setNX方法设置一个状态位,表明这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
3.缓存雪崩
- 问题描述:当大量缓存数据在同一时间过期时,此时如果有大量的用户请求,都无法在redis中处理,于是全部请求都直接访问数据库,从而导致数据库压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩问题。
- 解决方案:
- 将缓存失效时间随机打散:在原有的失效时间基础上增加一个随机值(比如1到10分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
- 设置缓存不过期:通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。
- 构建多级缓存架构:nginx缓存+redis缓存+其他缓存(ehcache等)
- 使用锁或队列:用加锁或队列的方式来保证不会有大量的线程对数据库一次性进行读写
4.分布式锁
- 问题描述:随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这使得原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
- 分布式锁的主流实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等):性能高
- 基于Zookeeper:可靠性高
- 分布式锁的实现要同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性。