Redis是什么?
NoSQL是什么?
在介绍Redis之前,我们先简单聊一下什么是NoSQL,NoSQL是基于内存的数据库,特指非关系型数据库,具备高并发、高可用、大数据存储等特点。 目前主流的NoSQL有如下:
- 键值对存储数据库 - redis
- 文档型数据库 - MongoDB
- 列存储数据库 - Hbase
- 图形数据库
Redis概述
Redis是其中一种非关系型数据库(NoSQL),作为高性能的key-value数据库,支持每秒十几万的读/写操作,其性能超数据库,并且支持集群、分布式、主从同步等配置,还支持一定事务能力。
常用场景
- 分布式锁
- 热点数据缓存
- 消息队列
- 发布订阅功能
- 分布式Session共享
Redis的数据类型
String类型
常用命令
// 赋值
set key value;
// 获取数据
get key;
// 赋值一个值,同时返回值
getset key value;
// 当值不存在时,赋值(可实现分布式锁)
setnx key value;
0:key存在,赋值失败
1:key不存在,赋值成功
(针对key为int类型使用)
// 每次对key的值递增1
incr key;
// 每次对key递增i的值
incr key i;
// 每次对key的值递减1
decr key;
// 每次对key的值递减i
decr key i;
// 向key拼接"hello"字符串,并返回长度
append key "hello";
// 获取key的长度
strlen key;
为什么 String 类型内存开销大?
String类型的数据存储包含
1.len:数据长度
2.alloc:内存大小
3.buff:数据本身
RedisObject数据:
RedisObject存储了额外的元数据,记录key的信息
比如最后一次访问的时间、被引用的次数等
指针信息:
redis实际是使用了哈希槽存储数据,每一个哈希槽都对应一个dictEntry。
dictEntry又包含了以下内容:
1.key的指针地址
2.value的指针地址
3.下一个dictEntry的指针地址
用什么数据结构可以节省内存?
Redis底层有一种数据结构为压缩列表(ziplist),使用一系列的entry连续存储,
不需要额外的指针来指向下一元素,节省内存空间,
Redis基于压缩列表实现了list/hash/sortSet等数据结构。
一个key可以存储整个集合的数据,只需要一个dictEntry,
而String类型的一个key就需要对应一个dictEntry
相比较,压缩列表极大的减少了dictEntyr的开销。
使用场景之分布式ID生成器
当一个表水平分表后,同一张表的数据存储在不同的数据库实例中,此时若采用数据库的自增id,会导致id重复问题。此时可以采用Redis来作为分布式ID生成器。
使用场景之对象缓存
当一个对象需要存储到Redis中,可以将对象序列化成Json数据后,以String格式保存到缓存中。下面是我所写RedisUtils的伪代码
/**
* 存储对象
* @param key key值,需要构建前缀,以区分业务
* @param value 需要存储的对象
* @param expireSec 缓存失效时间
*/
public <T> String set(String key, T value, Integer expireSec) {
if (StringUtils.isBlank(key)) {
return null;
}
String res;
try (Jedis jedis = jedisResourcePool.getResource()) {
res = jedis.set(buildKey(key), JsonUtils.toJson(value), SetParams.setParams().ex(expireSec).nx());
}
return res;
}
/**
* 获取对象
* @param key key值,需要构建前缀,以区分业务
* @param clazz 获取的对象类型,用于反序列化
*/
public <T> T get(String key, Class<T> clazz) {
if (StringUtils.isBlank(key)) {
return null;
}
try (Jedis jedis = jedisResourcePool.getResource()) {
String result = jedis.get(buildKey(key));
// 反序列化对象后返回
return JsonUtils.fromJson(result, clazz);
} catch (Exception e) {
LOGGER.info("获取缓存失败,key={}", key, e);
return null;
}
}
使用场景之分布式锁
分布式锁的实现方式
- 基于redis的分布式锁
- 基于zookeeper的分布式锁
- 基于数据库的悲观锁
设置分布式锁的注意事项
- 同一性:获取锁和释放锁必须是同一个请求
- 时效性:获取锁之后,必须设置过期时间,防止死锁的情况
- 互斥性:一个锁只能被一个客户端持有
采用Redis实现的分布式锁
- set方式
public boolean redisLock(String lockKey, String requestValue, int expire) {
String result = jedis.set(lockKey,requestValue,"NX","EX",int expire)
if(result.equals("ok")){
// 获取分布式锁成功
return true;
}
return false;
}
/**
* 获取分布式锁的参数说明
* @param lockKey 用于获取锁的业务key,例如订单id
* @param requestValue 请求值
* @param NX|XX NX-仅在键不存在时设置键。XX-只有在键已存在时才设置。
* @param EX|PX 设置指定的到期时间(EX以秒为单位,PX以毫秒为单位)。
* @param expire 过期时间
*/
jedis.set(String lockKey,String requestValue,String NX|XX,String EX|PX,int expire);
- setnx方式
public boolean redisLock(String lockKey, String requestValue, int expireTime) {
Long result = jedis.setnx(lockKey,requestValue);
// 获取锁成功
if(result == 1){
// 此处若发生异常,则导致加过期时间失败,可能会导致死锁
// 设置过期时间
jedis.expire(lockKey,expireTime);
return true;
}
return false;
}
- 释放锁
public void releaseLock(String lockKey,String requestValue){
//保证获取锁和释放锁必须是同一个请求
if(jedis.get(lockKey).equals(requestValue)){
jedis.del(lockKey); //释放锁
}
}
List类型
redis中的list采用的是LinkList双向链表,越接近两端,则获取值速度越快。可用于消息队列的实现。
常用命令
// 想链表左边添加元素
lpush list v1,v2:
// 向链表右边添加元素
rpush list v1,v2:
// 删除最左边的元素
lpop list:
// 删除最右边的元素
rpop list:
// 获取列表中两索引之间的值,当start等于0,end等于-1时,获取所有的值
lrange list start end:
// 获取索引为i的值。
lindex list i:
// 获取列表的长度。
llen list:
使用场景之消息队列
list类型的lpush和rpop(或者反过来lpop和rpush)能实现队列的先进先出功能,所以Redis的list类型能够实现简单的点对点消息队列。不过主流还是使用RocketMQ、Kafka、RabbitMQ等成熟的消息队列。
Hash散列类型
为键值对的数据类型,value只能为String类型
常用命令
// 例如给user的username赋值zhanshan
hset user username zhansan;
// 给map多个key进行赋值
hmset map key1 value1 key2 value2;
// 当key不存在时,才对key进行赋值
hsetnx map key1 value1;
// 获取map的key值
hget map key;
// 获取多个key的值
hmget map key1 key2;
//获取所有键值对
hgetAll map;
// 删除map中的key值
hdel map key;
常用场景
- 存储一些对象的属性,特别是容易发生增删改查的数据
- 例如:商品属性(如商品库存)
Set数据类型
set集合类型,它具备无序性/去重性
常用命令
// 给集合set添加多个元素
sadd set member1 member2 member3;
// 获取集合set的所有元素
smembers set;
// 删除元素
sremove set member1;
// 获取元素个数
scard set;
// 判断元素1是否存在
sismember set member1;
set集合运算
- 差集运算
//返回A-B的差集
SDiff setA setB;
- 并集运算
// 返回A与B的并集
sUnion setA setB;
- 交集运算
// 返回A与B的交集部分
sInner setA setB;
ZSet有序集合
有序集合,在set的基础之上关联了分数进行排序
相关命令
// 添加元素1以及对应分数
zadd zset score1 member1;
// 从小到大,获取索引之间的元素
zrange zset startIndex endIndex;
// 从大到小,获取索引之间的元素
zrevrange zset startIndex endIndex;
// 删除元素1
zrem zset member1;
// 获取分数1到分数2之中内的元素
zrangeByScore zset score1 score2;
// 删除指定分数内的元素
zremrangeByScore zset score1 score2;
常用场景
- 商品销量排行
- 点赞数排行
Redis 事务
- 介绍
Redis事务是为了保证一组命令的原子性(基于队列),主要通过四个命令实现
- 相关命令
multi;标记事务的开始,后续执行的redis命令会存入队列之中.
exec; 执行事务中的命令集队列.
discard; 清楚事务中的命令集队列
watch; 用于监控某个key的变化情况,当key发送变化时,事务不执行(可实现乐观锁)
- watch key : 监控某个key
- unwatch key:不监控某个key
- 事务失败机制
redis事务不支持回滚,若队列中的某个命令发生了错误,会分为两种情况处理:
情况1:
redis语法错误,此时整个事务都不执行.
情况2:
redis语法正确,但是由于类型错误的命令而执行失败,此时事务会执行正确的命令,而忽略错误的命令
不支持事务回滚的原因:
因语法错误和类型错误都是在开发阶段可以预见以及避免的,因此redis为了提高性能,不支持事务的回滚
redis持久化方案
在满足特定条件时,redis将缓存数据写入到磁盘中。分为rdb方式(默认)和aof方式
rdb快照方式
rdb是redis默认的持久化方式,rdb快照时,先将数据写入到临时,会将主机的数据写入到rdb文件中。
当redis宕机后重启时,会读取rdb文件中的数据进行恢复到缓存中。
-
快照触发的时机
-
触发配置文件中的条件-- redis.config文件
例 save 60 5 : 60秒内,有5个key发生了变化,则进行快照
-
执行save命令
save命令会阻塞redis服务器的进程,并进行快照,期间redis不能处理任何命令,直到rdb文件生成完毕
-
执行bgsave命令(主从同步)
bgsave命令不会阻塞redis服务器的线程,当bgsave命令执行时,redis父线程会fork一个子线程进行快照,而父线程继续处理服务器的请求,不会造成阻塞,因此bgsave更适用于线上数据的操作
-
-
bgsave不阻塞的原理
当redis进行快照时,redis的会fork一个子进程进行快照,将缓存中的数据写入到rdb文件中,
而父进程则继续处理客户端的请求,这种方式提高了redis的性能,在进行快照时,不会阻塞到其它请求。
当rdb文件写入完成后,才会将旧的rdb文件替换掉,保证rdb文件的完整性。
注:rdb文件是一种二进制文件,占用空间小,传输更快。
-
快照方式的优缺点
-
优点
rdb快照方式相较于aof持久化方式,对redis的性能损耗较小,它只有在满足特定条件下,才会执行持久化。同时它是通过fork子进程的方式进行快照,而父进程可以继续处理客户端的请求。
-
缺点
如果redis宕机的话,会丢失最后一次的正在进行的快照信息,当数据量比较大时,fork时间过长,会导致客户端请求的阻塞。
-
aof方式
aof方式是redis优化重写的持久化方案,当客户端每次请求服务器时,都会将每次更改的内容写入到aof日志中。
就算redis宕机,aof文件也包含了所有数据,因此它保证了数据的安全性,但它对redis的性能损耗较高。
-
aof的开启方式 aof默认是不开启的,如果需要开启,则需要修改redis.conf文件
appendonly yes :表示开启aof持久化方式,默认是appendonly no
appendfilename "appendonly.aof" :指定写入aof的文件名 -
aof的重写优化原理
-
安全原理
当客户端每次请求redis写操作时,redis每执行一次写命令,在对新的aof文件写入时,同时会对旧的aof同步写入,保证了写入过程中,redis宕机,也不会丢失旧数据。
当新的aof文件写入完毕后,新的aof文件包含了恢复当前数据的最小命令集合,才会替代旧的aof文件,内存相较于旧的aof文件,内存更小。-
特点
当aof文件过大时,会自动进行优化重写
-
优化重写的配置
auto-aof-rewrite-percentage 80 : 表示aof文件超过上一个aof文件的百分比,则开始重写
auto-aof-rewrite-min-size-64mb : 表示aof文件需要优化重写的最小内存 -
-
图解aof持久化过程
- aof相关参数调优
appendfsync always //每次写入磁盘都同步到aof文件中,保证数据安全,但影响性能
appendfsync everysec //每秒执行一次磁盘和aof文件的同步
appendfsync no //不主动同步,交由操作系统处理,速度快,但此种方法不安全
-
aof文件修复
-
场景:
当redis在重写优化时,发生了宕机,则再次重启时,会拒绝加载此aof文件,保证了数据一致性,并对其修复。
-
修复方式:
1、备份现用的aof文件
2、启动redis-check-aof命令,对原有的aof文件进行修复
3、重启redis服务,等待服务器载入修复后的aof文件,进行数据的修复。 -
持久化方案比较
rdb方式更适用于对数据安全不高,此方案性能更优 aof方式适用于对数据安全要求高的场景,此方案性能更安全
redis主从复制
持久化方案保证了redis宕机后,可以从硬盘恢复数据。当单点硬盘故障后,持久化方案不再
可以恢复数据,此时需通过主从复制来进行恢复redis服务。
- 主从复制机制
特点
1、redis主机每更新完数据候,会实时同步给从机。
2、当主机宕机后,从机可以继续提供服务
3、只能有一台主机,但可以有多台从机
4、从机可以提供读操作,但不可提供写操作。
- 主从同步配置
主机:
无需配置
从机:
修改redis的conf文件
slaveof 192.168.0.1 6379 //主机地址
- 同步过程
slave第一次连接时(全量同步):
1、slave向master发送sync命令
2、master接收完sync命令后,执行bgsave命令,生成快照文件
3、slave读取快照文件实现全量同步。
增量同步:
当slave初始化完毕后,master每更新一次数据,则将命令同步发送给slave。
增量同步,主库和从库在第一次进行全量同步之后,会建立一个长连接,主库会陆续将接受到的
写请求,发送给从库,避免了频繁的网络连接.
长连接带来的问题:
当主库和从库存在网络波动,此时写请求无法及时发送给从库.
redis2.8之前:
如果出现网络断开,则主库会重新给从库进行一次全量同步
redis2.8之后:
主库会将网络断开后的这段时间产生的写请求写入到缓冲区中,待网络恢复正常后,
再将缓冲区的写请求发送给从库,实现增量同步
缓冲环大小设置:
repl_backlog_size
如果它配置得过小,在增量复制阶段,可能会导致从库的复制进度赶不上主库,
进而导致从库重新进行全量复制。
redis哨兵机制
- 作用
监控:
哨兵可以通过心跳机制,监控redis集群中的各个节点的健康情况 故障切换: 当哨兵监控到master节点故障后,会通过选举机制将一个slave从机切换为master主机 提醒: 当哨兵监控到某个节点故障后,可以通知管理员或者其它应用
- 故障切换的过程
1、首先哨兵会将slave转为master,并将其它slave指向新的master
2、当客户端连接redis集群时,redis会给客户端返回新master的ip地址
- 哨兵的工作过程
1、哨兵会定期地向集群的每个节点发送ping命令
2、当集群中的服务器超过一定时间没有响应pong命令,则被哨兵标记为主观下线。
3、当一个服务被标记为主观下线后,所有的哨兵都会向该服务发送ping命令3.1、当超过一定数量的哨兵没有接收到服务的pong时,则服务会被标记为客观下线
3.2、当没有足够多的哨兵同意服务客观下线时,则移除掉主观下线
- 图解
redis缓存问题
缓存数据的过程(被动缓存):
1、先查询缓存,当缓存无数据则查询数据库。 2、查询数据库,当数据库有数据则写入缓存中。
-
过期key的删除策略
redis默认提供了两种对过期key的删除策略- 惰性删除: 当客户端访问到过期的key时,才对过期key进行删除
优点:对cpu友好,不会占用太多cpu
缺点:无效的过期key占用过多,影响redis的内存大小。- 定时删除: redis每个一段时间,就会扫描过期的key,为了不影响性能,每次只会扫描部分的key将其删除,减少对cpu的影响。
优点:增加空间利用率。
缺点:redis需要频繁扫描过期key,并操作删除,影响cpu性能。
基于以上原因,redis默认采用惰性删除+定时删除的混合策略。
-
缓存穿透
-
概念
当缓存和数据库都无key值时,大量并发请求该key时,造成服务器压力大,则是缓存穿透
-
解决方法
1、当数据库没该key值时,也对其进行缓存,当进行insert操作时,再更新该缓存。
2、布隆过滤器(bitmap位图),向其存储数据库不存在的key,请求都经过布隆器过滤掉无效请求 -
-
缓存击穿
-
概念
指缓存由于到期时间没有该数据,而数据库有该数据,此时有高并发的请求读取缓存,
而缓存没此数据,则高并发对数据库造成很多压力则是缓存击穿。-
解决方法
1、设置热点数据不过期
2、采用分布式锁,防止高并发同时请求数据库 -
-
缓存雪崩
-
概念
当某一时间有大量的key值过期失效,此时有高并发请求这些key,造成数据库压力大,则叫缓存雪崩。
-
解决方法
1、不同的key设置不同的过期时间。
2、二级缓存
2、缓存失效后,加锁或使用队列来控制查询数据库和写缓存的线程数 -