Redis
NoSQL
NoSQL = Not Only SQL
NoSQL特点
- 方便扩展(数据之间没有关系,很好扩展)
- 高性能
- 数据类型是多样性的
- 传统的 RDBMS和NoSQL对比
1 | 传统的 RDBMS |
1 | Nosql |
NoSQL的四大分类
| 分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
|---|---|---|---|---|---|
| key-value | Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载 | Key-Value对应的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
| 列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
| 文档型数据库 | CouchDB, MongoDb | Web应用 | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
| 图形数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,不容易做分布式集群方案 |
Redis入门
Redis(Remote Dictionary Server ),即远程字典服务,
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
Redis 能干嘛?
内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof)
效率高,可以用于高速缓存
发布订阅系统
地图信息分析
计时器、计数器(浏览量!)
特性
- 多样的数据类型
- 持久化
- 集群
- 事务
学习中需要用到的东西
- 狂神的公众号:狂神说
- 官网:https://redis.io/
- 中文网:http://www.redis.cn/
基础知识
redis默认有16个数据库,默认使用的是第0个
SELECT:切换数据库,DBSIZE查看当前db大小
1 | Redis:0>select 1 |
FLUSHDB:清除当前数据库
FLUSHALL:清除全部数据库的内容
Redis是单线程的
明白Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!所有就使用了单线程了!
Redis 是C 语言写的,官方提供的数据为 100000+ 的QPS,完全不比同样是使用 key-vale的Memecache差!
Redis 为什么单线程还这么快?
1、误区1:高性能的服务器一定是多线程的?
2、误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
先去CPU>内存>硬盘的速度要有所了解!
核心:redis 是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!
五大数据类型
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
Redis-Key
官方中文文档:http://www.redis.cn/commands.html
基本命令:
keyspattern( 使用 * 表示查询所有的key)delkey[key2 key3…] (删除键值对)movekey db(移动key到指定db)existskey (不存在返回0,存在返回1)typekey (返回value 的类型)
Strings(字符串)
介绍
set
设置或覆盖一个值:
SETkey value [EX seconds] [PX milliseconds] [NX|XX]1
2
3
4
5- EX *seconds* – 设置键key的过期时间,单位时秒
- PX *milliseconds* – 设置键key的过期时间,单位时毫秒
- NX – 只有键key不存在的时候才会设置key的值
- XX – 只有键key存在的时候才会设置key的值
注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。设置多个值:
msetkey value [key value …]设置values到对应keys上:
msetnxkey value [key value …],是个原子操作,要么一起成功(返回1),要么一起失败(返回0)
查
- 获取指定key的值:
getkey,只处理String类型的值 - 获取多个key的值:
mgetkey [key …] - 返回指定范围的value的子串:
getrangekey start end - 获取指定字符串的长度:
strlenkey
- 获取指定key的值:
存值并返回原值:
getsetkey value (get andthen set),如果原本不存在值,则返回null改
- 从指定的offset处开始,覆盖value的长度:
setrangekey offset value - 追加值:
appendkey value,如果当前key不存在,就相当于set key
- 从指定的offset处开始,覆盖value的长度:
使用示例
set、get、append:
1 | 127.0.0.1:6379> setnx name atomsk # 创建值 |
mset、mget、setnx、msetnx、getset:
1 | 127.0.0.1:6379> mset k1 v1 k2 v2 |
incr、decr和incrby、decrby(i++、i– 和 i+= 步长、 i-= 步长)
1 | 127.0.0.1:6379> set views 10 |
getrange字符串截取和setrange字符串替换
1 | 127.0.0.1:6379> set str abcde |
- 对象操作
1 | # 这里的key是一个巧妙的设计: user:{id}:{filed} , 如此设计在Redis中是完全OK了! |
使用Redis Desktop Manager查看:

Lists(列表)
- 介绍
- 增
Lpushlist value1[value2…]Rpushlist value1[value2…]Lpushxlist value,当list存在时才插入value到list表头Rpushxlist value,当list存在时才插入value到list表尾
- 查
- 查询出指定区间元素:
lrangelist start end (闭区间),示例:lrange list 0 -1(查询所有) - 通过索引获得值:
lindexlist index , 从0开始,从左到右,右边第一个索引-1 - 查列表的长度:
llenlist
- 查询出指定区间元素:
- 删
Lpoplist ,移除列表的第一个元素Rpoplist ,移除列表的最后一个元素lremlist count element,移除列表指定个数的指定元素,优先移除左边元素
- 改
ltramlist start end,通过下标截取list,会改变原来的listlsetlist index value,修改指定索引上的值,修改不存在list或者索引越界会报错linsertkey BEFORE|AFTER 位置 value,在指定位置前|后插入值RpopLpushsourceList targetList,Rpop出一个列表的元素,并将其Lpush到其他列表中!
使用示例
- Lpush、Rpush、lrange、lrem
1 | 127.0.0.1:6379> lpush list one two three |
- ltram、lset
1 | 127.0.0.1:6379> ltrim list 1 2 # 截取保留区间[1,2]的值 |
- linsert
1 | 127.0.0.1:6379> linsert list after one two |
小结
- 他实际上是一个链表, left,right,before|after Node ,都可以插入值
- 如果移除了所有值,空链表,也代表不存在!
- 在两边插入或者改动值,效率最高!改动中间元素,相对来说效率会低一点~
消息队列 (Lpush Rpop), 栈( Lpush Lpop)!
Sets(集合)
介绍
增
- 增加一个或多个元素成员:
saddset value[value2…],如果成员已存在则忽略
- 增加一个或多个元素成员:
查
- 查所有成员:
smembers - 随机抽选出一个或多个成员:
srandmemberset [count] - 判断是否是set的成员:
sismemberset value - 查成员数量:
scardset
- 查所有成员:
删
- 随机删除一个或多个成员:
spopset [count] - 删除指定成员:
sremset members[member1,member2…]
- 随机删除一个或多个成员:
改
- 移动指定成员到目标set:
smovesourceSet targetSet value
- 移动指定成员到目标set:
集合操作
- 差集:
Sdiffkey [key …],返回一个集合与给定集合的差集的元素. - 交集:
Sinterkey [key …],返回指定所有的集合的成员的交集. - 并集:
Sunionkey [key …],返回给定的多个集合的并集中的所有成员.
- 差集:
1 | 127.0.0.1:6379> sadd set1 a b c |
Hashes(哈希)
- Redis Hashes是字符串字段和字符串值之间的映射,可以看成具有String Key和对象数据(filed value)的Map容器
- 增
- 设置一个或多个值:
hsetkey field1 value1 [field2 value2 …],返回1是新增,0是修改 hmset和hset用法和作用一样
- 设置一个或多个值:
- 指定字段不存在时,设置值:
hsetnx - 查
- 查一个字段的值:
hgetkey field - 查多个字段的值:
hmgetkey field1 [field2 …] - 查出所有字段的名字:
hkeyskey - 查出所有字段的值:
hvalskey - 查所有的字段和值:
hgetallkey - 获取hash表的字段数量:
hlenkey - 获取指定字段的长度:
hstrlenkey field
- 查一个字段的值:
- 判断指定字段是否存在:
hexistskey field,存在返回1,不存在0 - 删除一个或多个字段:
hdelkey field1 [field2 …] - 增加|减少字段的值:
hincrby和hdecrbykey field value
使用示例
1 | 127.0.0.1:6379> hset user name atomsk age 22 |
hash变更的数据 user name age,尤其是是用户信息之类的,经常变动的信息! hash 更适合于对象的存储,String更加适合字符串存储!
Sorted sets(有序集合)
介绍
- 和集合的差别是,每个有序集合的成员都关联着一个评分(score),集合成员按score从低到高排序
- 一个数据(score,value) 其中score可重复,value不可重复
- 有趣用法:
- 在一个巨型在线游戏中建立一个排行榜,每当有新的记录产生时,使用ZADD 来更新它。你可以用ZRANGE轻松地获取排名靠前的用户, 你也可以提供一个用户名,然后用ZRANK获取他在排行榜中的名次。 同时使用ZRANK和ZRANGE你可以获得与指定用户有相同分数的用户名单。 所有这些操作都非常迅速。
- 有序集合通常用来索引存储在Redis中的数据。 例如:如果你有很多的hash来表示用户,那么你可以使用一个有序集合,这个集合的年龄字段用来当作评分,用户ID当作值。用ZRANGEBYSCORE可以简单快速地检索到给定年龄段的所有用户。
通用指令
将指定成员添加到ss里:
zaddkey [NX|XX] [CH] [INCR] score member [score member …]1
2
3
4XX: 仅仅更新存在的成员,不添加新成员。
NX: 不更新存在的成员。只添加新成员。
CH: 修改返回值为发生变化的成员总数,原始是返回新添加成员的总数 (CH 是 changed 的意思)。更改的元素是新添加的成员,已经存在的成员更新分数。 所以在命令中指定的成员有相同的分数将不被计算在内。注:在通常情况下,ZADD返回值只计算新添加成员的数量。
INCR: 当ZADD指定这个选项时,成员的操作就等同ZINCRBY命令,对成员的分数进行递增操作。返回指定索引范围的元素:
zrangekey start stop[withscores],从高到低:zrevrange返回成员数量:
zcardkey移除指定成员:
zremkey member [member …]
*score *分数相关指令
返回成员的score值:
zscorekey member按score从低到高排序:
zrangebyscorekey min max [withscores] [limit offset count],min和max指定socre的范围,使用limit进行分页,从高到低:zrevrangebyscore1
2
3##区间及无限
min和max可以是-inf和+inf,这样一来,你就可以在不知道有序集的最低和最高score值的情况下,使用ZRANGEBYSCORE这类命令。
默认情况下,区间的取值使用闭区间,你也可以通过给参数前增加 ( 符号来使用可选的开区间。获取指定score区间的成员数量:
zcountkey min max移除指定score区间的成员:
zremrangebyscorekey min max删除一个或多个score最高|最低的成员:
zpopmax|zpopminkey [count]
lex 相关指令(成员分数相同时才能使用):
- 返回指定成员区间内的成员,按成员字典正序排序:
zrangebylexkey min max [LIMIT offset count],按倒序:zrevrangebylex - 获取指定成员之间的成员数量:
zlexcountzset [member1 [member2 - 删除名称按字典由低到高排序成员之间所有成员:
zremrangebylexkey min max
- 返回指定成员区间内的成员,按成员字典正序排序:
rank 排名相关指令(score值最小的成员排名为0):
- 返回指定成员的排名,按从小到大排:
zrankkey member,从大到小排:zrevrank - 移除指定排名区间的成员:
zremrangebyrankkey start stop
- 返回指定成员的排名,按从小到大排:
集合操作
并集:
zunionstoredestination numkeys key [key …] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]1
2
3把numkeys个集合的并集放到destination中
weights,乘法因子,可选,有几个key就得有几个weight,让score在传递给聚合函数前相乘
aggregate,SUM,保留score为所有score之和,MAX|MIN,选择保留最大或者最小score交集:
zinterstore,用法和上面一样
使用示例
通用命令
1
2
3
4
5
6
7
8
9
10
11127.0.0.1:6379> zadd zset 10 one 20 two 30 three
(integer) 3
127.0.0.1:6379> zrange zset 1 2 withscores # 返回索引[1,2]的成员
1) "two"
2) "20"
3) "three"
4) "30"
127.0.0.1:6379> zcard zset
(integer) 3
127.0.0.1:6379> zrem zset one # 移除成员 one
(integer) 1score相关命令
lex相关命令
rank相关命令
集合操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14redis 127.0.0.1:6379> ZRANGE programmer 0 -1 WITHSCORES
1) "peter"
2) "2000"
redis 127.0.0.1:6379> ZRANGE manager 0 -1 WITHSCORES
1) "bob"
2) "4000"
# 公司决定加薪。。。除了程序员。。。
redis 127.0.0.1:6379> ZUNIONSTORE salary 2 programmer manager WEIGHTS 1 3
(integer) 2
redis 127.0.0.1:6379> ZRANGE salary 0 -1 WITHSCORES
1) "peter"
2) "2000"
3) "bob"
4) "12000"
特殊数据类型
Geospatial 地理位置
Hyperloglog 基数统计
Bitmap 位图
事务
Redis 事务:事务中的所有命令都会序列化、按顺序地执行。
Redis事务不保证原子性,原因:
- 有命令入队失败(如出现编译型异常)时,事务中所有命令都不会被执行,这种情况保证原子性,但是
- 命令存在语法问题(如 incr 一个非数值型字符串)但入队成功,在执行失败时,不会影响到其他命令的正常执行
事务执行流程:
使用
muliti开启事务执行命令,让命令进入事务队列
使用
exec执行事务或者
使用
discard清空事务队列, 并放弃执行事务。
WATCH
被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。
使用watch命令实现乐观锁:
1 | 127.0.0.1:6379> set money 100 |
取消对键的监控:
- 使用
unwatch手动取消对所有键的监视 - 执行
exec后, 不管事务是否成功执行, 对所有键的监视都会被取消。 - 执行
discard后
脚本和事务
从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。
Jedis
导入依赖
1 | <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> |
常用API
方法名和命令一样
https://gitee.com/Atomsk/practice/tree/redis/redis-01-jedis
事务
1 |
|
SpringBoot整合
SpringBoot 操作数据:spring-data jpa jdbc mongodb redis!
说明: 在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce?
jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接池! 更像 BIO 模式
lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像 NIO 模式
源码分析:
1 |
|
导入依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>配置
1
2=127.0.0.1
=6379测试使用
1
2
3
4
5
6
7
8
9
private RedisTemplate redisTemplate;
void testTemplate() {
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();
redisTemplate.opsForValue().set("k1", "v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
}
使用原生的RedisTemplate会有编码问题,因为使用的都是默认的序列化方式
所以自己配置一个RedisTemplate:
自定义RedisTemplate
1 |
|
然后把自动注入改成以下即可
1 |
|
RedisUtil
企业开发中,一般不使用原生RedisTemplate的API,会将自己自定义的Template再进行封装,使用Redis工具类:
配置详解
网络相关
1 | #bind 127.0.0.1 # 绑定的ip,使用docker时不配置 |
General
1 | daemonize no # 以守护进程的方式运行,如果在使用docker管理redis时开启,会导致redis启动失败,因为docker的redis默认没有pidfile文件 |
持久化相关
redis 是内存数据库,如果没有持久化,那么数据断电即失
主从复制
安全 Security
1 | 127.0.0.1:6379> config get requirepass # 获取redis的密码 |
限制 Clients
1 | maxclients 10000 # 设置能连接上redis的最大客户端的数量 |
持久化
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能!
RDB(Redis Database)
什么是RDB
默认的redis数据库,主从复制中的从机,即数据库备份
在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时将快照文件直接读到内存里。
- Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。
- 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,RDB方式要比AOF方式更加的高效。
- 默认的持久化方式就是RDB,一般情况下不需要修改
db保存的文件是 dump.rdb ,可以在配置文件中进行配置,有时候在生产环境我们会将这个文件进行备份!
RDB配置
1 | # 当存在最少一个key 变更时,900秒(15分钟)后保存到硬盘 |
触发机制
- save的规则满足的情况下,会自动触发rdb规则
- 执行 flushall 命令,也会触发我们的rdb规则!
- 退出redis,也会产生 rdb 文件!
机制触发后会自动生成一个 dump.rdb文件
恢复方式
只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!
查看需要存在的位置
1 | 127.0.0.1:6379> config get dir |
优点和缺点
优点:
- 适合大规模的数据恢复!
- 对数据的完整性要求不高!
缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机了,最后一次的修改数据就没有的了!
- fork进程的时候,会占用一定的内容空间!
AOF(Append Only File)
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
Aof保存的是 appendonly.aof 文件
AOF配置
AOF默认是不开启的,把 appendonly 改为yes就开启了 aof,重启 redis 就可以生效了,可以和RDB方式一起使用
1 | appendonly no # 默认不开启 |
持久化策略
appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能差但数据完整性比较好
appendfsync everysec ,默认选项,异步操作,每秒记录,如果一秒钟内宕机,会有数据丢失
appendfsync no 将缓存回写的策略交给系统,linux 默认是30秒将缓冲区的数据回写硬盘的
重写规则
AOF采用文件追加的方式持久化数据,所以文件会越来越大,为了避免这种情况发生,增加了重写机制
当AOF文件的大小超过了配置所设置的阙值时,Redis就会启动AOF文件压缩,只保留可以恢复数据的最小指令集
触发机制:Redis会记录上次重写时的AOF文件大小,默认配置时当目前aof文件大小超过上一次重写的aof文件大小的百分之 100 ,且文件大于 64M 时触发
1 | # 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间 |
AOF文件损坏修复方法
如果 aof 文件有错位,redis 会启动失败,可以使用 redis-check-aof --fix aoffile 进行修复
和RDB相比
优点:通常情况下AOF文件保存的数据比RDB文件保存的数据要完整
缺点:
- 相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
- Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化!
小结
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这条规则 - 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
- 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。
- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留
发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、
微博、关注系统!
Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图:
第一个:消息发送者, 第二个:频道 第三个:消息订阅者!
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
测试
订阅端:
1 | 127.0.0.1:6379> subscribe atomsk |
发送端:
1 | 127.0.0.1:6379> publish atomsk "ground controll to major tom" |
原理
Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。
Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。
微信公众号:
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 频道!,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。
通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景:
- 实时消息系统!
- 事实聊天!(频道当做聊天室,将信息回显给所有人即可!)
- 订阅,关注系统都是可以的!
稍微复杂的场景我们就会使用 消息中间件 MQ
主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);
数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主。
*默认情况下,每台Redis服务器都是主节点 *
一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:
- 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
- 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有
内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过 20G。
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是”多读少写“。
对于这种场景,我们可以使如下这种架构:
主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用!
另外,slave服务器也可以有自己的slave服务器,这样的服务器称为sub-slave,而这些sub-slave通过主从复制最终数据也能与master保持一致,如下图所示:
只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!
Replication配置
默认的replication配置:
1 | # 指导 slave 通过指定ip和端口号的 master 来获取DB数据 |
只需修改从机的以下配置:主库的ip和端口号,日志和rdb文件的名字
1 | replicaof <masterip> <masterport> |
修改完成后使用主机测试:
1 | 127.0.0.1:6379> info replication # 查看当前库的信息 |
复制原理
复制方式
Redis主从复制分为以下三种方式:
- 增量复制:正常连接时,master会发生数据命令流给salve,将自身数据的改变复制到slave
- 部分复制:当连接断开后,slave在重连时会尝试重新获取断开后未同步的数据
- 全量复制:如果无法部分同步,则会请求进行全量复制,master将自己的rdb文件发送给slave,并记录同步期间的其他写入,再发送给slave,以达到完全同步的目的
工作原理
master会记录一个replicationId的伪随机字符串,用于标识当前的数据集版本,还会记录一个当数据集的偏移量offset,不管master是否有slave服务器,replication Id和offset会一直记录并成对存在,我们可以通过 info replication查看
1 | ··· |
当master与slave正常连接时,slave使用fsyn命令向master发送自己记录的旧master的replication id和offset,而master会计算与slave之间的数据偏移量,并将缓冲区中的偏移数量同步到slave,此时master和slave的数据一致。
而如果slave引用的replication太旧了,master与slave之间的数据差异太大,则master与slave之间会使用全量复制的进行数据同步。
其他细节
故障转移
如果主机宕机了,会根据replic-priority的值将优先级高的从机选举为主机——哨兵模式
或者,(不推荐)我们可以使用 SLAVEOF no one把从机设置成主机,再使用命令将其他节点手动连接到最新的这个主节点,如果后面默认的主机恢复了,那就重启服务,恢复到配置文件的主从配置。
主从复制中的key过期问题
slave服务器没有权限处理过期的key,这样的话,对于在master上过期的key,在slave服务器就可能被读取,所以master会累积过期的key,积累一定的量之后,发送del命令到slave,删除slave上的key。
哨兵模式
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。
哨兵模式能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
测试示例
配置哨兵配置文件 sentinel.conf
1
2# sentinel monitor 被监控的名称 host port 进行投票时所需 认为主机已宕机的 哨兵数量
sentinel monitor myredis 127.0.0.1 6379 11表示只要有一个哨兵认为主机宕机了,就进行投票选举
可能用到的docker命令
1 | docker run -d -p 6379:6379 --name redis -v /home/atomsk/redis/redis.conf:/etc/redis/redis.conf -v /home/atomsk/redis/data:/data redis redis-server /etc/redis/redis.conf |
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
- Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
sentinel.conf详解
1 | # Example sentinel.conf |
缓存穿透和雪崩
缓存穿透
(查不到)
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿
(量太大,缓存过期!)
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大
缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)
限流降级(在SpringCloud讲解过!)
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。