Redis
一. NoSQL介绍
NoSQL非关系型数据库,通常称为"non-relation",也被称为"not only sql"。NoSQL泛指非关系型的数据库,区别于关系型数据库,它不保证关系型数据库的ACID特性。NoSQL有着易扩展,大数据量,高性能等特点,得益于数据的无关系性,NoSQL的数据结构简单,具有比较快的读写性能。
1.1. NoSQL的分类(四大分类)
- 键值对:这一类的数据库会使用一个哈希表,表中有一个特定的键值指向一个特定的数据。key/value键值对类型数据库,扩展性好,灵活性好,大量写操作性能好。通用用于内容缓存,常见的有:redis,memcached
- 列式数据库:列式数据库通常用来存储海量数据的,键仍然存在,但是指向了多个列,这些列式由列家族来安排的。列式数据库,查找速度快,扩展性好,容易进行分布式扩展。通常用于分布式数据存储与管理,常见的有:HBase,Cassandra,HadoopDB
- 文档数据库:同键值对类型数据库相似,该类型的数据是版本化的文档,半结构的文档以特定的形式存储,如JSON。文档型的数据库可以看做是键值对的升级,支持键值的嵌套。性能好,灵活性高,数据结构灵活。通常用于存储,索引并管理面向文档的数据或类似半结构化的数据,常见的有:MongoDB,CouchDB
- 图形数据库:图形数据库使用灵活的图形模型,并且能够扩展到多个服务器。灵活性,支持复杂的图形算法,可用于构建复杂的关系图谱。通常用于大量复杂,互连接,低结构的图结构场合,如社交网络等,常见的有:Neo4j,InfoGrid,GraphDB
不同分类比较:
二. Redis及数据类型
Redis(Remote Dictionary Server)远程字典服务,开源的由ANSI C语言编写,支持网络、可基于内存操作数据也可持久化的,key-value的数据库,可用于高作缓存,消息队列。Redis支持字符串、列表,集合,哈希表,有序集合等数据类型。内置复制,lua脚本,LRU收回,事务以及磁盘持久化(RDB/AOF)。
TIPS:以下各数据类型中只示范部分命令,更多命令请参考:官方地址
2.1. String(字符串)
String是Redis最基本的类型,是由一组字节组成的序列。在redis中是二进制安全的,可以接受任何格式的数据,是最标准的key-value,通常用于存储字符串,整数和浮点,value最大可以存储512MB的数据。
字符串常用命令:
192.168.72.128:6379> select 1 # 切换redis数据库
OK
192.168.72.128:6379[1]> select 0
OK
192.168.72.128:6379> flushdb # 清空当前数据库数据
OK
192.168.72.128:6379> flushall # 清空所有数据库数据
OK
192.168.72.128:6379> set str hello,redis # 设置key为"str",value为"hello,redis"
OK
192.168.72.128:6379> get str # 获取key为"str"的value值
"hello,redis"
192.168.72.128:6379> setnx name redis # 当key不存在时设置key的值,设置成功返回1,失败为0
(integer) 1
192.168.72.128:6379> setnx name redis # name已存在,设置失败,返回0
(integer) 0
192.168.72.128:6379> getrange str 0 4 # 获取指定key的子串,str从0到4子串
"hello"
192.168.72.128:6379> setex tmp 10 tmpvalue # 设置key为tmp的过期时间为10s
OK
192.168.72.128:6379> get tmp # 10s后获取tmp,已过期,返回nil
(nil)
192.168.72.128:6379> strlen str # 获取key为"str"的value值字符长度
(integer) 11
192.168.72.128:6379> setrange str 6 world # 用value覆盖指定key存储的值,从偏移量offset开始
(integer) 11
192.168.72.128:6379> get str
"hello,world"
192.168.72.128:6379> keys * # 获取当前库的所有key
1) "name"
2) "str"
2.2. List(列表)
Redis的列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部或尾部。列表最多可存储 2^32 - 1 元素 (4294967295, 每个列表可存储40多亿)
列表的常用命令:
92.168.72.128:6379> lpush list redis # 向列表list头部插入一个值
(integer) 1
192.168.72.128:6379> lpush list mongodb
(integer) 2
192.168.72.128:6379> lpush list memcache
(integer) 3
192.168.72.128:6379> lrange list 0 3 # 获取列表指定范围的值
1) "memcache"
2) "mongodb"
3) "redis"
192.168.72.128:6379> llen list # 获取列表的长度
(integer) 3
192.168.72.128:6379> lindex list 1 # 获取列表指定索引的值
"mongodb"
192.168.72.128:6379> linsert list after mongodb mysql # 在列表指定的元素(mongodb)后插入值(mysql)
(integer) 4
192.168.72.128:6379> lrange list 0 4
1) "memcache"
2) "mongodb"
3) "mysql"
4) "redis"
192.168.72.128:6379> linsert list before mongodb oracle # 在列表指定的元素(mongodb)前插入值(oracle)
(integer) 5
192.168.72.128:6379> lrange list 0 4
1) "memcache"
2) "oracle"
3) "mongodb"
4) "mysql"
5) "redis"
192.168.72.128:6379> rpop list # 移除并获取列表最后的元素
"redis"
192.168.72.128:6379> lpop list # 移除并获取列表的第一个值
"memcache"
192.168.72.128:6379> lrange list 0 4
1) "oracle"
2) "mongodb"
3) "mysql"
192.168.72.128:6379> lset list 0 redis # 将指定索引位置(0)的元素设置为指定的值(redis)
OK
192.168.72.128:6379> lrange list 0 3
1) "redis"
2) "mongodb"
3) "mysql"
2.3. Hash(哈希)
Hash是一个键值对的集合,是一个string类型的field和value的映射表,hash适合存储对象。每个 hash 可以存储 2^32 - 1 键值对(40多亿)。
哈希常用命令:
192.168.72.128:6379> hset myhash hashname hashcode # 设置hash表中myhash的filed(hashname)的值设为value(hashcode)
(integer) 1
192.168.72.128:6379> hsetnx myhash hashvalue redishash
(integer) 1
192.168.72.128:6379> hlen myhash # 获取hash表中的字段长度
(integer) 2
192.168.72.128:6379> hkeys myhash # 获取hash表的字段
1) "hashname"
2) "hashvalue"
192.168.72.128:6379> hvals myhash # 获取hash表的所有value
1) "hashcode"
2) "redishash"
192.168.72.128:6379> hget myhash hashname # 获取指定字段(hashname)的值
"hashcode"
192.168.72.128:6379> hgetall myhash # 获取hash表中指定key(myhash)的所有字段和值
1) "hashname"
2) "hashcode"
3) "hashvalue"
4) "redishash"
192.168.72.128:6379> hexists myhash hashage # hash表中指定key(myhash)是否存在指定的字段(hashage),存在返回1,不存在返回0
(integer) 0
192.168.72.128:6379> hsetnx myhash hashname redis # hash表中指定key(myhash)中不存在字段(hashname)时设置value(redis),存在则返回0
(integer) 0
192.168.72.128:6379> hsetnx myhash hashredis redis # hash表中指定key(myhash)中不存在字段(hashname)时设置value(redis),不存在设置成功返回1
(integer) 1
2.4. Set(集合)
Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
集合常用命令:
192.168.72.128:6379> sadd myset redis # 向集合中添加成员
(integer) 1
192.168.72.128:6379> sadd myset mogodb
(integer) 1
192.168.72.128:6379> sadd myset memcache
(integer) 1
192.168.72.128:6379> sadd myset redis # 集合中已存在要添加的成员时,添加失败
(integer) 0
192.168.72.128:6379> scard myset # 获取集合中成员数
(integer) 3
192.168.72.128:6379> smembers myset # 获取集合中所有成员
1) "mogodb"
2) "memcache"
3) "redis"
192.168.72.128:6379> srandmember myset # 随机返回指定集合中的成员
"mogodb"
192.168.72.128:6379> sadd myset1 redis mysql oracle hbase # 批量向集合(myset1)中添加成员
(integer) 4
###############################################################################################
192.168.72.128:6379> sdiff myset myset1 # 比较两个结合的差集
1) "mogodb"
2) "memcache"
192.168.72.128:6379> sdiffstore myset2 myset myset1 # 比较两个集合的差集,并保存在另一个集合
(integer) 2
192.168.72.128:6379> smembers myset2
1) "mogodb"
2) "memcache"
###############################################################################################
192.168.72.128:6379> sinter myset myset1 # 两个集合的交集
1) "redis"
192.168.72.128:6379> sinterstore myset3 myset myset1 # 两个集合的交集并存储到另一个集合
(integer) 1
192.168.72.128:6379> smembers myset3
1) "redis"
###############################################################################################
192.168.72.128:6379> sunion myset myset1 # 两个集合的并集
1) "redis"
2) "memcache"
3) "hbase"
4) "mogodb"
5) "oracle"
6) "mysql"
192.168.72.128:6379> sunionstore myset4 myset myset1 # 两个集合的并集并存储到另一个集合
(integer) 6
192.168.72.128:6379> smembers myset4
1) "redis"
2) "memcache"
3) "hbase"
4) "mogodb"
5) "oracle"
6) "mysql"
###############################################################################################
192.168.72.128:6379> spop myset
"memcache"
192.168.72.128:6379> smove myset myset5 redis # 移动指定集合(myset)中指定成员(redis)到指定集合(myset5)
(integer) 1
192.168.72.128:6379> smembers myset5
1) "redis"
2.5. Zset(有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
有序集合常用命令:
192.168.72.128:6379> zadd myzset 0 redis # 向有序集合添加一个或多个成员,已存在则更新成员的分数值
(integer) 1
192.168.72.128:6379> zadd myzset 1 mongodb
(integer) 1
192.168.72.128:6379> zadd myzset 2 memcache
(integer) 1
192.168.72.128:6379> zcard myzset # 获取有序集合成员数量
(integer) 3
192.168.72.128:6379>
192.168.72.128:6379> zscore myzset redis # 获取指定成员在集合中的分数值
"0"
192.168.72.128:6379> zincrby myzset 9 redis # 有序集合中指定的成员(redis)分数值加上增量increment
"9"
192.168.72.128:6379> zcount myzset 0 10 # 获取有序集合指定区间的成员个数
(integer) 3
192.168.72.128:6379> zrank myzset mongodb # 返回有序集合中指定成员(mongodb)的索引
(integer) 0
192.168.72.128:6379> zrange myzset 0 3 # 通过索引(0,3)返回有序集合中指定区间的成员
1) "mongodb"
2) "memcache"
3) "redis"
2.6. 三种特殊类型
- Geospatial(地理位置):Redis3.2版本推出的,可以推算地理位置的信息,两地之间的举例,方圆范围的人。使用场景:朋友定位,附件的人,打车举例计算。
命令示例:
192.168.72.128:6379> geoadd china:city 1 1 beijing # 将指定的地理位置(经纬度)添加到指定的key中
(integer) 1
192.168.72.128:6379> geoadd china:city 2 2 shanghai
(integer) 1
192.168.72.128:6379> geoadd china:city 3 3 guangzhou
(integer) 1
192.168.72.128:6379> geoadd china:city 4 4 tianjin
(integer) 1
192.168.72.128:6379> geodist china:city beijing shanghai m # 计算key中指定的两个地理位置之间的距离
"157270.0561"
192.168.72.128:6379> geopos china:city beijing # 获取key中指定位置元素的位置(经纬度)
1) 1) "0.99999994039535522"
2) "0.99999945914297683"
192.168.72.128:6379> georadius china:city 1 1 400 km # 以key中指定位置元素(经纬度)为中心,找出指定半径的元素
1) "beijing"
2) "shanghai"
3) "guangzhou"
- Hyperloglog(基数):数学上来讲,两个集合不重复的元素,在redis中,可能有一定的误差,Hyperloglog占内存是固定的,2^64个元素,相当于只需要12Kb的内存即可,效率极高。使用场景:允许误差的情况下,IP数统计,页面的uv。
命令示例:
192.168.72.128:6379> pfadd myhyperlog a b c d e f j h i j k l # 添加指定元素到hyperlog中
(integer) 1
192.168.72.128:6379> pfcount myhyperlog # 返回指定hyperlog的基数估算值
(integer) 11
192.168.72.128:6379> pfadd myhyperlog1 a h i j b c
(integer) 1
192.168.72.128:6379> pfcount myhyperlog1
(integer) 6
192.168.72.128:6379> pfmerge datalist myhyperlog myhyperlog1 # 合并两个hyperlog到指定的第三个hyperlog中
OK
192.168.72.128:6379> pfcount datalist
(integer) 11
- Bitmap(位示图):Bitmap是通过一bit位来表示某个元素对应的值或者状态,只有0和1两个状态,key对应的就是元素本身。使用场景:用户签到,在线活跃用户。
命令示例:
192.168.72.128:6379> setbit login 1 0 # 统计登陆装填 1:周一 0:未登陆
(integer) 0
192.168.72.128:6379> setbit login 2 1 # 统计登陆装填 1:周二 1:登陆
(integer) 0
192.168.72.128:6379> setbit login 3 0
(integer) 0
192.168.72.128:6379> setbit login 4 0
(integer) 0
192.168.72.128:6379> setbit login 5 1
(integer) 0
192.168.72.128:6379> setbit login 6 1
(integer) 0
192.168.72.128:6379> setbit login 7 0
(integer) 0
192.168.72.128:6379> getbit login 1 # 获取周一是否登陆
(integer) 0
192.168.72.128:6379> bitcount login # 统计登陆天数
(integer) 3
三. Redis.conf
3.1. linux环境Redis安装
- 进入官网 (p3-juejin.byteimg.com/tos-cn-i-k3…) 选择要安装的版本,这里使用6.2.7版本
- 解压压缩包,并移动解压后的文件到指定目录
[zc@localhost Downloads]$ ls
redis-6.2.7.tar.gz
[zc@localhost Downloads]$ tar -xvf redis-6.2.7.tar.gz # 执行解压命令
[zc@localhost Downloads]$ ls
redis-6.2.7 redis-6.2.7.tar.gz
- 编译安装
[zc@localhost Downloads]$ cd redis-6.2.7/
[zc@localhost redis-6.2.7]$ ls
00-RELEASENOTES COPYING MANIFESTO runtest-cluster src
BUGS deps README.md runtest-moduleapi tests
CONDUCT INSTALL redis.conf runtest-sentinel TLS.md
CONTRIBUTING Makefile runtest sentinel.conf utils
[zc@localhost redis-6.2.7]$ make
## make执行完成后执行 make install, make install默认安装在/usr/local/bin下, 我们可以使用以下命令安装在指定目录下
[zc@localhost redis-6.2.7]$ su
Password:
[root@localhost redis-6.2.7]# make PREFIX=/usr/local/redis install
# 安装完毕后/usr/local/redis下会生成bin文件夹, 此时移动/home/zc/Downloads/redis-6.2.7到/usr/local/redis下
[zc@localhost Downloads]$ su
Password:
[root@localhost Downloads]# cp -r redis-6.2.7 /usr/local/redis
[root@localhost Downloads]# cd /usr/local/redis
[root@localhost redis]# ls
bin redis-6.2.7
[root@localhost redis]#
- 启动redis并连接
[root@localhost redis]# cd bin/
[root@localhost bin]# ls
redis-benchmark redis-check-rdb redis-sentinel
redis-check-aof redis-cli redis-server
[root@localhost bin]# ./redis-server # 启动redis
#####################################################
# 重新开启一个终端测试连接
[zc@localhost bin]$ ./redis-cli -p 6379
127.0.0.1:6379> ping
PONG # 连接成功
127.0.0.1:6379>
3.2. redis.conf配置文件
./redis-server /path/to/redis.conf # redis-server启动,加载指定配置文件
# 内存单位
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
include /path/to/local.conf ## 如果存在多个redis实例时,可以加载通用配置
include /path/to/other.conf
#########################################################################
bind 192.168.xx.xx # 给redis实例绑定ip
port 6379 ## redis默认端口
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0 # 客户端在N秒内空闲连接, 关闭客户端连接(0:空闲不关闭连接)
datebases 16 # 数据库数量, 默认16个
daemonize yes # 是否开启守护进程
requirepass xxx # 开启密码认证
#########################################################################
# 持久化配置
# RDB 多少秒内有多少次更新操作, 就将数据同步到 XX.rdb文件中
# save "" # 不需要持久化时, 配置save ""
save 900 1 # 900秒内有一个修改则持久化
save 300 10 # 330秒内有10个修改
save 60 10000 # 60秒内有一万个修改
dbfilename dump.rdb # RDB持久化, 默认存储文件名
rdbcompression yes # 持久化时是否对数据进行压缩, 默认开启
dir ./ # 持久化文件存储位置
# AOF
appendonly no # 是否开始aof, 默认为否
appendfilename appendonly.aof # aof持久化默认文件名
appendfsync always # 每次执行写命令后, 调用fsync进行数据持久化
appendfsync everysec # 每秒执行一次持久化
appendfsync no # 等待操作系统将缓存刷进磁盘
auto-aof-rewrite-percentage 100 # 当文件的大小达到原先文件大小(上次重写后的文件大小,如果没有重写过,那就是redis服务启动时的文件大小)的两倍
auto-aof-rewrite-min-size 64mb # aof文件重写的最小文件大小,小于64mb时不会重写
四. Redis事务及持久化
4.1. Redis事务
Redis事务可以一次执行多个命令,并且带有两个重要的保证:事务是一个单独隔离操作,事务中的命令都会序列化,按顺序执行,事务执行过程中不会被其他客户端的命令中断;事务是一个原子操作,要么全部执行,要么全部不执行。
multi、exec、discard、和watch是redis相关的事务命令。
- multi: 开启事务, redis会将之后的命令入队
- exec: 执行事务中的所有命令
- discard: 取消事务, 放弃事务块中的所有命令
- watch: 监视一个或多个key, 如果事务在执行前, 这个key(或多个key)被其他命令修改, 则事务被中断, 不会执行事务中的任何命令
- unwatch: 取消WATCH对所有key的监视。
事务执行分为三个步骤:开启事务(multi),命令入队,执行事务(exec)
192.168.72.128:6379> multi # 开启事务
OK
192.168.72.128:6379> set redistx redistx # 命令入队
QUEUED
192.168.72.128:6379> set redistx1 redistx1
QUEUED
192.168.72.128:6379> exec # 执行事务
1) OK
2) OK
192.168.72.128:6379> get redistx # 事务执行成功
"redistx"
#####################################################
192.168.72.128:6379> multi # 开启事务
OK
192.168.72.128:6379> set k1 v1 # 命令入队
QUEUED
192.168.72.128:6379> set k2 v2
QUEUED
192.168.72.128:6379> discard # 取消事务
OK
192.168.72.128:6379> get k1 # 事务取消, 命令未执行, key值不存在
(nil)
#####################################################
事务出现错误时,Redis的处理:
- 语法错误(编译错误):开启事务,修改k3和k4的值,由于k4语法错误,事务提交失败
192.168.72.128:6379> set k3 v3
OK
192.168.72.128:6379> set k4 v4
OK
192.168.72.128:6379> multi # 开启事务
OK
192.168.72.128:6379> set k3 v3-1
QUEUED
192.168.72.128:6379> sets k4 v4-1 # 语法错误
(error) ERR unknown command `sets`, with args beginning with: `k4`, `v4-1`,
192.168.72.128:6379> exec
(error) EXECABORT Transaction discarded because of previous errors. # 语法错误导致事务未提交
192.168.72.128:6379> get k3 # 事务提交失败,k3值未改变
"v3"
192.168.72.128:6379> get k4 # 事务提交失败,k4值未改变
"v4"
- Redis类型错误(运行错误):开启事务后,修改k1值为k1-1,k2值加1,但将k2的value类型作为string,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行
192.168.72.128:6379> set k1 v1
OK
192.168.72.128:6379> set k2 v2
OK
192.168.72.128:6379> multi # 开启事务
OK
192.168.72.128:6379> set k1 v1-1 # 命令入队
QUEUED
192.168.72.128:6379> incr k2
QUEUED
192.168.72.128:6379> exec # 执行事务
1) OK
2) (error) ERR value is not an integer or out of range
192.168.72.128:6379> get k1 # 事务提交成功,k1值修改成功
"v1-1"
192.168.72.128:6379> get k2 # k2的操作命令在提交事务时,运行是类型检测错误,放弃执行,值未改变
"v2"
watch:在事务开始前用WATCH监控k1,之后修改k1为v1-1,说明事务开始前k1值被改变,MULTI开始事务,修改k1值为v1-2,k2为v2-1,执行EXEC,发回nil,说明事务回滚;查看下k1、k2的值都没有被事务中的命令所改变。
192.168.72.128:6379> set k1 v1
OK
192.168.72.128:6379> set k2 v2
OK
192.168.72.128:6379> watch k1
OK
192.168.72.128:6379> set k1 v1-1
OK
192.168.72.128:6379> multi
OK
192.168.72.128:6379> set k1 v1-2
QUEUED
192.168.72.128:6379> set k2 v2-1
QUEUED
192.168.72.128:6379> exec
(nil)
192.168.72.128:6379> get k1
"v1-1"
192.168.72.128:6379> get k2
"v2"
4.2. Redis数据持久化
Redis是一个内存数据库,在读写效率高的同时,也带来一个缺点,一旦宕机或停电,内存中的数据将会丢失。Redis持久化是指在指定的时间间隔内将内存中的数据快照写入磁盘,恢复时会将快照文件读入内存,redis提供了两种数据持久化方式RDB(Redis Database)和AOF(Append of File)。
- RDB(Redis默认方式)
RDB持久化是在指定的时间间隔内将内存的数据集快照写入磁盘,将内存的数据以快照的方式写入二进制文件,默认文件名dump.rdb(参照redis.conf)。通过客户端执行save(save策略参照redis.conf)或bgsave会生成dump.rdb文件,每次执行命令都会将所有内存快照写入一个新的rdb文件,并覆盖原有rdb文件。
同步方式:Redis会单独创建一个子进程(fork函数)来持久化,先将数据写入一个临时文件中,持久化过程结束后,再用这个临时文替换上次持久化好的文件。
持久化过程:- save:save命令是一个同步操作,执行该命令,RDB持久化是在主进程进行的,这样会阻塞服务,直到RDB持久化结束后,客户端才能正常连接redis服务。
- bgsave(copy on wirte:cow):bgsave命令是对save命令的一个优化,是一个异步操作。执行该命令后,redis主进程会通过fork操作创建一个子进程,RDB持久化是由子进程操作,完成后自动结束。这个过程中,主进程不阻塞,可以继续接收客户端的访问。
- AOF
AOF是redis提供的另一种数据持久化方式,它会记录客户端对redis服务端的每一次写操作,并将这些写操作以redis协议追加保存到后缀为aof的文件末尾。在redis服务器重启时,会读取并加载aof文件,达到恢复数据的目的。aof持久化方式redis是默认不开启的,redis重启后会默认先加载aof恢复数据。AOF默认支持三种持久化配置(参见redis.conf)。
AOF通过日志追加的方式存储redis的写操作,当针对key进行多次写操作时,会产生针对这个key操作的日志指令,导致aof文件过大,恢复数据会变的比较慢,此时可以通过重写aof文件,可手动执行指令bgwriteaof,或修改redis.conf。
持久化过程:
aof重写:
AOF文件内容分析:
# 设置redis.conf配置中aof开启,且同步设置为appendfsync everysec
192.168.72.129:6379> set k1 v1 # 向redis中添加key值,此时会生成一个appendonly.aof文件
OK
# aof文件内容
*3 # 当前操作语句的字段值
$3 # redis命令(set)的字符长度
set
$2 # redis命令中key(k1)的字符长度
k1
$2 # redis命令中value(v1)的字符长度
v1
- RDB和AOF比较
4.3. Redis缓存过期策略和淘汰机制
- 过期策略
- 定时删除:设置某个key的过期时间后,我们创建一个定时器,让定时器在过期时间到来时,立即执行对key的删除操作。
优点:定时删除是对内存最友好的,能够保证内存中的key一旦过期立马就会被删除。
缺点:对CPU不友好,过期键比较多的时候,删除过期键会占用部分cup,对服务器的响应造成影响 - 定期删除:每隔一段时间对缓存的key进行检查,删除过期的key。Redis默认每隔100ms随机抽取部分设置了过期时间的key,检查这些key是否过期,过期则删除(参见redis.conf)。
优点:可以通过限制删除操作的执行频率来减少删除操作对cpu的影响,定期删除也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键长时间占用的内存没有及时释放的话,当我们再次获取这个过期的key时,依然会返回这个key的值,就相当于这个过期时间是无效的了。
# The range is between 1 and 500, however a value over 100 is usually not # a good idea. Most users should use the default of 10 and raise this up to # 100 only in environments where very low latency is required. hz 10 # redis.conf中默认配置- 惰性删除:设置key的过期时间后,不去管他。当需要该key时,再检查其是否过期,如果过期就删除,未过期则返回。
优点:对CPU友好,只会在使用该键时才进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,如果一个已经过期的key一直未被使用,那该键就一直存在内存中,如果数据库中有很多使用不到的过期键,这些键便永远不会被删除,内存永远不会释放,从而造成内存泄漏。所以redis还引入了另一种内存淘汰机制。
- 定时删除:设置某个key的过期时间后,我们创建一个定时器,让定时器在过期时间到来时,立即执行对key的删除操作。
- 淘汰机制
Redis使用“定期删除+惰性删除”只能保证最终一定被删除,若遗漏大量过期的key,而这些key也长时间未被使用,这些key便会一直存在于内存,最终导致内存耗尽。因此Redis引入了“内存淘汰机制”,内存淘汰机制能保证在redis内存占用过高的时候,去进行内存淘汰,也就是删除一部分key,保证redis的内存占用率不会过高。- no-eviction: 当内存不足时,新写入的数据会报错,无法写入数据(redis默认,一般不采用)
- allkeys-lru: 当内存不足时,删除最近最少使用的key
- allkeys-random: 当内存不足时,随机删除key
- allkeys-lfu: 当内存不足时,移除最少使用的key
- volatile-lru: 当内存不足时,在设置了过期时间的key中,删除最近最少使用的key
- volatile-random: 当内存不足时,在设置了过期时间的key中,随机删除key
- volatile-lfu: 当内存不足时,在设置了过期时间的key中,删除最少使用的key
- volatile-ttl: 当内存不足时,在设置了过期时间的key中,删除过期时间最早的key
maxmemory <bytes> # 当配置了redis最大内存时,超过了该配置时,会执行maxmemory-policy中的淘汰机制 maxmemory-policy noeviction # redis默认淘汰机制,一般使用allkeys-lru - 一些场景下对过期key的处理
- 生成RDB: 过期的key不会保存在RDB文件
- 服务器重载RDB: master载入时会忽略过期key,slave会载入所有,主从同步时,slave再和master保持一致
- AOF写入: AOF保存的是执行命令,如果redis未执行del命令,AOF也不会保存del操作,当过期key被删除时,del命令会被保存在AOF文件中
- 重写AOF: 执行bgrewriteaof时,过期key不会被记录
- 主从同步: master删除key时,会向slave发送一个del命令,slave收到命令后会删除这些key(slave节点读取过期key时,不会怕判断删除操作,继续返回值,只有master发出del命令后,slave才会删除过期key)
五. Redis集群
5.1. 主从模式
主从其实包含一个主节点,一个或多个从节点。从节点从主节点复制数据,可以实现读写分离,主节点写数据,从节点读数据。
开启两台虚拟机。
- 启动两个虚拟机ubuntu中redis作为主节点,启动redis服务
# ubuntu主节点
root@ubuntu:./redis-server ../redis-6.0.16/redis.conf # 启动主节点server
root@ubuntu:/usr/local/redis/bin# ./redis-cli -h 192.168.72.128 -p 6379 # 主节点客户端
#############################################
# 查看主节点信息
192.168.72.128:6379> info replication
# Replication
role:master # 当前节点角色(主节点)
connected_slaves:2 # 连接的从节点个数
slave0:ip=192.168.72.129,port=6379,state=online,offset=756,lag=0 # 从节点1信息
slave1:ip=192.168.72.129,port=6380,state=online,offset=756,lag=0 # 从节点2信息
master_replid:b27109417f02262ba0f358b2e691d01df290dfa0
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:756
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:756
#############################################
# centos启动两个从节点
# 修改redis.conf和redis1.conf中配置
# replicaof 192.168.72.128 6379 主节点ip及端口
# masterauth ***** 如果主节点开启requirepass,则从节点配置文件需要添加主节点密码
[root@localhost bin]# ./redis-server ../redis-6.2.7/redis.conf # 从节点1启动
[root@localhost bin]# ./redis-server ../redis-6.2.7/redis1.conf # 从节点2启动
# 启动从节点客户端
[root@localhost bin]# ./redis-cli -h 192.168.72.129 -p 6379
[root@localhost bin]# ./redis-cli -h 192.168.72.129 -p 6380
#############################################
# 从节点1
192.168.72.129:6379> info replication
# Replication
role:slave # 节点角色(从节点)
master_host:192.168.72.128 # 所属主节点ip
master_port:6379 # 所属主节点ip
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:854
slave_repl_offset:854
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:b27109417f02262ba0f358b2e691d01df290dfa0
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:854
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:854
#############################################
# 从节点2
192.168.72.129:6380> info replication
# Replication
role:slave # 节点角色(从节点)
master_host:192.168.72.128 # 所属主节点ip
master_port:6379 # 所属主节点ip
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:840
slave_repl_offset:840
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:b27109417f02262ba0f358b2e691d01df290dfa0
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:840
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:840
主从数据同步
从节点发起sync或psync命令;主节点执行bgsave生成rdb快照(同时在缓存去记录从现在的所有写命令);rdb生成后,主节点发送给从节点;从节点加载rdb文件,从节点更新同步状态为主节点执行bgsave时的状态;主节点将缓冲区的命令同步给从节点,从节点执行这些写命令,并更新同步状态为主节点最新状态。
- 优点:读写分离,主节点的数据会复制给从节点
- 缺点:主节点宕机,导致部分数据未同步;不具备容灾和恢复功能
5.2. 哨兵(Sentinel)模式
哨兵模式引入了一个Sentinel系统去监控主服务器及其所属的所有从服务器,主节点宕机后,会根据选举算法选取从节点升级为主节点。Sentinel也可以配置一个或多个Sentinel来达到高可用,多个Sentinel之间互相监控。
- 优点:哨兵模式可以算主从模式的升级吧,主从的优点都有,而且哨兵还有监控的功能,当主节点宕机之后哨兵会推选一个健康的从节点做主节点,这样提高了软件的可用性
- 缺点:主从模式的缺点都有,而且配置哨兵啥的比较麻烦,而且在重新选举主节点期间,无法确定主从,无法工作;集群容量达到上限时在线扩容复杂
Sentinel工作方式
- 每个哨兵进程以每秒钟一次的频率向整个集群的节点以及其他sentinel发出一个ping命令
- 如果一个实例距离最后一次有效回复ping的时间超过down-after-millseconds指定的值,则这个实例会被sentinel主观下线(SDOWN)
- 如果一个msater被标记为SDOWN,则正在监视这个master的其他sentinel要以每秒一次的频率确认该master的确进入了主观下线状态
- 当足够多的sentinel在指定时间范围内认为master主观下线状态,则master会被标记为客观下线(ODOWN)
- 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
- 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
5.3. Cluster
Redis3.0之后加入了Cluster模式,实现redis的分布式存储,每个redis节点存储不同的数据。redis-cluster采用无中心结构:所有redis节点彼此互联,内部使用二进制协议;集群中的超过半数检测失效时,认为节点fail;客户端与redis直连,不需要连接所有节点,连接其中一个节点即可。
Redis集群工作方式
- slot: 插槽,redis-cluster会将整个数据库分为16384个slot,数据库中的每个键都属于中16384个slot其中的一个,集群中的每个节点可以是0个或者16384个slot
- cluster: 集群管理的插件
优缺点
优点:无中心架构;数据按多个slot存储分布在多个节点,节点间数据共享,可动态调整数据分布;可扩展性,可在线扩展,动态添加或删除;高可用,部分节点不可用,集群仍可使用 缺点:数据异步复制,不保证数据的一致性;无法区分冷热数据,资源隔离性差,容易出现互相影响;slave在集群中充当“冷备”角色,不能缓解读压力;不支持多数据库,单机下可以使用16个库,集群下只能是用db0
六. 缓存雪崩/缓存穿透/缓存击穿
Redis因其高速读写的特性,常常被作为后台数据库的缓存,缓存一些热点儿数据,通常情况下,缓存能处理大部分请求。当缓存未命中有效数据时,会去后台访问数据库。然而当Redis作为缓存时,存在一些应用问题,如:缓存雪崩,缓存穿透,缓存击穿。
- 缓存雪崩:
当某一时刻发生大规模的缓存失效的情况,如服务器宕机,大量key在同一时间过期,这样导致的后果就是大量的请求会直接打到DB上,可能导致整个系统的崩溃,这种称为缓存雪崩。如果重启宕机的数据库,马上又会有大量新的请求流量到来,再次引起数据库宕机。
- 可能的原因:服务器宕机,大量key在同一时间失效
- 解决办法:引入随机性,对缓存key的失效加上一个随机时间,避免大量数据在同一时间失效;通过请求限流,熔断机制,服务降级等手段降低服务的负载;实现缓存组件的高可用,防止单点故障,机器宕机等情况
- 缓存穿透:
当有大量的查询请求未命中缓存时,引起数据库的频繁访问,导致数据库负载压力过大,这种现象就叫缓存穿透。
- 可能原因:大量访问不存在的key,导致数据库请求压力过大
- 解决办法:访问鉴权;将无效的key存进redis,若查询数据库不存在的key时,将这个key缓存到redis中,value设置为null;若每次随机请求的key不一致时,该方法失效,依然会引起穿透问题;可以使用布隆过滤器(BloomFilter并不支持删除操作,只支持添加操作)
- 缓存击穿:
当Redis中存在极热热点数据时,即大量并发请求的key-value数据。当极热数据突然失效时,缓存未命中时,会引起瞬间造成数据库的频繁访问,这种现象叫缓存击穿。
- 可能原因:热点数据瞬间失效
- 解决办法:设置key永不失效;使用互斥锁,若缓存失效,只有获得锁才可以访问数据库,降低了同一时间数据库的访问量,防止数据库崩溃,但是会导致系统的性能变差。
七. 鸣谢
在此特别感谢Bilibili UP主:遇见狂神说,本篇文章参考狂神的redis教学视频(www.bilibili.com/video/BV1S5…