一、学习安排
- nosql讲解
- 阿里巴巴架构演进
- nosql数据模型
- nosql四大分类
- CAP
- BASE
- Redis入门
- Redis安装(Windows&Linux)
- 五大基本数据类型
- String
- List
- Set
- Hash
- Zset
- 三种特殊数据类型
- geo
- hyperloglog
- bitmap
- Redis配置详解
- Redis持久化
- RDB
- AOF
- Redis事务操作
- Redis实现订阅发布(消息队列)
- Redis主从复制
- Redis哨兵模式
- 缓存穿透及解决方案
- 缓存击穿及解决方案
- 缓存雪崩及解决方案
- 基础API之Jedis详解
- SpringBoot集成Redis
- Redis实践分析
二、Nosql概述
2.1 Nosql的引出
1. 技术演进
1. 单机Mysql
最早的网站:
一个网站基本访问量不会太大,单个数据库完全足够。更多是使用静态网站,服务器没有压力。整个网站的瓶颈:
- 数据量如果太大,一台机器放不下
- 数据的索引,单标超过300W一定要建立索引,索引(B+树)太多一个机器也放不下
- 访问量(读写混合),单个服务器承受不了
只要开始出现以上情况,就必须要升级。
2. 缓存(Memcache)+ Mysql + 垂直拆分(读写分离)
网站80%的操作都是读操作,如果每次都去查数据库的话就十分麻烦,只要数据不变的情况下,查询结果会进行缓存,下次再查就直接使用缓存,而不用走数据库。
发展过程:优化数据结构和索引:arrow_forward:文件缓存(IO):arrow_forward:Memcache
3. 分库分表 + 水平拆分(Mysql集群)
本质:数据库就是读和写两个操作,最早读写都在单机,使用缓存解决了读的问题,因此这里要解决写的问题。
写问题的解决方案在早些年通过更改数据库表物理引擎一定程度解决写问题:
- MyISAM(表锁):查一行,锁整个表,影响效率
- Innodb(行锁):查一行,锁一行
慢慢的,就开始使用分库分表来解决写的压力,使用集群将整个表的数据拆分到不同的集群数据库中,来解决写的问题。Mysql推出了表分区,但是并没有火起来,然后推出Mysql集群,很好满足了当年的需求。
现如今
Mysql这种关系型数据库不够用了,因为数据量大,变化快。于是出来一些新型的数据库:
- 图形数据库
- BSON:(MongoDB使用),理解为就是2进制的Json
2. 为什么要用Nosql
用户的个人信息,社交网络,地理位置,用户自己产生的数据,用户日志等等爆发式增长,这时候我们就需要使用Nosql数据库,可以很好处理以上的情况。
Nosql
Nosql == Not Only SQL
泛指非关系型数据库,随着web2.0互联网诞生,传统的关系型数据库很难应对,尤其是超大规模、高并发的社区。
关系型数据库
表格、行、列
非关系型数据库
Map<String, Object>使用键值对来控制,Redis
3. Nosql特点
- 方便扩展,数据之间没有关系,很好扩展
- 大数据量,高性能(Redis一秒写8w次,读11w次,Nosql缓存记录级,细粒度,性能高)
- 数据类型多样(五大基本、三种特殊,不需要实现设计数据库,随取随用)
- 传统RDBMS和Nosql区别
- RDBMS
- 结构化组织
- SQL
- 数据和关系存在单独表中
- 数据定义语言
- 严格的一致性
- 基础事务操作
- Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对、列存储、文档存储、图形存储
- 最终一致性
- CAP定理、BASE理论(异地多活)
- 高性能、高可用、高可扩展
了解大数据时代的3v:
- 海量(Volume)
- 多样(Variety)
- 实时(Velocity)
3高:
- 高并发
- 高可扩(随时可以水平拆分,加机器)
- 高性能(保证用户体验和性能)
三、Nosql的四大分类
3.1 四大分类使用情况
KV键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度:Redis + memcache
文档型数据库BSON
- MongoDB(一般必须要掌握)
- 是一个基于分布式文件存储的数据库
- 一般用来处于大量文档,c++编写
- MongoDB是介于关系型数据库和非关系型数据库,是Nosql数据库中功能最丰富,最像关系型数据库的
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
- 不是存图片,是存关系,朋友圈、社交网路、广告推荐
- Neo4j,InfoGrid
3.2 四大类型对比
| 分类 | Examples | 典型应用场景 | 数据模型 | 优点 | 缺点 |
|---|---|---|---|---|---|
| KV | Redis、Oracle | 内容缓存,用于处理大量数据高访问负载 | HashTable | 查找速度快 | 数据无结构化 |
| 列存储数据库 | HBase | 分布式文件系统 | 以列簇式存储,将同一系列数据存在一起 | 查找快,可扩展性强,易分布式扩展 | 功能相对局限 |
| 文档型数据库 | MongoDB | web应用,与KV类似,数据库能够了解Value的内容 | Value为结构化数据 | 表结构可变,不需要像sql一样预先定义表结构 | 查询性能不高,缺乏统一查询语法 |
| Graph数据库 | Neo4J、InfoGrid | 社交网络,推荐系统,专注于构建关系图谱 | 图结构 | 利用图结构相关算法,最短路径寻址、N度关系查找 | 对整个图计算才能得出信息,不太好做分布式 |
四、Redis入门
4.1 概述
1. Redis是什么
Remote Dictionary Server,远程字典服务,是一个开源使用的C语言编写、支持网络、可基于内存亦可持久化的日志型、KV数据库,并提供多种语言的API。Redis会周期性的把更新的数据写入磁盘或者追加记录文件,并且再次基础上实现主从同步
2. Redis能干什么
- 内存存储、持久化(RDB、AOF)
- 效率高,用于告诉缓存
- 发布订阅系统
- 地图信息分析
- 计时器、计数器、浏览量
3. 特性
- 开源
- 支持多数据类型
- 持久化
- 支持集群、事务
4.2 安装
1. Windows
2. Linux
- 解压
mv redis-6.2.6.tar.gz /opt
tar -zxvf redis-6.2.6.tar.gz
- 安装文件
yum install gcc-c++
make
make install
redis默认的路径:
/usr/local/bin
- redis-server:服务器
- redis-cli:客户端
- redis-sentinel:哨兵
- 备份redis默认配置文件
mkdir nick-config
cp /opt/redis-6.2.6/redis.conf nick-config/
3. Linux开放端口设置
- 开启防火墙
# 开启防火墙
systemctl start firewalld
# 查看防火墙状态
firewall-cmd --state
#设置开机自启:
systemctl enable firewalld.service
#重启防火墙:
systemctl restart firewalld.service
# 关闭防火墙
systemctl stop firewalld
# 对外开放端口
firewall-cmd --zone=public --add-port=xxxx/tcp --permanent
4.3 使用
- 修改配置
Redis默认不是后台启动的,修改配置文件
daemonize yes
- 启动服务
redis-server nick-config/redis.conf
使用nick-config/redis.conf这个配置文件启动redis-server
- 测试使用
set key value:存储键值对
get key:获取key对应的值
keys *:查看所有的key
- 查看Redis进程是否开启
ps -ef|grep redis
- 如何关闭redis服务
使用SHUTDOWN断开连接,使用exit退出Redis
4.4 性能测试工具
redis-benchmark是一个压力测试工具,官方自带,相关参数如下:
| 序号 | 选项 | 描述 | 默认值 |
|---|---|---|---|
| 1 | -h | 指定服务器主机名 | 127.0.0.1 |
| 2 | -p | 指定服务器端口 | 6379 |
| 3 | -s | 指定服务器socket | |
| 4 | -c | 指定并发连接数 | 50 |
| 5 | -n | 指定请求数 | 10000 |
| 6 | -d | 以字节形式指定SET/GET值的数据大小 | 2 |
| 7 | -k | 1=keep alive; 0=reconnect | 1 |
| 8 | -r | SET/GET/INCR使用随机key,SADD使用随机值 | |
| 9 | -P | 通过管道传输<numreq>请求 | 1 |
| 10 | -q | 强制退出redis,仅显示query/sec值 | |
| 11 | --csv | 以CSV格式输出 | |
| 12 | -l | 生成虚幻,永久执行测试 | |
| 13 | -t | 仅运行以逗号分隔的测试命令列表 |
- 简单测试
# 测试100个并发连接 每个并发100000个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
# 结果
====== PING_INLINE ======
100000 requests completed in 1.69 seconds
100 parallel clients
3 bytes payload
keep alive: 1
host configuration "save": 3600 1 300 100 60 10000
host configuration "appendonly": no
multi-thread: no
Latency by percentile distribution:
0.000% <= 0.583 milliseconds (cumulative count 6)
50.000% <= 1.143 milliseconds (cumulative count 50354)
75.000% <= 1.375 milliseconds (cumulative count 75131)
87.500% <= 1.559 milliseconds (cumulative count 87698)
93.750% <= 1.815 milliseconds (cumulative count 93752)
96.875% <= 2.175 milliseconds (cumulative count 96884)
98.438% <= 2.727 milliseconds (cumulative count 98445)
99.219% <= 3.599 milliseconds (cumulative count 99220)
99.609% <= 4.191 milliseconds (cumulative count 99610)
99.805% <= 4.775 milliseconds (cumulative count 99805)
99.902% <= 5.175 milliseconds (cumulative count 99904)
99.951% <= 5.463 milliseconds (cumulative count 99952)
99.976% <= 5.695 milliseconds (cumulative count 99976)
99.988% <= 5.807 milliseconds (cumulative count 99988)
99.994% <= 5.871 milliseconds (cumulative count 99994)
99.997% <= 5.895 milliseconds (cumulative count 99997)
99.998% <= 5.911 milliseconds (cumulative count 99999)
99.999% <= 5.935 milliseconds (cumulative count 100000)
100.000% <= 5.935 milliseconds (cumulative count 100000)
Cumulative distribution of latencies:
0.000% <= 0.103 milliseconds (cumulative count 0)
0.298% <= 0.607 milliseconds (cumulative count 298)
5.758% <= 0.703 milliseconds (cumulative count 5758)
15.045% <= 0.807 milliseconds (cumulative count 15045)
24.759% <= 0.903 milliseconds (cumulative count 24759)
35.787% <= 1.007 milliseconds (cumulative count 35787)
46.081% <= 1.103 milliseconds (cumulative count 46081)
57.214% <= 1.207 milliseconds (cumulative count 57214)
67.484% <= 1.303 milliseconds (cumulative count 67484)
78.315% <= 1.407 milliseconds (cumulative count 78315)
85.053% <= 1.503 milliseconds (cumulative count 85053)
89.419% <= 1.607 milliseconds (cumulative count 89419)
91.861% <= 1.703 milliseconds (cumulative count 91861)
93.632% <= 1.807 milliseconds (cumulative count 93632)
94.852% <= 1.903 milliseconds (cumulative count 94852)
95.809% <= 2.007 milliseconds (cumulative count 95809)
96.463% <= 2.103 milliseconds (cumulative count 96463)
98.816% <= 3.103 milliseconds (cumulative count 98816)
99.557% <= 4.103 milliseconds (cumulative count 99557)
99.889% <= 5.103 milliseconds (cumulative count 99889)
100.000% <= 6.103 milliseconds (cumulative count 100000)
Summary:
throughput summary: 59136.61 requests per second
latency summary (msec):
avg min p50 p95 p99 max
1.207 0.576 1.143 1.919 3.319 5.935
// ......
- 如何查看
SET方法测试十万个请求,使用==1.66==秒,有==100==个并行的客户端,每次写入==3==个字符串,只有一台服务器处理(单机性能)
在==0.591==好眠秒时处理了7个写操作,在==4.175==毫秒的时候处理完成。
4.5 Redis基础知识
- Redis默认有16个数据库
- 默认使用第0个,可以使用
select进行数据库切换
DBSIZE:查看当前数据库使用大小
flushdb:清空当前数据库flushall:清空全部
- 端口号:6379
- Redis是单线程
Redis是很快地,是基于内存操作的,CPU不是性能瓶颈,Redis的瓶颈是机器的内存和网络带宽,因此可以使用单线程。
为什么Redis单线程还真么快?
误区1:高性能服务器一定是多线程的(不一定,Redis)
误区2:多线程一定比单线程快(CPU切换上下文)
核心:Redis将所有数据放入内存中,所以单线程效率就是最高的,对于内存系统来说,没有上下文切换效率就是最高的。多次读写都是在一个CPU上。
五、数据类型
5.1 五大数据类型
什么是五大基础数据类型:
Redis可以做数据库、缓存、消息中间件MQ,支持多种数据类型:
- 字符串(strings)
- 散列(hashes)
- 列表(lists)
- 集合(sets)
- 有序集合(sorted sets)
与范围查询,bitmaps、huperloglogs和地理空间(geospatial)索引半径查询。Redis内置了:
- 复制(replication)
- LUA脚本
- LRU驱动时事件
- 事务
- 不容级别的磁盘持久化
并通过**哨兵(Sentinel)和自动分区(Cluster)**提供高可用性
1. redis-key
相关指定:
exists keymove key dbexpire key secttl keytype keysetex key sec valuesetnx key value
- 判断key是否存在
- 移动key
- 设置key过期时间
expire key sec将某个存在的KV设置过期时间
setex key sec valueset with expire:SET一个带过期时间的KV
- 如果存在则设置
setnx key valueset if not exist:如果不存在就进行设置,在分布式锁中常用
- 查看key中值的类型
- 批量设置、获取值
mset key value [key value...]
mget key [key...]可以搭配使用:
msetnx k1 v1 k4 v4,由于k1已经存在,k4的设置会失败,原子操作
- 设置对象
msetnx user:1:name nick user:1:age 28实际上就相当远
user:{id}:{filed}是一个复杂的key
- 组合命令
getset key value先get操作,然后执行set操作,返回get结果(CAS)
2. String字符串类型
使用场景:处理字符串,还可以使用数字(计数器、统计多单位数量、粉丝数、对象缓存)
- 字符串拼接
命令:
append key value如果当前key不存在,则相当于
SET
- 获取字符串长度
命令:
strlen key
- 自增自减操作
命令:
incr keys自增加一
命令:
decr keys自减一
命令:
incrby keys increment带步长自增
命令:
decrby keys increment带步长自减
- 字符串截取
命令:
getrange key start end前闭后闭,-1表示到最后
- 字符串替换
命令:
setrange key offset value
3. List类型
- 实际上是一个链表(before|node|after)
- 如果key不存在,创建新的列表
- 如果key存在,新增内容
- 在两边插入或者改动值效率最高,中间元素操作效率相对低一点
在Redis里面,可以将list当做队列和栈使用,所有的List命令都是以L开头的
- 添加、查看元素操作
LPUSH:从左添加元素到List
LRANGE:从左获取List开始到结束位置的元素
RPUSH:从右添加元素到List
- 移除元素
LPOP
RPOP
- 移除指定的值
LREM key count element从key中移除几个指定的值(List可以有重复的值)
- 获取下标元素
LINDEX:获取下标元素
- 查看List长度
LLEN
- list截断
LTRIM key start end前闭后闭
- 修改某一个值
LSET key index value如果
index不存在,就会报错index out of range可以使用
EXISTS key查看list是否存在,如果不存在也会报错ERR no such key
- 前插、后插
LINSERT key BEFORE|AFTER pivot value在key中pivot的前或者后插入value
- 复杂操作
rpoplpush source destination:从source右侧取一个值放入destination左侧
4. Set类型
集合,set中的值不能重复,
- 创建和查看
SADD
SMEMBERS
- 判断元素是否在set中
SISMEMBER
- 获取元素个数
SCARD
- 移除元素
SREM
- 获取随机元素
SRANDMEMVER
- 删除随机元素
SPOP
- 复杂操作
SMOVE source destination member将source中的member移动到destination中
- 集合操作
差集:
SDIFF交集:
SINTER并集:
SUNION应用:社交软件中所有用户放入set中,将其粉丝也放入set中,共同关注,共同爱好,推荐好友
5. Hash类型
哈希,可以想象成Map集合,存的是Key-Map,KV中的value是Map集合
- 创建与获取
HSET key field value
- 设置获取多个值
HMSET key field value [field value...]
HMGET key field [field...]
HGETALL key
- 删除
HDEL key field
- 获取集合长度
HLEN
- 判断是否存在
HEXISTS
- 只获得所有field、value
HKEYS
HVALS
- field对应value自增自减
HINCRBY
6. Zset类型
有序集合,在set基础上,增加一个值用于排序
应用:带权重进行判断,排行榜应用,取top10
- 创建和查看
ZADD key score value
ZRANGE
- 排序
ZRANGEBYSCORE key start end [WITHSCORES]从小到大排序
ZREVRANGE从大到小排序
- 移除
ZREM key value
- 查看元素个数
ZCARD
- 统计指定区间的成员数量
ZCOUNT key min max
5.2 三种特殊数据类型
1. geospatial地理位置
可以推算地理位置信息,两地之间的距离,方圆几里的人
附近的人、打车距离计算、朋友定位等
指令:
GEOADDGEODISTGEOHASHGEOPOSGEORADIUSGEORADIOSBYMEMBER
GEOADD添加地理位置
指令:GEOADD key longitude latitude member
GEOPOS获取地理位置
指令:GEOPOS key member
GEODIST获取两定位距离
指令:GEODIST key member1 member2 [m|km|ft|mi]
GEORADIUS获取附近的成员
指令:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD|WITHDIST] [COUNT count]
说明:以给定的经纬度为中心,找出某个半径内的元素
WITHCOORD:显示坐标WITHDIST:显示距离
COUNT:显示个数
GEORADIUSBYMEMBER获取成员附近的成员
指令:GEORADIUSBYMEMBER key member radius [m|km|ft|mi]
GEOHASH返回一个或多个位置元素的GEOHASH表示
指令:GEOHASH key member
该命令返回11个字符的GEOHASH字符串
GEO底层实际上是使用ZSET实现
2. hyperloglog
什么是基数?就是不重复的元素,如果数据量特别大的时候可以接受误差
A{1,3,5,7,8,9,7}基数为6
B{1,3,5,7,8}基数为5
hyperloglog是在redis2.8.9更新的数据结构,用于做基数统计的算法,相关业务:统计访问网页的UV,一个人访问多次还是算一个人。
传统方式使用Set保存用户的ID,因为Set不允许重复,因此可以统计数量。如果这种方式保存大量的用户ID就会麻烦,目的并不是为了保存ID,而是统计个数,但是SET占大量的空间。
hyperloglog的优点:
- 占用内存固定,2^64的元素只需要使用12KB内存
- 0.81%的错误率,对于统计UV的业务可以忽略不计
添加元素与统计个数
指令:
-
PFADD key value[value...] -
PFCOUNT key
合并统计不重复元素
指令:PFMERGE distination source1 source2
3. bitmap
按照位存储,相关应用有:
- 统计疫情感染人数,
00001则表示最后一个人被感染了- 统计活跃用户,Github活跃度
只有两个状态的都可以使用bitmaps
Bitmap是一个数据结构,位图,操作二进制位来进行记录,一年数据365天占用365bit,46个字节即可。
创建和查看
指令:
SETBIT key offset valueGETBIT key offset
上图表示周一到周五员工打卡情况,周三(offset为2)没有打卡
统计为1个数
指令:BITCOUNT key start end
上图表示员工工作五天中有四天完成打卡
六、Redis基本事务操作
事务要保证ACID原则,Redis事务本质是一组命令的集合,事务中命令会被序列化,在执行过程中,会按照顺序执行。
Redis事务没有隔离级别的概念,所有的命令在事务中并没有直接被执行,只有发起执行命令时才会执行。EXEC
Redis单条命令保证原子性,但是事务不保证原子性
Redis事务:
- 开始事务:
multi - 命令入队:
- 执行事务:
exec - 取消事务:
discard
正常执行事务
取消事务执行
命令有误
编译形异常,在命令入队时就会报错,执行事务时,所有的命令都不会执行
运行时异常
如果事务队列中存在语法错误,其他命令是可以正常执行的,错误命令抛出异常,所以不保证原子性
监控
- 悲观锁:什么时候都会出问题,无论做什么都加锁,影响性能
- 乐观锁:是么时候都不会出问题,不会加锁,更新数据时去判断在此期间是否有人修改(增加version字段,更新时候比较version)
应用场景:购物秒杀
指令:
watch
正常执行成功:
多客户端测试
使用
watch当做redis的乐观锁解锁使用
unwatch
七、Jedis
Jedis是Redis官方推荐的使用java来操作Redis开发工具,Java操作Redis的中间件
7.1 基本使用
- 创建一个空项目,创建一个module,引入依赖
<dependencies>
<!--Jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.1.1</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
</dependencies>
- 编码测试
- 连接数据库
- 操作命令
- 断开连接
这里使用远程连接
// 防火墙暴露6379端口 firewall-cmd --zone=public --add-port=6379/tcp --permanent // 重启防火墙,否则不生效 firewall-cmd --reload // 修改Redis配置文件 关闭ip绑定,让所有ip都可以访问: 注释`bind 127.0.0.1` 开启密码验证:`requirepass xxx`设置密码 关闭保护模式:`protect-mode no` 重启Redis服务
- 测试
public static void main(String[] args) {
// new Jedis对象
Jedis jedis = new Jedis("192.168.59.134", 6379);
jedis.auth("admin");
String ping = jedis.ping();
System.out.println(ping);
// Redis指令
System.out.println("清空数据库:" + jedis.flushDB());
System.out.println("判断某个键是否存在:" + jedis.exists("username"));
System.out.println("新增<'username', 'nick'>键值对:" + jedis.set("username", "nick"));
System.out.println("新增<'password', 'admin'>键值对:" + jedis.set("password", "admin"));
System.out.println("系统中所有的键:" + jedis.keys("*"));
System.out.println("删除password:" + jedis.del("password"));
System.out.println("判断password是否存在:" + jedis.exists("password"));
System.out.println("查看键username所存储值的类型:" + jedis.type("username"));
System.out.println("随机返回一个key:" + jedis.randomKey());
System.out.println("重命名username为name:" + jedis.rename("username", "name"));
System.out.println("取出改后的name:" + jedis.get("name"));
}
PONG
清空数据库:OK
判断某个键是否存在:false
新增<'username', 'nick'>键值对:OK
新增<'password', 'admin'>键值对:OK
系统中所有的键:[password, username]
删除password:1
判断password是否存在:false
查看键username所存储值的类型:string
随机返回一个key:username
重命名username为name:OK
取出改后的name:nick
7.2 常用API
1. String
public static void stringFunc() {
jedis.flushDB();
System.out.println("==============增加数据==============");
System.out.println(jedis.set("k1", "v1"));
System.out.println(jedis.set("k2", "v2"));
System.out.println(jedis.set("k3", "v3"));
System.out.println("在k3值后面追加数据:" + jedis.append("k3", "-appendValue"));
System.out.println("k3的值:" + jedis.get("k3"));
System.out.println("增加多个键值对:" + jedis.mset("k4", "v4", "k5", "v5", "k6", "v6"));
System.out.println("获取多个键值对:" + jedis.mget("k4", "k5", "k6"));
System.out.println("删除多个键值对:" + jedis.del("k1", "k2", "k3"));
jedis.flushDB();
System.out.println("==============新增键值对防止覆盖原来的值==============");
System.out.println("> setnx::SET if Not eXists::可用作分布式锁");
System.out.println("set k1: " + jedis.setnx("k1", "v1"));
System.out.println("set k2: " + jedis.setnx("k2", "v2"));
System.out.println("set k2 new value: " + jedis.setnx("k2", "v2-new"));
System.out.println(jedis.get("k1"));
System.out.println(jedis.get("k2"));
System.out.println("==============新增键值对并设置有效时间==============");
System.out.println("> setex::SET expire");
System.out.println(jedis.setex("k3", 2, "v3"));
System.out.println("立即获取k3:" + jedis.get("k3"));
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("三秒后查看k3:" + jedis.get("k3"));
}
==============增加数据==============
OK
OK
OK
在k3值后面追加数据:14
k3的值:v3-appendValue
增加多个键值对:OK
获取多个键值对:[v4, v5, v6]
删除多个键值对:3
==============新增键值对防止覆盖原来的值==============
> setnx::SET if Not eXists::可用作分布式锁
set k1: 1
set k2: 1
set k2 new value: 0
v1
v2
==============新增键值对并设置有效时间==============
> setex::SET expire
OK
立即获取k3:v3
三秒后查看k3:null
Process finished with exit code 0
2. List
public static void listFunc() {
jedis.flushDB();
System.out.println("==============添加一个List==============");
System.out.println("> lpush");
jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
jedis.lpush("collections", "HashSet");
jedis.lpush("collections", "TreeSet");
jedis.lpush("collections", "TreeMap");
printList("collections");
System.out.println("============================");
System.out.println("删除指定个数元素:" + jedis.lrem("collections", 2, "HashMap"));
printList("collections");
System.out.println("删除下标0-3之外的元素:" + jedis.ltrim("collections", 0, 3));
printList("collections");
System.out.println("列表左出栈元素:" + jedis.lpop("collections"));
printList("collections");
System.out.println("列表从右边添加元素:" + jedis.rpush("collections", "EnumMap"));
printList("collections");
System.out.println("列表右出栈元素:" + jedis.rpop("collections"));
printList("collections");
System.out.println("修改列表指定下标元素:" + jedis.lset("collections", 1, "LinkedArrayList"));
printList("collections");
System.out.println("获取列表指定下标元素:" + jedis.lindex("collections", 1));
System.out.println("============================");
jedis.lpush("sortedList", "3", "6", "2", "0", "7", "4");
printList("sortedList", "排序前");
System.out.println("排序后:" + jedis.sort("sortedList"));
}
private static void printList(String listKey, String... pre) {
StringBuilder sb = new StringBuilder("");
if (pre != null) {
Arrays.stream(pre).forEach(item->{sb.append(item);});
}
System.out.println(sb.append(listKey).append("的内容:").append(jedis.lrange(listKey, 0, -1)).toString());
}
==============添加一个List==============
> lpush
collections的内容:[TreeMap, TreeSet, HashSet, LinkedHashMap, WeakHashMap, HashMap, Stack, Vector, ArrayList]
============================
删除指定个数元素:1
collections的内容:[TreeMap, TreeSet, HashSet, LinkedHashMap, WeakHashMap, Stack, Vector, ArrayList]
删除下标0-3之外的元素:OK
collections的内容:[TreeMap, TreeSet, HashSet, LinkedHashMap]
列表左出栈元素:TreeMap
collections的内容:[TreeSet, HashSet, LinkedHashMap]
列表从右边添加元素:4
collections的内容:[TreeSet, HashSet, LinkedHashMap, EnumMap]
列表右出栈元素:EnumMap
collections的内容:[TreeSet, HashSet, LinkedHashMap]
修改列表指定下标元素:OK
collections的内容:[TreeSet, LinkedArrayList, LinkedHashMap]
获取列表指定下标元素:LinkedArrayList
============================
排序前sortedList的内容:[4, 7, 0, 2, 6, 3]
排序后:[0, 2, 3, 4, 6, 7]
Process finished with exit code 0
3. Set
public static void setFunc() {
jedis.flushDB();
System.out.println("==============向集合中添加元素(不重复)==============");
System.out.println("> sadd");
System.out.println(jedis.sadd("eleSet", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
System.out.println(jedis.sadd("eleSet", "e6"));
System.out.println(jedis.sadd("eleSet", "e6"));
System.out.println("eleSet所有元素为:" + jedis.smembers("eleSet"));
System.out.println("删除一个元素e0:" + jedis.srem("eleSet", "e0"));
System.out.println("eleSet所有元素为:" + jedis.smembers("eleSet"));
System.out.println("删除两个元素e7、e6:" + jedis.srem("eleSet", "e7", "e6"));
System.out.println("eleSet所有元素为:" + jedis.smembers("eleSet"));
System.out.println("随机移除集合中的一个元素:" + jedis.spop("eleSet"));
System.out.println("随机移除集合中的一个元素:" + jedis.spop("eleSet"));
System.out.println("eleSet所有元素为:" + jedis.smembers("eleSet"));
System.out.println("集合元素个数:" + jedis.scard("eleSet"));
System.out.println("e3是否在集合元素中:" + jedis.sismember("eleSet", "e3"));
System.out.println("e1是否在集合元素中:" + jedis.sismember("eleSet", "e1"));
System.out.println("e5是否在集合元素中:" + jedis.sismember("eleSet", "e5"));
System.out.println("============================");
System.out.println(jedis.sadd("eleSet1", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
System.out.println(jedis.sadd("eleSet2", "e1", "e2", "e4", "e3", "e0", "e8"));
System.out.println("将eleSet1中删除e1并入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e1"));
System.out.println("将eleSet1中删除e2并入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e2"));
System.out.println("eleSet1中元素为:" + jedis.smembers("eleSet1"));
System.out.println("eleSet3中元素为:" + jedis.smembers("eleSet3"));
System.out.println("==============集合运算==============");
System.out.println("eleSet1中元素为:" + jedis.smembers("eleSet1"));
System.out.println("eleSet2中元素为:" + jedis.smembers("eleSet2"));
System.out.println("eleSet1与eleSet2求交集:" + jedis.sinter("eleSet1", "eleSet2"));
System.out.println("eleSet1与eleSet2求并集:" + jedis.sunion("eleSet1", "eleSet2"));
System.out.println("eleSet1与eleSet2求差集:" + jedis.sdiff("eleSet1", "eleSet2"));
jedis.sinterstore("eleSet4", "eleSet1", "eleSet2");
System.out.println("eleSet4中元素为:" + jedis.smembers("eleSet4"));
}
==============向集合中添加元素(不重复)==============
> sadd
8
1
0
eleSet所有元素为:[e5, e6, e7, e8, e0, e1, e2, e3, e4]
删除一个元素e0:1
eleSet所有元素为:[e5, e6, e7, e8, e1, e2, e3, e4]
删除两个元素e7、e6:2
eleSet所有元素为:[e5, e8, e1, e2, e3, e4]
随机移除集合中的一个元素:e4
随机移除集合中的一个元素:e2
eleSet所有元素为:[e5, e1, e3, e8]
集合元素个数:4
e3是否在集合元素中:true
e1是否在集合元素中:true
e5是否在集合元素中:true
============================
8
6
将eleSet1中删除e1并入eleSet3中:1
将eleSet1中删除e2并入eleSet3中:1
eleSet1中元素为:[e5, e7, e8, e0, e3, e4]
eleSet3中元素为:[e1, e2]
==============集合运算==============
eleSet1中元素为:[e5, e7, e8, e0, e3, e4]
eleSet2中元素为:[e8, e0, e1, e2, e3, e4]
eleSet1与eleSet2求交集:[e3, e8, e0, e4]
eleSet1与eleSet2求并集:[e5, e7, e8, e0, e1, e2, e3, e4]
eleSet1与eleSet2求差集:[e7, e5]
eleSet4中元素为:[e3, e4, e8, e0]
Process finished with exit code 0
4. Hash
public static void hashFunc() {
jedis.flushDB();
Map<String, String> map = new HashMap<>(4);
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
map.put("k4", "v4");
System.out.println("==============散列Hash测试==============");
System.out.println("> hmset");
jedis.hmset("hash", map);
jedis.hset("hash", "k5", "v5");
System.out.println("散列hash所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("散列hash所有键为:" + jedis.hkeys("hash"));
System.out.println("散列hash所有值为:" + jedis.hvals("hash"));
System.out.println("在k6添加一个整数,如果k6不存在则添加:" + jedis.hset("hash", "k6", "v6"));
System.out.println("散列hash所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("在k6添加一个整数,如果k6不存在则添加:" + jedis.hset("hash", "k6", "v6"));
System.out.println("散列hash所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("删除一个或多个键值对:" + jedis.hdel("hash", "k6", "k5"));
System.out.println("散列hash所有键值对为:" + jedis.hgetAll("hash"));
System.out.println("散列hash键值对个数为:" + jedis.hlen("hash"));
System.out.println("散列hash判断是否存在k1:" + jedis.hexists("hash", "k1"));
System.out.println("散列hash获取k1的值:" + jedis.hget("hash", "k1"));
}
==============散列Hash测试==============
> hmset
散列hash所有键值对为:{k3=v3, k4=v4, k5=v5, k1=v1, k2=v2}
散列hash所有键为:[k3, k4, k5, k1, k2]
散列hash所有值为:[v3, v4, v1, v2, v5]
在k6添加一个整数,如果k6不存在则添加:1
散列hash所有键值对为:{k3=v3, k4=v4, k5=v5, k6=v6, k1=v1, k2=v2}
在k6添加一个整数,如果k6不存在则添加:0
散列hash所有键值对为:{k3=v3, k4=v4, k5=v5, k6=v6, k1=v1, k2=v2}
删除一个或多个键值对:2
散列hash所有键值对为:{k3=v3, k4=v4, k1=v1, k2=v2}
散列hash键值对个数为:4
散列hash判断是否存在k1:true
散列hash获取k1的值:v1
Process finished with exit code 0
7.3 Jedis事务操作
- 正常情况
public static void main(String[] args) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "wangwk-a");
jsonObject.put("age", 28);
jsonObject.put("gender", "男");
jedis.flushDB();
// 开启事务
Transaction multi = jedis.multi();
try {
// 事务操作
multi.set("user", JSONObject.valueToString(jsonObject));
// 执行事务
multi.exec();
} catch (Exception e) {
// 放弃事务
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user"));
jedis.close();
}
}
{"gender":"男","name":"wangwk-a","age":28}
- 异常情况
public static void main(String[] args) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "wangwk-a");
jsonObject.put("age", 28);
jsonObject.put("gender", "男");
jedis.flushDB();
// 开启事务
Transaction multi = jedis.multi();
try {
// 事务操作
multi.set("user", JSONObject.valueToString(jsonObject));
int i = 1 / 0; // 制造运行时异常
// 执行事务
multi.exec();
} catch (Exception e) {
// 放弃事务
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user"));
jedis.close();
}
}
java.lang.ArithmeticException: / by zero
at com.nick.JedisTransctionTest.main(JedisTransctionTest.java:33)
null
八、SpringBoot整合Redis
SpringBoot操作数据:SpringData jpa、jdbc、MongoDB、redis
SpringData是和SpringBoot齐名的项目。
8.1 创建项目
- 在创建项目的时候勾选
NoSQL中的Redis选项,系统会自动在pom文件中引入操作redis的依赖,点击进入可以看到底层是基于SpringDataRedis的
在SpringBoot 2.x之后,原来使用的
Jedis被替换成了Lettuce
jedis:采用直连,多线程操作不安全,如果想要避免,就要使用jedis pool连接池,会有很多问题,BIO模式
lettuce:底层采用netty,实例可以在多个县城中进行共享,不存在线程不安全的情况,可以减少线程数量,更像NIO模式
8.2 导入配置
- 找到redis相关的自动配置类
- 从自动配置类中找到默认配置文件
默认的redisTemplate没有过多的设置,两个泛型都是
Object类型,后续使用要进行强制转换,因此我们可以自己定义一个redisTemplate来替换默认的。由于String类型常用,所以说单独提出来了一个bean
spring:
redis:
host: 192.168.59:134
port: 6379
database: 0
password: admin
timeout: 0
注意:
如果在redis中配置使用密码,在jedis中使用jedis.auth来进行配置,在springboot中需要配置url,如下所示,格式为:
redis://password@ip:port
这是因为配置解析的时候,并没有从
password中获取密码,而是从url中解析
spring:
redis:
url: redis://admin@192.168.59.134:6379
database: 0
timeout: 0
8.3 连接测试
redisTemplate操作方法:
- opsForValue
- opsForList
- opsForSet
- opsForHash
- opsForZSet
- opsForGeo
- opsForHyperLogLog
redisTemplate事务方法:
- multi
- exec
- discard
redisTemplate连接方法:
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();
connection.flushAll();
完整代码如下:
@Test
void contextLoads() {
redisTemplate.opsForValue().set("k1", "v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
}
v1
8.4 自定义配置类
1. 序列化问题
- pojo
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}
- 测试类
@Test
void test() throws JsonProcessingException {
User user = new User("王文堃", 28);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
- 结果
org.springframework.data.redis.serializer.SerializationException: Cannot serialize;
nested exception is org.springframework.core.serializer.support.SerializationFailedException:
Failed to serialize object using DefaultSerializer;
nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.nick.pojo.User]
直接传对象的话会报错,需要将对象先序列化,然后在传入
2. 对象进行序列化
- 实体类
需要实现先关接口:implements Serializable
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private String name;
private int age;
}
- 测试类保持不变,结果如下
User(name=王文堃, age=28)
- 在Linux中连接redis查看存入的数据
发现user的key是乱码的
3. 使用json
如果不想pojo实现序列化接口,也可以使用json
- 测试类
@Test
void test() throws JsonProcessingException {
User user = new User("王文堃", 28);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
- 返回结果
{"name":"王文堃","age":28}
- Linux查看依旧有乱码
3. 乱码原因
- 在
RedisTemplate类中有如下几个默认序列化配置
- 看
KeySerializer赋值的地方可以发现,默认情况使用JDK的序列化,JDK的序列化会让字符串转义,导致中文乱码,因此我们会自己使用Json的序列化
4. 解决
编写自己的redisConfig
@Configuration
public class RedisConfig {
/**
* 从RedisAutoConfiguration中赋值过来
* @param redisConnectionFactory
* @return
*/
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 为了自己方便,使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 连接工厂
template.setConnectionFactory(redisConnectionFactory);
/* 序列化 */
// Json的序列化
Jackson2JsonRedisSerializer<Object> 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);
// value采用Json序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的key采用String序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// hash的value也采用Json序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
// 将所有的Properties设置进去
template.afterPropertiesSet();
return template;
}
}
- 测试类
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
@Qualifier("redisTemplate")
RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("k1", "v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
}
@Test
void test() throws JsonProcessingException {
User user = new User("王文堃", 28);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
}
- 结果
{"name":"王文堃","age":28}
v1
- 查看Linux中的keys
九、Redis.conf详解
启动的时候就是通过配置文件来启动的
9.1 介绍
9.2 包含配置
9.3 网络配置
- IP绑定
绑定redis服务器网卡ip,默认127.0.0.1,即本地回环地址,访问redis只能通过本机客户端连接,无法远程。
如果bind选项为空,会接受所有来与可用网络接口的连接
- 保护模式
保护模式,默认开启,只允许本地客户端连接,可以设置密码或添加bind来连接
- 端口、TCP、超时配置
- port:端口号,默认6379,
- tcp-backlog:TCP监听最大容纳数量,在高并发环境,需要调高
- unixsocket:指定redis监听unix socket,默认不启动,unixsockerperm只文件权限
- timeout:指定在一个client空闲多少秒后关闭连接(0表示永远不关闭)
9.4 通用配置
- 是否以守护进程开启
以守护进程运行,工作在后台
- 管理守护进程
- no:没有监督互动
- upstart:通过将redis置于SIGSTOP模式来启动信号
- systemd:signal systemd将READY=1写入
$NOTIFY_SOCKET- auto:检测uostart或者systemd方法基于UPSTART_JOB或NOTIFY_SOCKET环境变量
- pid文件
配置PID文件路径,当redis作为守护进程运行的时候,它会把 pid 默认写到 /var/redis/run/redis_6379.pid 文件里面
- 日志级别
- debug(记录大量日志信息,适用于开发、测试阶段)
- verbose(较多日志信息)
- notice(适量日志信息,使用于生产环境)
- warning(仅有部分重要、关键信息才会被记录)
- 日志文件名
为空则是标准的日志输出
- 数据库数量
- 是否显示logo
9.5 快照配置
做持久化使用,在规定时间内执行了多少次操作则会持久化
- 文件格式:
.rdb、.aof因为redis是操作内存的,如果不进行持久化,这些数据就会丢
- 持久化出错后是否继续工作
- 是否压缩rdb文件
压缩会消耗CPU资源
- 是否校验rdb文件
- rdb文件保存目录
9.6 主从复制配置
9.7 安全配置
- 设置密码
9.8 客户端配置
- 最大客户端数
9.9 内存配置
- 最大内存设置
- 内存满后的处理策略
- volatile-lru 利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )
- allkeys-lru 利用LRU算法移除任何key
- volatile-random 移除设置过过期时间的随机key
- allkeys-random 移除随机ke
- volatile-ttl 移除即将过期的key(minor TTL)
- noeviction 不移除任何key,只是返回一个写错误 ,默认选项
9.10 AOF设置
RDB和AOF是两个redis持久化的方式
AOF:Append Only Mode
AOF模式默认不开启,因为使用的是RDB持久化模式,大部分情况下RDB完全够用
持久化文件名称 “appendonly.aof”
- *** aof持久化策略的配置***
- no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
- always表示每次写入都执行fsync,以保证数据同步到磁盘。
- everysec表示每秒执行一次fsync,可能会导致丢失这1s数据
十、Redis持久化
10.1 RDB
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态就会消失。所以Redis提供了持久化功能
1. 什么是RDB
Redis DataBase
在指定时间间隔内将内存中的数据集Snapshot快照写入磁盘,它恢复时是将快照文件直接读到内存中。
Redis会单独创建(Fork)一个子进程来进行持久化,会先将数据写到一个临时文件中,待持久化过程结束,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作,这就确保了极高的性能,如果需要进行大规模数据恢复,且对于数据恢复完成性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失。
rdb保存的文件是dump.rdb文件,在配置文件中进行配置。
在主从复制中,rdb就是备用的,放在从机上,不占用主机内存,AOF几乎不使用
有时候,生产环境会对dump.rdb进行备份
2. RDB测试
- 修改配置文件,60s修改5次key就进行rdb操作
- 确认此时文件夹没有
dump.rdb
- 客户端连接,修改5个key
- 查看
dump.rdb文件
3. RDB触发机制与恢复
rdb触发时机
- sava规则满足的情况下,会自动触发rdb规则
- flushall命令执行,也会触发rdb规则
- 退出redis时,也会产生rdb文件
备份就会自动生成rdb文件
恢复rdb文件
只需要rdb文件放到redis启动目录就可以,redis启动时会自动检查dump.rdb恢复其中的数据。
查看需要存放的位置:
如果在这些目录下存在dump.rdb文件,启动就会自动恢复其中的数据,几乎默认配置就够用了。
4. 优缺点
优点
- 适合大规模数据恢复(fork子进程来处理)
- 对数据完整性要求不高,也是可以使用
缺点
- 需要一定的时间间隔,如果redis意外down机,最后一次修改数据就没有了
- fork进程的时候,会占用一定内存空间
10.2 AOF
1. 什么是AOF
Append Only File:将所有命令都记录下来,恢复的时候就将该文件全部执行一遍
以日志的形式来记录每个写操作,将redis执行过的所有指令记录下来,只需追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据。redis重启的话就会根据日志文件的内容将写指令从前到后执行一次以完成数据恢复。
aof保存的文件是appendonly.aof文件,在配置文件中进行配置。
2. AOF测试
- 修改配置文件,开启AOF
- 重启服务,并使用客户端连接
- 查看
appendonly.aof
- 故意修改
appendonly.aof文件,然后重启redis
由于破坏了aof,客户端连接失败
- 使用redis-check-aof进行文件修复
修复后的aof文件:
可以看到<k5, v5>没有了
3. AOF优缺点
优点:
- 每一次修改都同步,文件管理性更加好
- 每秒同步一次,可能会丢失一秒数据
- 从不同步,效率最高
缺点:
- 相对于rdb来说,aof远大于rdb
- aof是读写io操作,速度慢
- 如果aof文件大于64M(配置),fork一个新的进程来将我们文件进行重写
十一、Redis发布订阅
11.1 介绍
Redis发布订阅(pub/sub)是一种==消息通信模式==:发送者(pub)发送消息,订阅者(sub)接收消息。(微信微博的关注系统)
redis客户端可以订阅任意数量的频道,订阅/发布消息图:
- 消息发送者:kuangshen
- 频道:狂神说Java
- 消息订阅者:我
11.2 命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatRoom)和实时广播、实时提醒等。
PSUBSCRIBE pattern [pattern...]:订阅一个或多个符合给定模式的频道PUBSUB subcommand [argument [argument...]]:查看订阅和发布系统状态PUBLISH channel message:将信息发送到指定的频道(消息发送者)PUNSUBSCRIBE [pattern [pattern...]]:退订所有给定模式的频道SUBSCRIBE channel [channel...]:订阅给定的一个或多个频道信息UNSUBSCRIBE [channel [channel...]]:退订给定的频道
11.3 测试
- 订阅
- 发布
- 查看接收
接收消息:
1) "message" # 消息 2) "kuangshenshuo" # 哪个频道 3) "hello,kuangshen" # 具体内容
11.4 实现原理
redis使用c实现,源码中分析pubsub.c文件,了解发布和订阅机制的底层实现。Redis是通过PUBLISH、SUBSCRIBE、PSUBSCRIBE等指令实现发布和订阅功能。
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了字典,字典的键就是channel,值是一个链表,链表中保存了所有订阅这个channel的客户端,SUBSCRIBE命令核心就是将客户端添加到给定channel的订阅链中。
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它维护的channel字典中查找记录订阅这个频道的所有客户端链表,遍历链表,将消息发送给所有订阅者。
Pub/Sub从字面理解就是发布和订阅,在redis中,可以设定对某个key值进行消息发布以及订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到响应的消息,用处就是作为实时信息系统,比如普通的即时聊天、群聊等功能。
十二、主从复制
12.1 概念
主从复制,是指将一台redis服务器的数据,复制到其他redis服务器。前者称为主节点(master),后者称为从节点(slaver);数据的复制是单向的,只能由主节点到从节点,M以写为主,S以读为主。
默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点,但是一个从节点只能由一个主节点。
主从复制的作用包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余
- 负载均衡:在主从复制的基础上,配合==读写分离==,可以由主节点提供写服务,从节点提供读服务,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高redis服务器的并发量
- 高可用基石:除上述作用外,主从复制还是哨兵和集群能够实时的基础,因此说主从复制是redis高可用的基础
一般来说,要将redis运用于工程项目中,只是用一台redis是万万不能的,原因如下:
- 从结构上,单个redis服务器会发生单点故障,并且一台服务器需要处理所有请求负载,压力大
- 从容量上,单个redis服务器内存容量有限,就算容量为256G,也不能将所有内存用作redis存储内存,一般来说,单台最大使用内存不超所20G
12.2 环境配置
只用配置从机,默认就是主机
1. 从机安转软件
从机需要开放6379端口,修改对应配置文件
- 启动主机,查看info
2. 从机配置文件
因为我们设置了密码,因此在这里通过配置文件进行修改,这些也可以在客户端通过指令来进行修改
3. 主从连接
- 从机
- 主机
4. 细节
- 主机可以写,主机中的所有信息和数据都会被从机保存
- 从机只能读,不能写
- 主机断开连接后,从机并不会自动变主机,需要配置哨兵才可以。主机重新连接后,设置k2,从机也是可以获取到的。
- 如果使用命令行配置从机的主从关系,从机重启后,就会变成主机;配置文件的方式依旧是从机
- 如果从机断了,主机设置k3,重新连接从机,立马从主机获取数据,因此从机依旧可以拿到k3的数据
复制原理:
Slave启动成功连接到Master后,会发送一个sync命令
Master接到改命令,启动后台存盘进程,同时收集所有接收到用于修改数据集命令,在后台进程执行完毕后,Master将传送整个数据文件到Slave,并完成一次完全同步。
全量复制:Slave接收到数据库文件后,将其存盘并加载到内从中
增量复制:Master继续将新数据收集到的修改命令传给Slave,完成同步
要是重新连接Master,一次完全同步将被自动执行
5. 层层链路
- 这种模型下133是从机,依旧不能够写数据
- 主机写key,两个从机依旧可以获取数据
如果134Master主机断开,能否将133Slave变成主机?
要在133中使用SLAVEOF no one进行设置,指明如果此时没有主机,则当前机器为主机。其他节点手动使用SLAVEOF指令从新设置主从关系。
在哨兵模式出来前,使用该方式进行设置。
十三、哨兵模式
13.1 介绍
自动选Master的机制
主从切换技术的方法是:当Master宕机后,需要手动把一台Slave切换为Master,需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候有限考虑哨兵模式,Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。
哨兵模式是一种特殊的模式,能够后台监控主机是否故障,如果故障了根据投票数自动将从机转为主机。首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行,其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行多个Redis实例。
这里哨兵的作用:
- 通过发送命令,让Redis服务器返回监控器运行转台,包括主机和从机
- 当哨兵检测到Master宕机,会自动将Slave切换为新的Master,然后通过发布订阅模式通知其他从机,修改配置文件,让他们切换主机
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,因此可以使用多个哨兵进行监控,各个哨兵之间还会进行监控,就形成了多哨兵模式。
假设Master宕机,哨兵1先检测到这个结果,系统并不会马上进行failover(故障转移)过程,仅仅是哨兵1主管认为Master不可用,这个现象成为主观下线。当后面的哨兵也监测到Master不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从机切换主机,这个过程为客观下线。
13.2 测试
目前项目的状态是一主二从:
编写哨兵配置文件
- 位置:
nick_config/sentinel.conf
配置中:
myredis:被监控的名称192.168.59.134:hostIP6379:port1:表示主机宕机时,Slave投票看让谁接替主机,票数最多这成为新主机
sentinel monitor myredis 192.168.59.134 6379 1
启动哨兵
***说明:***如果主从服务器设置密码,需主从服务器密码保持一致,否则哨兵机制会失败!
- 主机
- 从机
- 然后关闭主机,等待一会,观察哨兵进程的心跳包,可以看到进行了failover过程
13.3 优缺点
优点:
- 基于主从复制模式,所有主从配置有点,都有
- 主从可以切换,故障可以转移,系统可用性更好
- 找并模式就是主从模式的升级,手动到自动,更加健壮
缺点:
- 不好在线扩容
- 实现哨兵模式的配置其实很麻烦
13.4 哨兵配置
- 端口
port 26379
如果有哨兵集群,我们还需要配置每个哨兵端口。
- 哨兵工作目录
dir /tmp
- 监控主节点
sentinel monitor mymaster 127.0.0.1 6379 2格式:
sentinel monitor <master-name> <ip> <redis-port> <quorum>
quorum配置多少个sentinel哨兵统一认为master主节点失联,那么客观上认为主节点失联了
- 授权密码
sentinel auth-pass <master-name> <password>设置哨兵sentinel连接主从的密码,注意必须为主从设置一样的验证密码
- 默认哨兵等待时间
sentinel down-after-millseconds <master-name> <milliseconds>指定多少毫秒后,主节点没有答应哨兵,此时哨兵主观认为主节点下线,默认30s
- 多少个slave同时对新master进行同步
sentinel parallel-suncs <master-name> <numskaves>这个数字越小,完成failover所需时间就越长
但如果这个数字越大,越多的slave因为replication而可不可用
可以通过将这个值改为1,来保证每次只有一个slave处于不能处理命令请求的状态
- 故障转移超时时间
sentinel failover-timeout <master-name> <milliseconds>故障转移的超时时间,
- 同一个sentinel对同一个master两次failover之间的间隔时间
- 当一个slave从一个错误的master哪里同步数据开始计算时间,知道slave被纠正为正确master同步数据时间
- 当想要取消一个正在进行的failover所需要的时间
- 当进行failover时,配置所有slaces指向新的master所需的最大时间,不过及时过了这个时间,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置规则来
默认三分钟
十四、缓存穿透和雪崩(服务高可用问题)
14.1 缓存穿透(查不到)
概念
缓存穿透指用户想要查询一个数据,发现redis内存数据库没有, 即缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大压力,这个时候相当于出现了缓存穿透。
解决方案
- 布隆过滤器:一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免对底层存储系统的查询压力。
- 缓存空对象:当存储层不命中后,即使返回空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护后端数据源。
但是这种方式会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能有很多空值的键
- 及时对空值设置过期时间,还是会存在缓存层和存储层数据有一段时间窗口不一致,这对于需要保持一致性的业务会有影响
14.2 缓存击穿(量太大,缓存过期)
概述
这里需要注意和缓存穿透区别,缓存击穿是指一个key非常热点,再不能扛着大并发,大并发集中对着一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破华奴才能,直接请求数据库,就像在屏障中凿开了一个洞。
当某个key在过期瞬间,有大量请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大。
解决方案
- 设置热点数据永不过期:从缓存层面看,没有设置过期时间,所以不会出现热点key过期后产生的问题
- 加互斥锁:使用分布式锁(setnx),保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高迸发的压力转移到分布式锁,因此对分布式锁的考研很大。
14.3 缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
产生雪崩的原因之一,比如在写文本的时候,马上就要双十二零点,很快就会迎来一波抢购,这波商品时间比较几重的放入了缓存,假设缓存一个小时。那么到了凌晨一点时,这批商品的缓存过期了,而对这批商品的访问查询,都落到数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有请求到会到达存储层,存储层调用量暴增,造成存储层挂掉的情况。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或者断网, 因为自然形成的缓存雪崩,一定是在某个时间段集中穿件缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性压力而已。而缓存服务节点宕机,对数据库服务器造成的压力不可预知,可能瞬间就把数据库压垮。
双十一案例:停掉一些服务,保证主要服务的可用,例如双十一不能够退款
解决方案
- redis高可用:既然redis可能会挂掉,多增设几台,一台挂掉后其他的继续工作。(异地多活)
- 限流降级:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个key只允许一个线程查询和写缓存,其他线程等待。
- 数据预热:数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样大量访问数据会加载到缓存中,在即将发生大并发前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。