文章目录
NoSQL与redis6
1、概述
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是 SQL”,泛指非关系型的数据库。 NoSQL 不依赖业务逻辑方式存储,而以简单的 key-value 模式存储。因此大大的增加了 数据库的扩展能力。
2、特点
- 不遵循SQL标准
- 不支持ACID.(ACID是值数据库管理系统(DBMS)在写入或者更新资料的过程中,为保证事务是正确可靠的,所必须具备的四个特性。)
- Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
- 远超SQL的性能
3、使用场景
- 适用场景
- 对数据高并发的读写
- 海量数据的读写
- 对数据高扩展性的读写
- 不适用场景
- 需要事务支持
- 基于SQL的结构化查询
4.常见Nosql
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r2rLzlVN-1640504514553)(_v_images/20211217093830711_16198.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8N7jUNog-1640504514562)(_v_images/20211217093938810_2448.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QH1QfixW-1640504514563)(_v_images/20211217094027221_14607.png)]
还有Hbase、Cassandr等数据库
5、Redis 介绍
- 默认16个数据库,类似数组下标从0开始,初始默认使用0号库
- Redis是单线程+多路IO复用技术
6、常用数据类型
6.1 Key
- keys *查看当前库所有 key (匹配:keys *1)
- exists key 判断某个 key 是否存在
- type key 查看你的 key 是什么类型
- del key 删除指定的 key 数据
- unlink key 根据 value 选择非阻塞删除 仅将 keys 从 keyspace 元数据中删除,真正的删除会在后续异步操作。
- expire key 10 10 秒钟:为给定的 key 设置过期时间
- ttl key 查看还有多少秒过期,-1 表示永不过期,-2 表示已过期
- select 命令切换数据库
- dbsize 查看当前数据库的 key 的数量
- flushdb 清空当前库
- flushall 通杀全部库
6.2 String
- String 是 Redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
- String 类型是二进制安全的。意味着 Redis 的 string 可以包含任何数据。比如 jpg 图片 或者序列化的对象。
- String 类型是 Redis 最基本的数据类型,一个 Redis 中字符串 value 最多可以是 512M
6.2.1 常用命令
- set 添加键值对
eg: > set key value [EX seconds | PX millisseconds | …]
*NX:当数据库中 key 不存在时,可以将 key-value 添加数据库
*XX:当数据库中 key 存在时,可以将 key-value 添加数据库,与 NX 参数互斥
*EX:key 的超时秒数
*PX:key 的超时毫秒数,与 EX 互斥 - get 查询对应键值
- append 将给定的 追加到原值的末尾
- strlen 获得值的长度
- setnx 只有在 key 不存在时 设置 key 的值
- incr 将 key 中储存的数字值增 1 只能对数字值操作,如果为空,新增值为 1
- decr 将 key 中储存的数字值减 1 只能对数字值操作,如果为空,新增值为-1
- incrby / decrby <步长>将 key 中储存的数字值增减。自定义步长。
- mset … 同时设置一个或多个 key-value 对
- mget … 同时获取一个或多个 value
- msetnx … 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 原子性,有一个失败则都失败
- getrange <起始位置><结束位置> 获得值的范围,类似 java 中的 substring,前包,后包
- setrange <起始位置> 用 覆写所储存的字符串值,从<起始位置>开始(索引从 0 开始)。
- setex <过期时间> 设置键值的同时,设置过期时间,单位秒。
- getset 以新换旧,设置了新值同时获得旧值
6.2.2 数据结构:
String 的数据结构为简单动态字符串(Simple Dynamic String,缩写 SDS)。是可以 修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式 来减少内存的频繁分配.内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次 只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
6.3 列表(List)
单键多值 Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头 部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
6.3.1 常用命令
- lpush/rpush … 从左边/右边插入一个或多个值。
- lpop/rpop 从左边/右边吐出一个值。值在键在,值光键亡。
- rpoplpush 从列表右边吐出一个值,插到列表左边。
- lrange
按照索引下标获得元素(从左到右) - lrange mylist 0 -1 0 左边第一个,-1 右边第一个,(0-1 表示获取所有)
- lindex 按照索引下标获得元素(从左到右)
- llen 获得列表长度
- linsert before 在的后面插入插入值
- lrem 从左边删除 n 个 value(从左到右)
- lset将列表 key 下标为 index 的值替换成 value
6.3.2 数据结构
List 的数据结构为快速链表 quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是 压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。 当数据量比较多的时候才会改成 quicklist。 因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只 是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next。 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指 针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
6.4 集合(Set)
Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动 排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选 择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所 不能提供的。 Redis 的 Set 是 string 类型的无序集合。它底层其实是一个 value 为 null 的 hash 表,所 以添加,删除,查找的复杂度都是 O(1)。 一个算法,随着数据的增加,执行时间的长短,如果是 O(1),数据增加,查找数据的 时间不变
6.4.1 常用命令
- sadd … 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
- smembers 取出该集合的所有值。
- sismember 判断集合是否为含有该值,有 1,没有 0 scard返回该集合的元素个数。
- srem … 删除集合中的某个元素。
- spop 随机从该集合中吐出一个值。
- srandmember 随机从该集合中取出 n 个值。不会从集合中删除 。
- smove value 把集合中一个值从一个集合移动到另一个集合
- sinter 返回两个集合的交集元素。
- sunion 返回两个集合的并集元素。
- sdiff 返回两个集合的差集元素(key1 中的,不包含 key2 中的)
6.4.2 数据结构
Set 数据结构是 dict 字典,字典是用哈希表实现的。 Java 中 HashSet 的内部实现使用的是 HashMap,只不过所有的 value 都指向同一个对象。 Redis 的 set 结构也是一样,它的内部也使用 hash 结构,所有的 value 都指向同一个内 部值。
6.5 哈希(hash)
Redis hash 是一个键值对集合。 Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。 类似 Java 里面的 Map<String,Object> 用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,如果用 普通的 key/value 结构来存储。
6.5.1 常用命令
- hset 给集合中的 键赋值
- hget 从集合取出 value
- hmset … 批量设置 hash 的值
- hexists查看哈希表 key 中,给定域 field 是否存在。
- hkeys 列出该 hash 集合的所有 field
- hvals 列出该 hash 集合的所有 value
- hincrby 为哈希表 key 中的域 field 的值加上增量 1 -1
- hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
6.5.2 数据结构
Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable。
6.6 有序集合(Zset)
Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用 来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分 可以是重复了 。 因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获 取一个范围的元素。 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成 员的智能列表。
6.6.1 常用命令
- zadd … 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
- zrange
[WITHSCORES] 返回有序集 key 中,下标在 之间的元素 带 WITHSCORES,可以让分数一起和值返回到结果集。 - zrangebyscore key minmax [withscores] [limit offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 有序集成员按 score 值递增(从小到大)次序排列。
- zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列。
- zincrby 为元素的 score 加上增量
- zrem 删除该集合下,指定值的元素
- zcount 统计该集合,分数区间内的元素个数
- zrank 返回该值在集合中的排名,从 0 开始
6.6.2 数据结构
SortedSet(zset)是 Redis 提供的一个非常特别的数据结构,一方面它等价于 Java 的数据结构 Map<String, Double>,可以给每一个元素 value 赋予一个权重 score,另 一方面它又类似于 TreeSet,内部的元素会按照权重 score 进行排序,可以得到每个元 素的名次,还可以通过 score 的范围来获取元素的列表。
zset 底层使用了两个数据结构:
(1)hash,hash 的作用就是关联元素 value 和权重 score,保障元素 value 的唯 一性,可以通过元素 value 找到相应的 score 值。
(2)跳跃表,跳跃表的目的在于给元素 value 排序,根据 score 的范围获取元素列表。
6.7 Bitmaps
- Bitmaps 本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
- Bitmaps 单独提供了一套命令, 所以在 Redis 中使用 Bitmaps 和使用字 符串的方法不太相同。 可以把 Bitmaps 想象成一个以位为单位的数组, 数组的每个单元只能存储 0 和 1, 数组的下标在 Bitmaps 中叫做偏移量。
6.7.1 常用命令
- setbit
格式:setbit设置 Bitmaps 中某个偏移量的值(0 或 1)
*offset:偏移量从 0 开始 - getbit
格式:getbit获取 Bitmaps 中某个偏移量的值
*获取键的第 offset 位的值(从 0 开始算) - bitcount
统计字符串被设置为 1 的 bit 数。
格式:bitcount[start end] 统计字符串从 start 字节到 end 字节比特值为 1 的数量
*start 和 end 代表起始和结束字节数 - bitop
格式: bitop and(or/not/xor) [key…]
*bitop 是一个复合操作, 它可以做多个 Bitmaps 的 and(交集) 、 or(并集) 、 not (非) 、 xor(异或) 操作并将结果保存在 destkey 中。
6.8 HyperLogLog
- Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输 入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小 的。
- 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接 近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成 鲜明对比。
- 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素 本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
6.9 Geospatial
Redis 3.2 中增加了对 GEO 类型的支持。GEO,Geographic,地理信息的缩写。 该类型,就是元素的 2 维坐标,在地图上就是经纬度。redis 基于该类型,提供了经纬 度设置,查询,范围查询,距离查询,经纬度 Hash 等常见操作。
7、配置文件
7.1 网络相关配置
- 默认情况 bind=127.0.0.1 只能接受本机的访问请求。不写的情况下,无限制接受任何 ip 地址的访问。
- protected-mode:如果开启了 protected-mode,那么在没有设定 bind ip 且没有设密码的情况下,Redis 只允许接受本机的响应。
- tcp-backlog:设置 tcp 的 backlog,backlog 其实是一个连接队列,backlog 队列总和=未完成三次握手 队列 + 已经完成三次握手队列。
- timeout:一个空闲的客户端维持多少秒会关闭,0 表示关闭该功能。即永不关闭。
- tcp-keepalive:对访问客户端的一种心跳检测,每个 n 秒检测一次。 单位为秒,如果设置为 0,则不会进行 Keepalive 检测,建议设置成 60。
- daemonize:是否为后台进程,设置为 yes
- loglevel:指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默 认为 notice
- logfile:日志文件名称
- maxclients:设置 redis 同时可以与多少个客户端进行连接,默认情况下为 10000 个客户端。如果达到了此限制,redis 则会拒绝新的连接请求,并且向这些连接请求方发出 “max number of clients reached”以作回应。
- maxmemory:建议必须设置,否则,将内存占满,造成服务器宕机;设置 redis 可以使用的内存量。一旦到达内存使用上限,redis 将会试图移除内部数 据,移除规则可以通过 maxmemory-policy 来指定。
- maxmemory-policy:
- volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键;(最近最少使用)
- allkeys-lru:在所有集合 key 中,使用 LRU 算法移除 key
- volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键
- allkeys-random:在所有集合 key 中,移除随机的 key
- volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key
- noeviction:不进行移除。针对写操作,只是返回错误信息 - maxmemory-samples: 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所 以你可以设置样本的大小,redis 默认会检查这么多个 key 并选择其中 LRU 的那个。
8、Redis 的发布与订阅
。。。
9、Redis 与Springboot整合示例
1、在 pom.xml 文件中引入 redis 相关依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X 集成 redis 所需 common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
2、application.properties 配置 redis 配置
#Redis 服务器地址
spring.redis.host=192.168.140.136
#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
3、添加 redis 配置类
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key 序列化方式
template.setKeySerializer(redisSerializer);
//value 序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap 序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间 600 秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair. fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
4、测试
RedisTestController 中添加测试方法
@RestController
@RequestMapping("/redisTest") public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping public String testRedis() {
//设置值到 redis
redisTemplate.opsForValue().set("name","lucy");
//从 redis 获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
10、Redis事务
Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
10.1 Multi、Exec、discard
从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行。
组队的过程中可以通过 discard 来放弃组队。
10.2 事务的错误处理
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都 会执行,不会回滚。
10.3 WATCH 和UNWATCH
- 在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务 执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
- unwatch取消 WATCH 命令对所有 key 的监视。 如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就 不需要再执行 UNWATCH 了。
Redis事务三特性
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会 被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都 不会被实际执行
- 不保证原子性
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
11、Redis持久化
11.1 RDB(Redis DataBase)
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的 Snapshot 快 照,它恢复时是将快照文件直接读到内存里.
11.1.1 如何备份
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件 中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程 中,主进程是不进行任何 IO 操作的,这就确保了极高的性能 如果需要进行大规模数 据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加 的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。
11.1.2 Fork
- Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、 程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进 程
- 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多 会 exec 系统调用,出于效率考虑,Linux 中引入了“写时复制技术”
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要 发生变化时,才会将父进程的内容复制一份给子进程。
11.1.3 dump.rdb
文件名:在 redis.conf 中配置文件名称,默认为 dump.rdb : 配置dbfilename dump.rdb
目录名:rdb 文件的保存路径,也可以修改。默认为 Redis 启动时命令行所在的目录下。配置:dir “/myredis/”
11.1.4 如何触发RDB快照
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bqgWJAZo-1640504514566)(_v_images/20211217160739075_13864.png)]
- save :save 时只管保存,其它不管,全部阻塞。手动保存。不建议。 bgsave:Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
- 可以通过 lastsave 命令获取最后一次成功执行快照的时间
- 执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义
- 当Redis无法写入磁盘的话,直接关掉Redis的写操作。配置:stop-writes-on-bgsave-error yes(推荐)
11.1.5 rdbcompression 压缩文件
对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用 LZF 算法进行压缩。 如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。
配置:rdbcompression yes(推荐)
11.1.6 rdbchecksum 检查完整性
在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验, 但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关 闭此功能
配置: rdbchecksum yes(推荐)
11.1.7 rdb的备份
先通过 config get dir 查询 rdb 文件的目录
将*.rdb 的文件拷贝到别的地方 rdb 的恢复
- 关闭 Redis
- 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
- 启动 Redis, 备份数据会直接加载
11.1.8 优劣势
- 优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
- 劣势
- Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑
- 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话, 就会丢失最后一次快照后的所有修改。
11.1.9 动态停止RDB
动态停止 RDB:redis-cli config set save “”#save 后给空值,表示禁用保存策略
11.2 AOF(Append Only File)
以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下 来(读操作不记录), 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重 新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一 次以完成数据的恢复工作.
11.2.1 AOF 持久化流程
- 户端的请求写命令会被 append 追加到 AOF 缓冲区内;
- AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的 AOF 文件中;
- AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量;
- Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的;
11.2.2 AOF 默认不开启
可以在 redis.conf 中配置文件名称,默认为 appendonly.aof AOF 文件的保存路径,同 RDB 的路径一致。
AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失)
11.2.3 AOF 启动/修复/恢复
- AOF 的备份机制和性能虽然和 RDB 不同, 但是备份和恢复的操作同 RDB 一样,都 是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载。
- 正常恢复
- 修改默认的 appendonly no,改为 yes
- 将有数据的 aof 文件复制一份保存到对应目录(查看目录:config get dir)
- 恢复:重启 redis 然后重新加载
- 异常恢复
- 修改默认的 appendonly no,改为 yes
- 如遇到 AOF 文件损坏,通过/usr/local/bin/redis-check-aof–fix appendonly.aof 进行恢复
- 备份被写坏的 AOF 文件
- 恢复:重启 redis,然后重新加载
11.2.4 AOF 同步频率设置
- appendfsync always 始终同步,每次 Redis 的写入都会立刻记入日志;
- 性能较差但数据完整性比较好
- appendfsync everysec
- 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
- appendfsync no
- redis 不主动进行同步,把同步时机交给操作系统。
11.2.5 优势/劣势
优势:
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
劣势: - 比起 RDB 占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别 Bug,造成恢复不能。
AOF总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dBi62Grk-1640504514568)(_v_images/20211220092833065_14347.png)]
11.3 使用
- 官方推荐两个都启用。
- 如果对数据不敏感,可以选单独用 RDB。
- 不建议单独用 AOF,因为可能会出现 Bug。
- 如果只是做纯内存缓存,可以都不用。
建议: - RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储
- AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些 命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾.
- Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大
- 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何 持久化方式.
- 同时开启两种持久化方式
- 在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据, 因为在 通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整.
- RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只 使用 AOF 呢?
- 建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份), 快速重 启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段。
- 性能建议 因为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要 15 分 钟备份一次就够了,只保留 save 900 1 这条规则。
- 如果使用 AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单 只 load 自己的 AOF 文件就可以了。 代价,一是带来了持续的 IO,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据 写到新文件造成的阻塞几乎是不可避免的。 只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M 太小了,可以设到 5G 以上。 默认超过原大小 100%大小时重写可以改到适当的数值。
12、Redis主从复制
12.1 WHAT
主机数据更新后根据配置和策略, 自动同步到备机的 master/slaver 机制,Master 以 写为主,Slave 以读为主
12.2 WHAT TO DO
读写分离、性能扩展、容灾快速恢复
12.3 HOW TO DO
- 启动多台redis服务器
- 配置从库 slaveof --> 主机挂掉,重启就行,从机如果挂掉需要重新设置(slaveof )
- 也可将配置加到文件中,永久生效。
12.4 常用三招
12.4.1 一主二仆
- 实现读写分离、主机挂掉,重启就行,从机如果挂掉需要重新设置(slaveof )
12.4.2 薪火相传
- 上一个 Slave 可以是下一个 slave 的 Master,Slave 同样可以接收其他 slaves 的连接和同
步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去
中心化降低风险。 - 用 slaveof
- 中途变更转向:会清除之前的数据,重新建立拷贝最新的
风险是一旦某个 slave 宕机,后面的 slave 都没法备份
主机挂了,从机还是从机,无法写数据了
12.4.3 反客为主
当一个 master 宕机后,后面的 slave 可以立刻升为 master,其后面的 slave 不用做任何
修改。手动用 slaveof no one 将从机变为主机。
12.4.4 哨兵模式
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库
转换为主库。
12.4.5 复制原理
- Slave 启动成功连接到 master 后会发送一个 sync 命令
- Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步
- 全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步
- 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行
13 redis集群
13.1 redis集群解决的问题
- 容量不够,扩容
- 并发写操作, redis 分摊压力
- 主从模式,薪火相传模式,主机宕机,导致 ip 地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息,之前通过代理主机解决,redis3.0可以使用集群解决,无中心化集群配置。
13.2 集群
- Redis 集群实现了对 Redis 的水平扩容,即启动 N 个 redis 节点,将整个数据库分布存储在这 N 个节点中,每个节点存储总数据的 1/N。
- Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
13.3 slot
- 一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384个插槽的其中一个,集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
- 集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点,其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B 负责处理 5461 号至 10922 号插槽。
节点 C 负责处理 10923 号至 16383 号插槽。
13.4 故障恢复
- 如果主节点下线,从节点自动升为主节点。(15秒超时),主节点恢复后,会变为从节点。
- 如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 yes ,那么 ,整个集群都挂掉。
- 如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 no ,那么,该插槽数据全都不能使用,也无法存储。
13.5 集群脑裂
集群脑裂,好比一个人有两个大脑,那么到底受谁来控制呢?在分布式集群中,分布式协作框架zookeeper很好的解决了这个问题,通过控制半数以上的机器来解决。redis中哨兵集群集群监控着集群,由于某种原因,比如网络原因,集群出现了分区,master与slave节点之间断开了联系,sentinel监控到一段时间没有联系认为master故障,然后重新选举,将slave切换为新的master。但是master可能并没有发生故障,只是网络产生分区,此时client任然在旧的master上写数据,而新的master中没有数据,如果不及时发现问题进行处理可能旧的master中堆积大量数据。在发现问题之后,旧的master降为slave同步新的master数据,那么之前的数据被刷新掉,大量数据丢失。
14、Redis使用问题解决
14.1 缓存穿透
- 问题描述
key 对应的数据在数据源并不存在,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。 - 解决方法:
- 对空值缓存
- 设置可以访问的白名单
- 进行实时监控,如果redis命中率急速下降,排查访问对象。
14.2 缓存击穿
- 问题描述
key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。 - 解决方法
key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决问题:
(1)预先设置热门数据:在 redis 高峰访问之前,把一些热门数据提前存入到redis 里面,加大这些热门数据 key 的时长
(2)实时调整:现场监控哪些数据热门,实时调整 key 的过期时长
(3)使用锁:- 就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db。
- 先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX)去 set 一个 mutex key
- 当操作返回成功时,再进行 load db 的操作,并回设缓存,最后删除 mutex key;
- 当操作返回失败,证明有线程在 load db,当前线程睡眠一段时间再重试整个 get 缓存的方法。
14.3 缓存雪崩
- 问题描述
key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key - 解决方法
- 构建多级缓存架构:nginx 缓存 + redis 缓存 +其他缓存(ehcache 等)
- 使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
- 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。
- 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
14.4 分布式锁
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis 等)
- 基于 Zookeeper
每一种分布式锁解决方案都有各自的优缺点: - 性能:redis 最高
- 可靠性:zookeeper 最高
基于Redis实现分布式锁,是利用了setnx
需要注意的点: - 互斥性:在任意时刻,只有一个客户端能持有锁
- 不会发生死锁,即使一个客户端在持有锁期间崩溃而没有主动释放锁,也能保证后续其他客户端能加锁。
- 加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给解了
- 加锁和解锁必须具有原子性
@GetMapping("testLock")
public void testLock(){
//1、申明一个uuid,将作为一个value放入我们的key所对应的值中
String uuid = UUID.randomUUID.toString();
//2、定义一个锁:lua脚本可以使用同一把锁来实现删除
String skuId = "25";
String locKey = "lock:"+skuId;
//3、 获取锁,保证获取锁是原子操作
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey , uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}