Redis基础总结
1、NoSQL出现的背景
单机MySQL→Memcached(缓存)+MySQL+垂直拆分→MySQL主从读写分离→分表分库+水平拆分+MySQL集群
MySQL存在的瓶颈
- MySQL数据库也经常存储一些大文本字段,导致数据库表非常的大,在做数据库恢复的时候就导致非常慢,不容易快速数据库
- 表结构更改困难
NoSQL–Not OnlySQL
- 泛指非关系型数据库
- 数据存储不需要固定的模式,无需多余操作就可以横向扩展
Nosql特点
- 易扩展:去关系型数据库的关系型特性
- 大数据量高性能:读写性能高、数据库的结构简单,细粒度的Cache(记录级)
- 多样灵活的数据模型:无需事先建立字段,随时可以存储自定义的数据格式
RDBMS与NoSQL比较
- RDBMS
- 高度组织化结构化数据
- 结构化查询语言(SQL)
- 数据和关系都存储在单独的表中
- 数据操纵语言,数据定义语言
- 严格的一致性
- 基础事务
- NoSQL
- 代表不仅仅是SQL
- 没有声明查询语言,没有预定义的模式
- 键-值对存储,列存储,文档存储,图形数据库
- 最终一致性,而非ACID属性
- 非结构化和不可预知的数据
- CAP定理
- 高性能、高可用和可伸缩性
ACID和CAP
- ACID:原子性、一致性、隔离性、持久性
- CAP:强一致性、高可用性、分布式容忍性
2、Redis入门
2.1 Redis初了解
Redis:Remote Dictionary Server 远程字典服务器
是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器。
Redis特点
- 支持数据持久化
- 支持key-value类型数据,同时还提供list、set、zset、hash等数据结构
- 支持数据备份,即master-slaver模式的数据备份
- 使用单线程的多路IO复用模型
Redis是单线程:单线程模型来处理客户端的请求。对读写等事件的响应
- Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是机器的内存和网络带宽,Redis是C语言编写的,官方提供的数据达到100000+的QPS(每秒内查询次数),这个数据不比Memcached差(单进程多线程的基于内存的Key-Value数据库)
Redis和Memecache区别
- Redis支持丰富的数据类型,包括简单的key/value的数据类型,同时还提供list,set,zset,hash等数据结构的存储。Memecache只支持简单的数据类型;
- Redis支持数据持久化,通过RDB和AOF方式持久化,而Memecache不支持;
- Redis原生支持集群模式,Memecache没有原生集群模式,需要依靠客户端来实现往集群分片写入数据
- Redis使用单线程的多路IO复用模型,Memecache是多线程非阻塞IO复用的网络模型。
Redis内存淘汰机制和实现
(1)内存淘汰策略
- 从已设置过期时间的的数据集中淘汰
- 从整个数据集中淘汰
1、volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
2、volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
3、volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
4、volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
5、allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
6、allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰
7、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
8、noeviction : 永不过期,返回错误,默认策略
(2)淘汰机制的实现
-
删除失效主键
-
消极方法(passive way),在主键被访问时如果发现它已经失效,那么就删除它。redis在实现GET、MGET、HGET、LRANGE等所有涉及到读取数据的命令时都会调用 expireIfNeeded,它存在的意义就是在读取数据之前先检查一下它有没有失效,如果失效了就删除它。
expireIfNeeded函数中调用的另外一个函数propagateExpire,这个函数用来在正式删除失效主键,并且广播告诉其他地方,目的地有俩:AOF文件,将删除失效主键的这一操作以DEL Key的标准命令格式记录下来;另一个就是发送到当前Redis服务器的所有Slave,同样将删除失效主键的这一操作以DEL Key的标准命令格式告知这些Slave删除各自的失效主键。
-
积极方法(active way),周期性地探测,发现失效就删除
-
主动删除:当内存超过maxmemory限定时,触发主动清理策略
-
-
置换策略:为了避免频繁的触发淘汰策略,每次会淘汰掉一批数据,淘汰的数据的大小其实是和置换的大小来确定的,如果置换的数据量大,淘汰的肯定也多。
Redis简单命令
# 开启redis服务
redis-server /myredis/reids.conf
redis-cli -p 6379
# 查看进程是否开启
ps -ef|grep redis
# 关闭进程、关闭redis
shutdown
exit
# 切换数据库,select dataNum[0-15]
select 1
# 查看数据库中所有的key
keys *
# 清除当前数据库
flushdb
# 清除所有数据库的内容
FLUSHALL
- 默认16个数据库,表从零开始,初始默认使用零号库
2.2 五大数据类型
String、List、Hash、Set、Zset
Redis:key
keys * # 查询数据库所有的key
set key value # 设置值
get key # 获得值
exists key # 判断当前的key是否存在
expire key 10 # 设置key的过期时间,单位秒
ttl key # 查看当前key存活的时间
type key # 查看当前key的类型
String:字符串
set key value # 设置值
get key # 获得值
exists key # 判断当前的key是否存在
append key hello # 追加字符串hello,如果当前key不存在,就相当于设置值
strlen key # 获取字符串的长度
incr key # 自增1
desr key # 自减1
incrby key 10 # 设置步长,指定增量
decrby key 10
getrange key 0 3 # 截取字符串[0,3],[0,-1]获取全部字符串
setex # 设置过期时间
setnx # 判断不存在,设置值
mset # 设置多个值
mget
msetnx # 原子性操作,有一个失败就全部失败
List:列表
list: 栈、队列、阻塞队列
lpush list value # 将一个值或多个值,插入到列表头部(左,后进先出) [栈]
lrange list 0 1 # 获取区间具体的值,[0,-1]获取list全部值
rpush list value # 将一个值或多个值,插入到列表尾部(右,先进先出) [队列]
lpop list # 移除list的第一个元素
rpop list # 移除list最后一个元素
lrem list 2 one # 移除list指定个数的value,精准匹配(移除key中2个one)
ltrim list 1 2 # 截取指定下标长度的list,并把截取部分赋值给list
rpoplpush list otherlist # 移除list的最后一个元素,并将它移动到新的list中
exists list # 判断这个列表是否存在
lset list 0 re # 更新list中0下标的值为re
linsert list before[after] world other # 将other插入到列表list的world前面或后面
- list的底层是链表结构
- 在两边插入或改动值效率最高,中间元素相对效率低一点
- 消息队列(lpush、rpop),栈(lpush、lpop)
Set:集合
set值是无序,不重复
sadd set1 value ... # 添加值到set1
smembers set 1 # 查看set1的所有值
sismember set1 hello # 判断hello是否在set1中
scard set1 # 获取set1集合的元素个数
srem set1 hello # 移除set1集合中的hello元素
srandmember set1 [2] # 随机抽出元素,可以指定抽出元素个数,默认为1
spop set1 # 随机删除set集合中元素,可以指定精确删除的值
smove set1 setother xuzy # 将元素xuzy从set1移动到setother
sdiff set1 set2 # 差集
sinter set1 set2 # 交集
sunion set set2 # 并集
Hash:哈希
Map集合,key-value
hset hash1 field1 value ... # 设置一个hash,值为field1-value
hget hash1 field1 field2 ... # 获取值
hgetall hash1 # 获取所有数据
hdel hash1 field1 # 删除hash1里的field1字段
hlen hash1 # 获取hash1的字段数量
hexists hash1 field1 # 判断field1在hash1中是否存在
hkeys hash1 # 获取hash1中所有field
hvals hash1 # 获取hash1中所有的value
hincrby hash1 field1 1 # 指定增量为1
hsetnx hash1 field hello # 如果不存在可以设置,如果存在着不能设置
Zset:有序集合
zadd zset1 score1 v1 # 添加值
zrange zset1 0 -1 # 获取所有值
zrangebyscore zset1 -inf +inf [withscores] # 显示所有值,并且根据score从小到大,[附带成绩],inf也可用实际的数值表示条件筛选
zrem zset1 xuzy # 删除指定元素xuzy
zcard zset1 # 获取有序集合的个数
zcount zset1 1 3 # 获取指定score区间的成员数量
2.3 三种特殊数据类型
Geospatial:地理位置
# geoadd key 经度 纬度 城市
geoadd city 116.40 39.90 beijing
geopos city beijing
# 两地(人)距离 单位
geodist city beijing guangzhou km
# 以给定的经纬度为中心,找出某一个半径内的元素,[显示到中心点距离],[显示半径内的元素的定位信息]
georadius city 110 20 1000 km [withdist] [withcoord]
# 筛选结果的数量
georadius city 110 20 1000 km count 1
# 找出位于指定元素(指定)周围的其他元素
georadiusbymember china:city beijing 1000 km
# 将二维的经纬度转换为字符串,两个字符串越接近,距离越近
geohash city beijing guangzhou
- 延伸
# geo底层实现原理是Zset,因此可以使用Zset的命令操作geo
# 查看全部元素
zrange city 0 -1
# 删除指定元素
zrem city beijing
Hyperloglog:基数
基数:不重复的元素,可以接受误差,如果不能容错,就使用set集合统计
Redis Hyperloglog 基数统计算法
pfadd key value1 value2 ... # 创建第一组元素
pfcount key # 统计key元素的基数数量
pfmerge key3 key key2 # 将key和key2合并到key3 并集
Bitmap:位存储
存储二进制记录,即只有0和1两种状态
# setbit key offset value 设置值
setbit sign 0 1
# getbit key offset 获取值
getbit sign 0
bitcount sign # 统计值为1的个数
3、事务
Redis事务:一组命令的集合、所有命令序列化执行。
一次性、顺序性、排他性
注意:Redis单条命令具有原子性,但事务不保证原子性
- 开启监控(watch)/【unwatch解锁】
- 开启事务(multi)
- 命令入队
- 执行事务(exec)/取消事务(discard)
编译期异常,事务所有命令都不会被执行。
运行期异常,错误命令无法执行,其他命令可以正常执行。(这是事务没有原子性的原因)
乐观锁
- 基于数据版本的记录机制实现
- 使用watch监视键值对,如果事务提交exec时,watch监视的键值对发生变化,事务将被取消
分布式锁
//TODO
Redis的并发竞争问题
问题描述:Redis的并发竞争Key问题是多个系统同时对一个key进行操作,但最后执行的顺序和我们期望的顺序不同,导致结果的不同。
解决方法:基于Zookeeper临时有序节点实现分布式锁。大致思想:每个客户端对某个方法加锁时,在Zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬间有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬间节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
5、解读Redis.conf
启动redis的配置文件
redis-server /../redis.conf
redis-cli -p 6379
配置文件配置内容解读
# unit单位,大小写不敏感
# include 包含 引入其他配置文件
# include /path/to/local.conf
# 网络
bind 127.0.0.1 # 绑定IP,只有绑定的IP才能访问
protect-mode yes # 保护模式,默认yes
port 6379 # 端口设置
# general 通用
daemonize yes # 以守护进程方式运行,默认是no
pidfile /var/run/redis_6379.pid #以后台方式运行,需要指定一个pid文件
# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境
# warning (only very important / critical messages are logged)
loglevel notice
logfile "" # 日志文件位置名
databases 16 # 数据库数量,默认16
always-show-logo yes # 是否显示logo
# snapshot 快照
# RDB持久化配置区域
save 900 1 # 900s内,至少有1个key进行修改,则触发持久化操作
save 300 10
save 60 10000
stop-write-on-bgsave-error yes # 持久化出错,是否还继续工作
rdbcompression yes # 是否压缩rdb文件,需要消耗一点CPU资源
rdbchecksum yes # 保存rdb文件的时候,进行错误检查校验
dir ./ # rdb文件保存目录
# replication 复制
# 主从复制设置
# security 安全
# 设置redis密码,默认没有密码
# clients 限制
maxclients 10000 # 设置能连接上redis的最大客户端数量
maxmemory <bytes> # 配置最大内存容量
maxmemory-policy noeviction # 内存到达上限之后的处理策略
1、volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
2、volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
3、volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
4、volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
5、allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
6、allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰
7、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
8、noeviction : 永不过期,返回错误,默认策略
# append only file 模式
# AOF持久化配置
appendonly no # 默认不开启AOF模式,大部分场景下RDB完全够用
appendfilename "appendonly.aof" # 持久化的文件名字
# appendfsync always # 每次修改都会 sync。消耗性能
appendfsync everysec # 每秒执行一次 sync,可能会丢失这1s的数据!
# appendfsync no # 不执行 sync,这个时候操作系统自己同步数据,速度最快!
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb # 如果aof文件大于64mb,将自动fork一个新的进程来将重写
6、Redis持久化
Redis是内存数据库,如果不将内存中的数据持久化到磁盘中,一旦服务器进程退出,则服务器终端数据库状态也会消失,因此Redis提供了持久化功能。
Redis持久化有两种方法:RDB和AOF
6.1 RDB
Redis会单独创建(fork)一个子进程来进行持久化,首先将数据写入一个临时RDB文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程不进行任何IO操作,确保了极高的性能。如果需要进行大规模数据恢复,且对数据恢复的完整性不是很敏感,那么RDB方式比AOF方式要更加高效。
缺点:
- 如果发生宕机,最后一次持久化的数据可能丢失
- fork进程的时候,会占用一定的内容空间
6.2 AOP(Append Only File)
以日志形式记录每个写操作,将Redis执行的所有指令记录下来(读操作不记录),只许追加文件,但不可以改写文件,Redis启动前会读取该文件重构数据。
redis提供一个检查校验工具:redis-check-aof --fix
优点:
- 每一次修改都同步,数据同步的完整性更好
- 每秒同步一次,最多丢失1s数据
- 从不同步的效率最高,由操作系统自己同步数据
缺点:
- 数据文件大小:aof远大于rdb,修复的速度也比rdb慢
- 运行效率:aof运行效率比rdb慢,redis默认的配置就是rdb持久化
7、主从复制
主从复制(master-slave)主节点Redis服务器的数据,复制到从节点Redis服务器。
默认情况下,每台Redis服务器都是主节点。
主从复制的作用:
- 数据冗余:实现数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复:当主节点发生问题,可以由从节点提供服务,实现快速的故障恢复
- 负载均衡:读写分离,提高Redis服务器的并发量
- 高可用(集群):Redis高可用的基础
环境配置:只配置从库
info replication # 查看当前库信息
slaveor ip port # 设置作为主机(ip)的从机
复制原理
- Slave启动成功连接master后会发送一个sync同步命令
- master在第一次接到同步命令时,会进行一次全量复制,后面开始做增量复制
- 全量复制:一次完全同步操作
- 增量复制:master将新的修改命令依次传给slave,完成同步
哨兵模式
哨兵(Sentinel)模式:后台监控注解是否故障,如果故障将根据投票数自动将从库转换为主库,其他从库作为新选定的主库的从库。
-
配置哨兵配置文件sentinel.conf
# sentinel monitor 被监控的名称 host port 1 sentinel monitor myredis 127.0.0.1 6379 1
-
启动哨兵
redis-sentinel /../sentinel.conf
注意:主机宕机重连后,会成为新主机的从机
8、Redis缓存穿透和雪崩
缓存穿透
故意的去请求缓存中不存在的数据,导致请求都打到了数据库上,导致数据库异常。
【缓存穿透是指缓存和数据库都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到的数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层查询,失去缓存的意义。】
解决方案:
- 接口层增加校验:用户鉴权校验、id基础校验
- 在缓存取不到的数据,在数据库中也没有取到的,这时将key-value写入key-null、设置缓存存活时间(setnx)短点,可以防止用同一个id进行的暴力攻击,对一些需要保持一致性的业务有影响
缓存击穿
redis中存储的是热点数据,当高并发请求访问redis中热点数据的时候,如果redis中的数据过期了,会造成缓存击穿的现象,请求都打到了数据库上。
【缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户太多,引起数据库压力瞬间增大。】
解决方案:
- 设置热点数据永不过期
- 接口限流与垄断,降级。重要的接口做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口的某些服务不可用时,进行熔断,失败快速返回机制
- 布隆过滤器:类似一个hash set,用于快速判断某个元素是否存在集合中
- 加互斥锁:
- 分布式锁:保证对应每个key同时只有一个线程去查询后端业务,其他线程没有获得分布式锁权限,因此只需要等待即可。
缓存雪崩
缓存同一时间大面积的失效,这个时候又来的一波请求都到数据库上,导致数据库连接异常。
【缓存雪崩是指缓存中数据大批量到过期时间,而查询量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同,缓存击穿是指并发查同一条数据,缓存雪崩是指不同数据都过期了,很多数据在缓存查不到从而查数据库。】
解决方案:
- 缓存数据过期时间设置随机,防止同一时间大量数据过期
- 如果缓存数据库是分布式部署,可以将热点数据均匀分布在不同的缓存数据库中
- 设置热点数据永不过期
4、Java连接Redis数据库
4.1 Jedis
解决连接虚拟机的Redis时出现的拒绝访问和超时的问题
https://blog.csdn.net/DragonFreedom/article/details/79512686?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
- 导入依赖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
- 测试
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.204.128",6379);
System.out.println(jedis.ping());
}
常用API接口方法,与上述2命令一致。
4.2 SpringBoot整合Redis
springboot操作数据:spring-data jpa jdbc mongodb redis
springboot2.x以后,底层由原来的jedis替换为lettuce
- jedis:采用直连方式,多线程操作,不安全。如果想避免不安全,使用jedis pool连接池。
- lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数据。
手写一个RedisConfig替换自动配置redisTemplate组件
@Configuration
public class RedisConfig {
// 自己定义了一个 RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
factory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String,
Object>();
template.setConnectionFactory(factory);
// Json序列化配置
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);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new
StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
参考文献
1、【狂神说Java】Redis最新超详细版教程通俗易懂:https://www.bilibili.com/video/BV1S54y1R7SB
2、【面试】redis缓存穿透、缓存击穿、缓存雪崩区别和解决方案:https://blog.csdn.net/fcvtb/article/details/89478554