文章目录
- 一.NoSQL 简介
- 二、Redis 简介
- 三、Redis 安装
- 四、Redis 基础知识
- 基础指令
* [Redis单线程](about:blank#Redis_290)
- 基础指令
- 1.五大数据类型
- 2.三种特殊数据类型
- 3.Redis 事务
- 4.Redis实现乐观锁
- 五、Java 操作 Redis
- 六、Redis 高级
一.NoSQL 简介
1.数据库演化
早期网站访问量不大,单个数据够用,多数为静态网页,服务器没有太大压力,网站瓶颈主要在
- 数据量过大,一台机器放不下
- 数据索引(B+Tree)内存也放不下
- 访问量读写混合,一个服务器承受不了
为了解决以上问题,开始使用 Memcached(缓存)+MySQL+垂直拆分(读写分离)
- 开始没有MyCat中间件,为了确保多个数据库服务器数据同步,所以数据库采用了读写分离的方式
- 网站的主要消耗在读操作,所以经常使用的数据使用缓存来保证查询效率
进一步优化结构,采用 分库分表+水平拆分+MySQL集群
- 核心问题在解决数据库的 读、写
数据量进一步增长,需要保存较大的文件,如博客、图片等,MySQL开始力不从心,效率低下,到了近期技术需要进一步提升,由转恩的数据库来处理。比如,一个几亿条数据的表,需要加一个列,很难想象。
2.为什么使用NoSQL
用户个人信息、社交网络、地理位置、用户自己产生的数据、用户日志等数据量巨大,这些类型数据存储不需要固定的格式,也就是行列,不需要操作就可以横向扩展,
需要新型的专门数据库出里,随之出现了NoSQL
NoSQL=Not Only SQL
3.NoSQL特点
- 方便扩展,数据之间没有关系,方便扩展
- 大数据量高性能,读写速度快,是细粒度缓存
- 数据类型多样,不需要事先设计数据库,随取随用,数据量庞大的表很不好设计
传统的关系型数据库RDBMS
- 结构化组织
- 语言SQL
- 数据和关系都存在单独的表中
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
NoSQL
- 存储不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性(最终一致即可)
- CAP定理和BASE
- 高性能,高可用,高可扩展
4.NoSQL分类
- 泛指非关系型数据库,为了和关系型数据库做区分,Redis就是一款NoSQL
- 主流分为四大类:
1.Key-Value型:Redis Tair memecache
2.文档型:ElasticSearch Solr MongoDB(非关系型数据库中最像关系型数据库的)
3.面向列:Hbase Cassandra
4.图形化:Neo4j,不是存图片,而是存关系图形化的
简单的理解:关系型数据库以外的都是非关系型数据库,因为它不采用表结构
5.电商的数据存储
商品的基本信息
名称、价格、商家等信息,
采用关系型数据库,MySQL、Oracle
商品的描述、评论(文字较多)
文档型数据库,如MongoDB
图片
分布式文件系统,FastDFS
淘宝自研技术,TFS
Google:GFS
Hadoop:HDFS
阿里云:OSS
商品关键字(搜索)
早期搜索引擎:solr elasticsearch
淘宝使用:ISearch
商品热门信息
内存数据库:Redis,Tair,Memache
商品的交易,外部支付接口
第三方应用如银行
二、Redis 简介
1.简介
- Redis全称Remote Dictionary Server,即远程字典服务,由C语言编写,所以不需要安装Java环境,是一款基于K-V的NoSQL,使用起来就像map一样,免费,开源,也叫结构化数据库
- 一个意大利人需要开发一款LLOOGG统计页面,因为MySQL性能不好,所以自己研发一款非关系型数据库,并命名为Redis
- 基于内存存储数据,读写速度快,性能达到110000次/s读数据,81000次/s写数据,但是内存存储不持久,所以Redis提供了持久化机制
- Redis还提供了主从、哨兵以及集群的搭建方式,可以更方便的横向扩展以及垂直扩展
- 新浪有全世界最大的Redis集群,数据量大的服务器基本都需要Redis,他是后台开发人员的必备技能之一
2.为什么需要Redis
- 由于用户量增大,请求数量增大,数据压力过大,服务器集群能承受住,数据库无法承受
- 多台服务器之间存在数据不同步,比如,上次登录的服务器有session,这次登录的服务器没有session
- 多台服务器之间的锁已经不具备互斥性,各论各的
3.Redis帮我们解决什么
- 针对热点数据添加缓存,存到Redis中,Redis基于内存存储、读取数据,读写效率非常高
- 将之前存储在session中的共享数据统一存放Redis中
- Redis基于接收用户请求的是单线程的
- 可以实现持久化
- 可以发布订阅,实现消息队列功能
- 可以地图信息分析
- 支持计时器、计数器,比如支持浏览量
- 提供多种语言API
特性:多样的数据类型、持久化、集群、事务
4.学习途径
中文网:http://www.redis.cn/、
论坛
5.下载
windows版本在Github上下载,很久没有更新,Redis官方推荐使用Linux版本
三、Redis 安装
1.windows环境
github下载,放在指定的目录下,直接解压文件,Redis很小,只有几兆
- redis-server.exe:启动服务端
- redis-cli.exe:启动客户端
- redis-check-aof.pdb:检查持久化文件是否正确
- redis-benchmark.exe:测试性能
启动服务端
双击redis-server,启动服务端,Redis默认端口号6379,
如果启动服务端发现闪退,可能是重复启动导致的,进入任务管理器关闭旧的即可
启动客户端
双击redis-cli,默认连接6379端口,使用ping测试是否连接正常
测试存储,设置一个k-v,并获取值,如,
set name jack
get name
window下使用很简单,但是Redis推荐我们使用Linux环境使用!官方已经不维护了,只是微软在维护而已
windows环境下也可以使用RedisDesktopManager来作为客户端界面,
2.Linux安装
下载
官网下载:https://redis.io/download
使用VMware启动Linux,MobaXterm连接Linux
将Redis压缩包上传到Linux中,进入到上传目录,
解压Redis压缩包,将程序移动到opt目录下(推荐放在此处),
tar -zxvf redis-6.2.0.tar.gz
mv redis-6.2.0 /opt
环境
Redis运行需要C++环境,安装C++,查看C++版本状态
yum install gcc-c++
gcc-v
安装
进入Redis解压缩的目录,执行make
命令,开始安装Redis(需要一段时间),make insall
确认一下
cd redis-6.2.0/
make
make install
Redis默认安装目录在/usr/local/bin中,这里的目录就相当于windows中解压缩后进入的目录
配置文件
在当前目录下新建一个配置文件目录,将Redis解压文件中的redis.con配置文件复制到其中
以后使用这个复制的配置文件启动Redis
mkdir rconfig
cp /opt/redis-6.2.0/redis.conf rconfig/
启动
Redis默认不是后台启动,将其修改为后台启动,编辑配置文件,
vim命令不好使的,需要在线安装vim,自行百度搜方法
vim redis.conf
i开始插入编辑,这里修改为yes表示可以后台启动,esc退出编辑,:wq退出配置文件
回到bin目录,通过配置文件启动Redis服务端
redis-server rconfig/redis.conf
启动客户端,连接服务
redis-cli -p 6379
测试连接,测试存储
查看Redis进程命令
ps -ef|grep redis
关闭
在客户端连接中,使用命令shutdown关闭服务端,退出客户端即可
shutdown
exit
3.测试性能
bin目录下的redis-benchmark,官方自带
如,测试100个并发,100000个并发请求
确保客户端连接服务,再开启一个新的连接窗口,进入到bin目录
新窗口bin目录中执行测试命令
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
分析结果
写入测试,100000个请求,0.84秒完成,100个并发,每次写入3字节,保持1个连接完成多少百分比的请求量所消耗的时间
计算结果,每秒处理119189.52个请求
四、Redis 基础知识
Redis默认16个数据库,默认使用的是第一个数据库,select *指令可以切换,配置文件中可以查看到,
基础指令
以下指令在客户端连接状态下使用
#切换数据库
select num(0-15)
#查看数据库大小
dbsize
#查看数据库所有的key
keys *
#清空当前数据库
flushdb
#清除所有数据库
flushall
Redis单线程
Redis速度很快,基于内存操作,性能瓶颈不在CPU,而是内存和网络带宽,我们使用Redis用单线程即可
多线程的CPU上下文切换比较耗时,所有对于Redis来说,单线程效率最高
1.五大数据类型
提示:五大基本数据类型的操作使用需要熟练掌握,因为Java操作Redis的API都是基于Redis原生操作来定义的
Redis-Key
key泛指键,value泛指值,以实际为准
查看所有key
keys *
判断某一个key是否存在
exist key
将数据移动到指定的数据库中
move key num(数据库编号)
设置某一条数据的过期时间,t的单位是秒
expire key t
查看某条数据的剩余生命时长,-2表示没了
ttl key
查看key的类型
type key
String(字符串)
赋值、取值、追加、长度
set key value
get key
#向key对应的值后面追加value,如果当前key不存在,则相当于set key value
append key value
#查看key对应的值的长度
strlen key
自增减、步长
#给key的值+1,常用于计数,如网页浏览量
incr key
#将key的值-1
decr key
#增减时设置增减步长
incrby key num
decrby key num
截取
#截取字符串,从num1-num2,0到-1表示查看全部字符串
getrange key num1 num2
替换
#替换字符串,从offset处开始
setrange key offset value
判断,分布式锁中经常使用
#如果key存在,则设置过期时间和key的值
setex key seconds value
#如果key不存在,则创建新的key和value,如果这个key存在,那么也不会设置成功,值也不会覆盖
批量处理,原子性
#批量设置键值对k1-v1,k2-v2,k3-v3
mset k1 v1 k2 2 k3 v3
#批量获取值
mget k1 k2 k3
#批量设置,如果不存在,则设置k-v,只要其中有一个是存在的,则整体设置不成功,体现出原子性
msetnx k1 v1 k2 v2 k3 v3
利用Redis批量设置,可以这样保存json格式的对象
对象名:对象id:属性名
作为Redis的key,属性值
作为Redis的value
先get再set,不存在则返回nil并设置新的值,如果存在则返回旧的值并设置新的值
#先get再set,返回的是get的值,值会被覆盖
getset key value
- 总结:Redis String类型可以用作计数器、统计多单位数量、浏览量、对象缓存等
List(列表)
基本数据类型,列表
Redis中,可以将list当做栈、队列、阻塞队列来使用,所有的list命令都用l开头
很多命令都是左右对称可以使用的,我们主要以左侧举例,右侧镜像可以自行测试
从头部(左侧)操作,赋值、取值,先进后出
#向key列表赋值,按照入栈顺序
listpush key element
#从key列表取值,按照出栈顺序,0到-1表示所有
lrange key start stop
从尾部(右侧)操作
lrange list start stop
当做数组使用,使用下标
#从数组key中取值,下标为index
lindex key index
获取队列长度,
同一个队列中可以有重复的值
移除队列中指定的值,如果这个值在队列中只有一个,移除个数就是1,如果有多个,可以指定移除多个这样的值
#移除队列中的值,可以指定移除个数
lrem list count element
截取指定区间的元素,通过下标,永久性修改了队列
ltrim list start stop
组合命令,可以实现上述功能组合后的效果
移除列表最后一个元素,或者说从右侧弹出一个元素,放到新的队列中,返回值就是弹出的那个元素
#移除最后(右侧)的元素,放在新的队列中,左侧入栈
rpoplpush source destination
判断队列是否存在,如果存在则,如果不存在则
#判断是否存在队列
exists key
#如果队列存在,则将指定索引出的值进行更新,如果队列不存在,则没有效果,如果队列存在索引不存在,也报错
lset list index element
在指定的元素(值,不是索引)处插入一个值,指定在前面插入,或在后面插入
linsert key before|after pivot element
总结:
- list实际上是一个链表,before node after,left,right 都可以插入值
- 如果key不存在,则创建新链表
- 如果key存在,则新增内容
- 如果移除所有值,成为空链表,代表不存在,
- 对于链表而言,在两端的值操作值效率最高,对中间的值操作效率略低
- 消息排队,lpush rpop 表示左进右出,相当于消息队列,lpush lpop 表示左进左出,相当于栈
Set(集合)
因为无序,所以存储的值不可重复,命令以s开头
添加集合元素,查看集合元素,判断某个元素是否存在
#向名为key的集合添加元素member
sadd key member
#显示集合key中所有元素
smembers key
#判断集合key中是否存在元素member
sismember key member
获取集合中元素个数
#获取集合key中元素的个数
scard key
移除集合中的指定元素
#移除集合key中的指定元素member
srem key member
随机取出指定个数的元素
#随机从集合key中抽选指定个数的元素,没有指定个数就是默认1
srandmember key [count]
删除元素
#随机移除指定个数的元素,没有指定个数就是默认1
spop key [count]
将集合中元素移动到另一个集合中
#将集合key中的元素member移动到新的集合destination中
smove key destination member
两个集合之间的运算,应用在比如,共同通讯好友,共同关注上
#集合key1中减去集合key2与集合key1的交集,得到的剩余部分,如果key2没有指定得到的还是完整的key1
sdiff key1 [key2]
#集合key1与集合key2做交集,如果key2没有指定,则得到完整的key1
sinter key1 [key2]
#集合key1与集合key2做并集,如果key2没有指定,则得到完整的key1
sunion key1 [key2]
Hash(HashMap集合、散列表、链表数组)
与String类型类似,只不过每个元素变成了一个键值对
Hash类型的命令以h开头
赋值、取值、键不可重复,如果键相同则新值覆盖旧值
#向集合key中存入键值对,键为field,值为value
hset key field value
#向集合key中存入多个键值对,field1-value1,field2-value2...
hmset key field1 value1 field2 value2
#根据键获取集合key中的多个值
hmget key field1 field2
#获取集合Key中的所有的键值对
hgetall key
删除
#根据键field删除集合key中的指定键值对
hdel key field
长度
#查看集合key的长度,元素个数
hlen key
判断
#根据键field判断集合key中是否存在该键值对
hexists key field
只获取键,只获取值
#只获取集合Key中的键
hkeys key
#只获取集合key中的值
hvals key
自增长
#设置集合key中的键值对中的值增长,并指定步长increment
hincrby key field increment
判断元素如果存在,如果不存在
#判断指定的键是否存在,如果不存在,则指定值,如果存在,则设置不成功
hsetnx key field value
存储对象,可变更数据,对象信息的保存,前面String类型也可以存对象,但是推荐使用Hash类型存储对象
如,将对象名:对象id
作为集合,属性名
作为键,属性值
作为值,可以同时给集合添加多个属性名、属性值
对象名:id
只是一种规范,自己也可设置其他规范
Zset(有序集合)
还是集合,在Set类型基础上,增加一个值score,值可以用来实现排序,命令以z开头
赋值、
#集合key添加元素member,元素的值为score
zadd key score member
#添加多个元素
zadd key score1 member1 score2 member2
#获取范围内的值,0到-1为全部的值,这里的范围编号和score不同,注意区分
zrange key start stop
#降序
zrevrange key start stop
排序
#根据score的大小,展示范围内的元素,-inf +inf 表示正无穷、负无穷,这个范围代表集合中的所有元素
#m默认升序,withscores表示结果中包含scores值
zrangebyscore key min max [witscores] [limit offset count]
移除,
#移除集合key中的元素member
zrem key member
统计
#统计集合key总的元素个数
zcard key
#根据score的区间范围,统计元素个数,范围含头含尾
zcount key min max
使用场景:需要set排序,如成绩表,薪资表,需要使用权重判断,排行榜等
2.三种特殊数据类型
geospatial
定位、附近人、地理位置推算距离,
http://www.redis.cn/commands/geoadd.html
添加地理位置,南北极无法直接添加,通常会下载城市数据直接导入
地理数据与实际地图吻合不可乱写,否则会计算错误
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
- 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
#向集合key中添加元素member的经度longitude、纬度latitude,可以一次添加多个位置
geoadd key longitude latitude member
#获取地理位置
geopos key member
定位之间距离
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺
#返回两个位置之间的距离
geodist key member1 member2 [m|km|mi|ft]
获取半径内的元素
#以给定经纬度longitude latitude为中心,找出半径radius内的元素,
#可以选择是否返回元素到中心的距离、经纬度、限制查询的个数
georadius key longitude latitude radius [m|km|mi|ft] [withdist] [withcoord] [count count]
根据已有的元素,筛选范围内其他元素
#给定一个元素,查找半径内的其他元素
#可以选择是否返回查找的元素到中心的距离、经纬度、限制查询的个数
georadiusbymember key member radius [m|km|mi|ft] [withdist] [withcoord] [count count]
返回元素的Hash,该命令将返回11个字符的Geohash字符串,不常用
geohash key member1 member2
geo底层实现原理还是zset,所以可也以使用zset命令来操作地理位置,如查看全部元素、移除元素
Hyperloglogs
基数:指一个集合中不重复的元素的个数
Redis HyperLogLog 是用来做基数统计的算法
用途:同一个人访问多次,可以算作一个访问人。
传统的方式使用set集合,统计id,如果用户数据量很大就会比较麻烦,消耗资源,
而Redis HyperLogLog 底层数据结构就是去重统计基数,占用内存非常小,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,但是有0.81%的错误率,统计UV时可以忽略不计
新增,统计
#添加元素
pfadd key member [member]
#统计集合元素个数
pfcount key
#统计多个集合总的元素个数,也会自动去重
pfcount key [key]
合并,去重
#合并多个集合,自动去重复元素,合并之后的集合替换掉destkey,sourcekey不变
pfmerge destkey sourcekey [sourcekey]
Bitmaps
在开发中,可能会遇到这种情况:需要统计用户的某些信息,如活跃或不活跃,登录或者不登录;又如需要记录用户一年的打卡情况,打卡了是1, 没有打卡是0,如果使用普通的 key/value存储,则要记录365条记录,如果用户量很大,需要的空间也会很大,所以 Redis 提供了 Bitmap 位图这中数据结构,Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1;如果要记录 365 天的打卡情况,使用 Bitmap 表示的形式大概如下:0101000111000111…,这样有什么好处呢?当然就是节约内存了,365 天相当于 365 bit,又 1 字节 = 8 bit , 所以相当于使用 46 个字节即可
通俗理解:
- 当我们使用标记存储两种状态时,如果用一个变量存一个标记太浪费资源,现在将所有标记统一放在一条二进制数据上,每一位都用0或1对应一个标记,大大节约空间
bitmap位图这种数据结构,使用二进制操作数据,
新增、查看、统计
#向key中存入数据,offet相当于下标,记录位置,从0开始,value只能是0和1,对应我们所需的状态
setbit key offet value
#查看指定位置的状态
getbit key offet
#统计二进制数据上1的个数,可以加范围,start 和 end 参数的设置和 GETRANGE 命令类似,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,以此类推
bitcount key [start end]
3.Redis 事务
事物的本质是一组命令一块执行,事务执行是,里面的命令按照顺序执行,一次性,顺序性,排他性
Redis事务没有隔离级别的概念,因为所有命令在事务中没有直接执行,只有发起命令的时候才会执行exec
Redis单条命令保证原子性,但Redis整个事务不保证原子性,原因见下面
- 开启事务
multi
- 命令入队
- 执行事务
exec
- 放弃事务
discard
- 编译异常
开启事务后,当出现代码异常,直至提交事务,这段期间的命令都不会执行,
这种情况在Java中比较少见,因为Java操作Redis基本不会出现代码异常
- 运行异常
开启事务后,执行命令,提交事务,如果运行期间出现问题,除了异常命令,其他命令正常执行
所以说,Redis单条命名保证原子性,但整个Redis事务无法保证原子性
结论:
Redis单条命令保证原子性,因为Redis是单线程
Redis整个事务不保证原子性,因为开启事务时并没有执行命令,而是提交之后统一执行
这与MySQL截然不同,注意区分
4.Redis实现乐观锁
悲观锁:一开始就认为会出现问题,所以什么时候都加锁
乐观锁:认为不会出现问题,只有在需要的时候才会判断是否有必要加锁
Redis使用监视命令 watch
,配合Redis事务一起使用
正常执行成功时
模拟多线程问题
第一个线程,监视数据,开启事务,但没有提交
打开新窗口,新客户端同样连接6379,模拟多线程操作,修改被监视的数据
然后第一个线程提交事务,发现被监视的元素被篡改,返回nil表示当前线程的整个事务提交失败
数据按照另一个线程修改为准
Redis的这种监视事务的机制,相当于实现了Redis乐观锁
当监视出现事务执行失败后,先取消监视unwatch key
,再去重新开始监视,开启事务
五、Java 操作 Redis
1.Jedis 官方原生API
概念
Redis官方推荐的Java开发工具,实际中可能不常用,但是与Redis原生API关联非常紧密,一定要熟练掌握
项目、依赖
在maven项目中导入Jedis依赖,maven镜像搜索坐标,地址:https://mvnrepository.com/
导入jedis
fastjson
依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
连接、测试
开启redis服务端,默认端口号6379
创建Jedis对象,添加连接参数,测试连接
注意:如果是连接Linux中的Redis,先将redis.conf配置文件中的IP绑定注释掉,保护模式关闭,Jedis才能正产连接
public class demo1 {
public static void main(String[] args) {
//创建对象,连接
Jedis jedis = new Jedis("192.168.126.130", 6379);
//连接测试
System.out.println(jedis.ping());
}
}
常用API
Jedis的所有命令都是基于Redis原生命令,在这里命令变成了方法
Key操作
public class demo2 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.130", 6379);
System.out.println("清空当前数据库: "+jedis.flushDB());
System.out.println("判断: "+jedis.exists("k1"));
System.out.println("添加: "+jedis.set("k1", "v1"));
System.out.println("添加: "+jedis.set("k2", "v2"));
Set<String> keys = jedis.keys("*");
System.out.println("列出所有key: "+keys);
System.out.println("删除: "+jedis.del("k1"));
System.out.println("判断: "+jedis.exists("k1"));
System.out.println("查看键对应的值的类型: "+jedis.type("k2"));
System.out.println("随机返回一个key: "+jedis.randomKey());
System.out.println("重命名key: "+jedis.rename("k2", "k3"));
System.out.println("获取元素: "+jedis.get("k3"));
System.out.println("根据索引查询: "+jedis.select(0));
System.out.println("清空当前数据库: "+jedis.flushDB());
System.out.println("数据库key数量: "+jedis.dbSize());
System.out.println("清空所有数据库的key: "+jedis.flushAll());
}
}
String类型
public class demo3 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.130", 6379);
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("删除: "+jedis.del("k2"));
System.out.println("取值: "+jedis.get("k2"));
System.out.println("修改值: "+jedis.set("k1", "v11"));
System.out.println("取值: "+jedis.get("k1"));
System.out.println("追加值: "+jedis.append("k3", "end"));
System.out.println("取值: "+jedis.get("k3"));
System.out.println("添加多个元素: "+jedis.mset("k4","v4","k5","v5"));
System.out.println("获取多个值: "+jedis.mget("k4","k5","k6"));
System.out.println("删除多个元素: "+jedis.del("k4","k5"));
System.out.println("获取多个值: "+jedis.mget("k3","k4","k5"));
System.out.println("清空: "+jedis.flushDB());
System.out.println("新增键值对,防止被覆盖");
System.out.println("第一次赋值: "+jedis.setnx("k1", "v1"));
System.out.println("第一次赋值: "+jedis.setnx("k2", "v2"));
System.out.println("覆盖原值: "+jedis.setnx("k2", "v2new"));
System.out.println("取值: "+jedis.get("k1"));
System.out.println("取值: "+jedis.get("k2"));
System.out.println("添加元素设置生命周期: "+jedis.setex("k3", 2, "v3"));
System.out.println("取值: "+jedis.get("k3"));
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("过期后取值: "+jedis.get("k3"));
System.out.println("获取原值,并更新: "+jedis.getSet("k2", "v2abc"));
System.out.println("获取新的值: "+jedis.get("k2"));
System.out.println("截取部分字符串: "+jedis.getrange("k2", 2, 4));
}
}
List集合
public class demo4 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.130", 6379);
System.out.println("清空数据: "+jedis.flushDB());
System.out.println("新增元素");
System.out.println(jedis.lpush("list", "a","b","c","d"));
System.out.println(jedis.lpush("list", "d"));
System.out.println(jedis.lpush("list", "e"));
System.out.println(jedis.lpush("list", "f"));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("查看区间数据: "+jedis.lrange("list", 0, 3));
System.out.println("删除列表中指定的值,第二参数为删除的个数(有重复时),后进先出原则,相当于出栈");
System.out.println("删除指定值的元素: "+jedis.lrem("list", 2, "d"));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("删除区间以外的元素: "+jedis.ltrim("list", 0, 3));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("左侧弹出: "+jedis.lpop("list"));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("右侧新增: "+jedis.rpush("list", "g"));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("右侧弹出: "+jedis.rpop("list"));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("修改指定下标元素: "+jedis.lset("list", 1, "new"));
System.out.println("查看所有: "+jedis.lrange("list", 0, -1));
System.out.println("队列长度: "+jedis.llen("list"));
System.out.println("获取指定下标元素: "+jedis.lindex("list", 2));
System.out.println("新增元素: "+jedis.lpush("list1", "4","2","0","6","5","8"));
System.out.println("查看所有: "+jedis.lrange("list1", 0, -1));
System.out.println("排序: "+jedis.sort("list1"));
System.out.println("查看所有: "+jedis.lrange("list1", 0, -1));
}
}
Set集合
public class demo5 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.130",6379);
jedis.flushDB();
System.out.println("新增元素,不可重复");
System.out.println(jedis.sadd("set1", "s0","s1","s2","s3","s4","s5","s7","s8"));
System.out.println(jedis.sadd("set1", "s6"));
System.out.println(jedis.sadd("set1", "s6"));
System.out.println("列出所有元素: "+jedis.smembers("set1"));
System.out.println("删除一个元素: "+jedis.srem("set1", "s0"));
System.out.println("列出所有元素: "+jedis.smembers("set1"));
System.out.println("删除元素: "+jedis.srem("set1", "s6","s7"));
System.out.println("随机移除一个元素: "+jedis.spop("set1"));
System.out.println("随机移除一个元素: "+jedis.spop("set1"));
System.out.println("列出所有元素: "+jedis.smembers("set1"));
System.out.println("查看元素个数: "+jedis.scard("set1"));
System.out.println("判断是否存在某个元素: "+jedis.sismember("set1", "s3"));
System.out.println("判断某个元素是否存在: "+jedis.sismember("set1", "s1"));
System.out.println(jedis.sismember("set1", "s5"));
System.out.println("================");
System.out.println("新增: "+jedis.sadd("set2", "s0","s1","s2","s4","s5","s7","s8"));
System.out.println("新增: "+jedis.sadd("set3", "s0","s1","s2","s4","s8"));
System.out.println("set2中删除,并存到另一集合上 : "+jedis.smove("set2", "set3", "s1"));
System.out.println("set2中删除,并存到另一集合上 : "+jedis.smove("set2", "set3", "s1"));
System.out.println("删除元素并放在新集合中 :"+jedis.smove("set2", "set3", "s2"));
System.out.println("列出所有元素: "+jedis.smembers("set2"));
System.out.println("列出所有元素: "+jedis.smembers("set3"));
System.out.println("交集: "+jedis.sinter("set2","set3"));
System.out.println("并集: "+jedis.sunion("set2","set3"));
System.out.println("差集: "+jedis.sdiff("set2","set3"));
System.out.println("交集保存到集合中: "+jedis.sinterstore("set4", "set2","set3"));
System.out.println("列出所有元素: "+jedis.smembers("set4"));
}
}
Hash
public class demo6 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.130",6379);
System.out.println("清空当前数据库");
jedis.flushDB();
Map<String,String> map = new HashMap();
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
map.put("k4", "v4");
System.out.println("添加map集合");
jedis.hmset("hash", map);
System.out.println("添加元素: "+jedis.hset("hash", "k5","v5"));
System.out.println("所有键值对: "+jedis.hgetAll("hash"));
System.out.println("所有键: "+jedis.hkeys("hash"));
System.out.println("所有值: "+jedis.hvals("hash"));
System.out.println("给一个键保存整数,如果该键不存在则添加: "+jedis.hincrBy("hash", "k6", 6));
System.out.println("所有键值对: "+jedis.hgetAll("hash"));
System.out.println("添加或修改: "+jedis.hincrBy("hash", "k6", 60));
System.out.println("所有键值对: "+jedis.hgetAll("hash"));
System.out.println("删除一个或多个元素: "+jedis.hdel("hash", "k2"));
System.out.println("所有元素: "+jedis.hgetAll("hash"));
System.out.println("元素个数: "+jedis.hlen("hash"));
System.out.println("判断元素是否存在: "+jedis.hexists("hash", "k2"));
System.out.println("判断元素是否存在: "+jedis.hexists("hash", "k3"));
System.out.println("根据键获取值: "+jedis.hmget("hash", "k3"));
System.out.println("根据键获取值: "+jedis.hmget("hash", "k3","k4"));
}
}
Jedis 事务
使用 multi 方法
public class demo7 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.126.130",6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("hi", "Java");
jedis.flushDB();
System.out.println("开启事务");
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("k1", result);
multi.set("k2", result);
// 模拟异常
// int i = 1/0;
multi.exec();//提交事务
} catch (Exception e) {
multi.discard();//回滚事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("k1"));
System.out.println(jedis.get("k2"));
jedis.close();//关闭连接
}
}
}
成功
失败
如果使用watch方法,可以监控,在try catch中进行判断监控结果,根据情况抛异常
2.SpringBoot 整合 Redis
SpringData 用来操作各种数据库的整合
项目准备
新建springboot项目模块,勾选Redis依赖,其他根据需要
删除模块里的这些文件,暂时不需要
pom文件依赖
java web, redis, fastjson
注意:
- 有些版本springboot连接redis使用jedis,有些使用lettuce
- jedis采用直连,多个线程下不安全,使用连接池可以解决
- 采用netty,实例可以在多个线程中共享,没有现成安全情况,像NIO模式,性能高一些
- 使用连接池尽量选用lecttuce的,功能更强大,支持的类更多
主配置文件
Redis地址,端口号
Redis测试
先通过redistemplate对象调出所需的Redis操作对象,再进一步调用数据库方法API
进入Redistemplate源码,可以看到所有的操作API
序列化问题
对象存入Redis之前需要序列化,否则可能出现中文乱码
使用jdk自带序列化方案
@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User implements Serializable {
private Integer age;
private String name;
}
测试存入Redis
@Test
void test1() throws Exception{
User user = new User(18,"螺蛳粉");
String jsonUser = new ObjectMapper().writeValueAsString(user);//对象转json字符串
redisTemplate.opsForValue().set("user", jsonUser);//存入set集合
Object user1 = redisTemplate.opsForValue().get("user");
System.out.println(user1);
}
自己配置序列化方案,准备配置类,自己配置bean对象,公司常用这个模板
@Configuration
public class RedisConfig {
//手动修改redisTemplate,常见的固定模板
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 连接
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 创建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);
// value采用jackson序列化配置
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash key采用string序列化配置
template.setHashKeySerializer(stringRedisSerializer);
// hash value采用value序列化方案
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
注入RedisTemplate时选择我们配置的Bean对象
运行测试,查看Redis数据库,
这样使用我们自己定义的Redis序列化配置,更好用,如果不配置默认使用jdk自带序列化方案,可能会出现中文乱码或者转义斜杠等问题
自己写工具类
- 实际生产中通常不直接使用RedisTemplate原生API,而是自己准备RedisUtils工具类,需要使用时直接注入属性,调用方法
如,
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 设置失效时间
public boolean expire(String key, long time) {
try {
if (time>0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 获取过期时间
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
// 判断key是否存在
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 删除缓存
public void del(String... key) {
if (key != null && key.length>0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(String.valueOf(CollectionUtils.arrayToList(key)));
}
}
}
// 添加值
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 取值
public Object get(String key, Object value) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
// 存入普通缓存设置时间
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 设置递增
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException();
}
return redisTemplate.opsForValue().increment(key, delta);
}
// 设置递减
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException();
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// Hash get
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
// Hash get 获取对应的所有值
public Map<Object,Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
// Hash set
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// Hash set 并设置时间
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 存入数据,如果不存在则新增
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 存入数据,设置时间,如果不存在则新增
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 删除
public void hdel(String key, Object... item){
redisTemplate.opsForHash().delete(key, item);
}
// 判断是否有该值
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
// 存入值,如果不存在则新增,并设置时间
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
// 设置递减
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// set 获取值
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// 查询是否存在
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 存入值
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// 存入职,并设置时间
public long sSet(String key, long time, Object... values) {
try {
long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// 获取值的数量
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// 移除值
public long setRemove(String key, Object... values) {
try {
long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// list相关API
}
六、Redis 高级
1.redis.conf 配置文件
redis.conf 配置了Redis的详细参数,Redis启动需要使用配置文件
进入 redis.conf 编辑状态,分析配置参数,vim redis.conf
规定单位,单位书写对大小写不敏感
可以包含其他配置文件,将多个配置文件组合起来
NETWORK 网络配置,重点
绑定IP,只能使用指定的IP地址链接
bind 127.0.0.1
保护模式 yes/no,保护模式下只能本机连接
protected-mode no
连接端口号
port 6379
GENERAL 通用配置,
是否后台运行 yes/no
daemonize yes
管理守护进程的,一般不用动
# supervised auto
如果选择后台运行,则指定一个pid进程文件
pidfile /var/run/redis_6379.pid
日志级别,注释里有详解极少各个级别的用途
loglevel notice
生成日志的位置,空则使用默认
logfile ""
默认数据库数量 16
databases 16
是否显示启动logo
always-show-logo no
SNAPSHOTTING 快照
持久化策略,如,这里表示,
save 900 1 #900秒内,至少有一个key发生修改,则进行持久化操作
save 300 10 #300秒内,至少有10个key发生修改,则进行持久化
save 60 10000 #60秒内,至少有10000个key发生修改,则进行持久化
持久化出现错误,是否继续工作
stop-writes-on-bgsave-error yes
是否压缩rdb文件,会消耗一定cpu资源
rdbcompression yes
保存rdb文件时,是否进行校验
rdbchecksum yes
持久化文件存放目录,默认在当前文件目录
dir ./
REPLICATION 主从复制,后面详解
SECURITY 安全
设置密码,默认没有
# requirepass foobared
在Redis客户单,执行命令可以操作当前密码,
如果设置密码后发现操作无权限,只需要在客户端验证密码即可 auth 密码
CLIENTS 客户端设置
最大客户端连接数,默认10000
# maxclients 10000
MEMORY MANAGEMENT 内存设置
Redis最大内存
# maxmemory <bytes>
内存达到上限的处理策略
- volatile-lru:只对设置了过期时间的key进行LRU(默认值)
- allkeys-lru : 删除lru算法的key
- volatile-random:随机删除即将过期key
- allkeys-random:随机删除
- volatile-ttl : 删除即将过期的
- noeviction : 永不过期,返回错误
# maxmemory-policy noeviction
APPEND ONLY MODE aof的持久化配置,后面详解
默认不开启aof,而是使用了 rdb
appendonly no
持久化文件名
appendfilename "appendonly.aof"
同步的频率,always每次修改都同步,比较消耗性能,everysec每秒一次,也可能有数据丢失,no不同步
# appendfsync always
appendfsync everysec
# appendfsync no
2.Redis 持久化
RDB(Redis DataBase)
根据配置文件的持久化参数,定期将内存数据写入磁盘,即定期拍快照,恢复时将快照读到内存中
Redis持久化使用一个单独的子进程fork,主进程不进行任何IO操作,确保了Redis的高性能,因此RDB模式效率更高,对于数据完整性不是很敏感的情况下大多使用RDB持久化策略。
唯一的缺点就是在拍快照的间隙如果出现宕机,可能会有数据丢失
rdb保存文件,默认名dump.rdb,与redis.conf在同一个目录
生产环境下,我们会定期备份RDB文件
配置RDB持久化策略
触发机制
- save规则满足条件下,可以触发
- 执行flushdb,可以触发
- 退出redis,可以触发
恢复数据
- 只需将rdb文件放在redis启动目录下,redis启动时可以自动识别读取
- 查看启动目录,在客户端执行
config get dir
优点
- 适合大规模数据恢复
- 对数据完整性要求不高
缺点
- 持久化操作有时间间隔,如果redis以外宕机,最后一次操作修改的数据会丢失
- fork进程运行时,会占用一定内存空间
AOF(Append Only File)
直译,追加文件,将所有命令记录下来,恢复时将里面的命令全部执行一遍,所以Redis重启就会将该持久化记录里面的指令从到位执行一遍,比较麻烦
配置文件
默认不开启,手动开启,需要设置
appendonly yes
默认声明文件名
appendfilename "appendonly.aof"
追加记录的频率
# appendfsync always
appendfsync everysec
# appendfsync no
其他配置一般使用默认,有需要可以查询
修改配置文件,开启AOF,重启Redis服务端,即可生效,持久化文件默认保存在Redis启动目录下,
执行一段Redis数据命令,查看文件目录,打开持久化文件
如果我们修改了这个日志文件,会影响数据的恢复,可以使用 自动检测修复aof文件,
执行命令
redis-check-aof --fix appendonly.aof
重写规则,当aof文件超过配置参数的大小(这里是64mb),Redis就会开启一个新的进程,重写一个新的aof文件
aof配置中默认文件无限追加,会导致文件越来越大
no-appendfsync-on-rewrite no
如果配置重写,可以指定文件多大触发重写新的aof
auto-aof-rewrite-min-size 64mb
优点
- 每次修改都记录,确保文件完整性,注意需要将持久化策略改为always
缺点
- 持久化文件远大于RDB文件,修复速度也更慢
- 开启AOF持久化策略下,Redis运行效率也会比RDB模式慢
使用策略
- 实际生产中会开启主从复制,rdb策略放在从机上,用来起到备份作用
Redis 持久化总结
1.RDB持久化方式能够在指定的时间间隔内对数据进行快照存储
2.AOF持久化方式记录每次对服务器写的操作,当服务器重启时会重新执行这些命令来恢复原始数据,AOF命令以Redis协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,避免AOF文件体积过大
3.如果只是使用Redis作为服务器运行的缓存,其实也可以不必使用持久化
4.同时开启两种持久化方式
- Redis重启会优先载入AOF文件来恢复原始数据,因为保存的数据更完整
- RDB的数据不实时,但是也推荐开启。因RDB更适合用于备份数据库,AOF不断变化不好备份,RDB重启速度快,没有AOF潜在的bug
5.性能建议
- RDB文件通常作为后备,建议只在Slave上持久化RDB文件,而且只需要15分钟备份一次就够了,只保留save 900 1这条规则
- 如果开启AOF,好处是在最糟糕的情况下,也只会丢失不超过2秒的数据,启动脚本也简单;但是产生了一个持续的IO,还有,在rewrite重写过程中,将新的数据写入新文件不可避免会造成阻塞,所以只要硬盘允许,尽量减小重写频率,默认是64mb,可以设置到5g以上,超过原大小100%触发重写也可以适当提升数值
- 如果不使用AOF,只靠Master-Slave Replication 时间高可用性也可以,性能节省一大笔IO,减少rewrite带来的波动,代价是,如果主机从机同时宕机,会丢失十几分钟的数据,启动脚本也会比较主机从机中的RDB文件,选择比较新的
3.Redis 发布订阅
Redis发布订阅是一种消息通信模式,发送者发送消息,订阅者接收消息
Redis客户端可以接收任意数量的频道
要素:消息发送者、频道、消息接收者
命令描述
测试使用
订阅频道,c1
开启一个新的窗口,连接服务端,发送消息
接收端会实时接收消息,并显示
总结
- 发布者向频道发送消息
- 接收者订阅频道,实施接收频道消息
- redis-server底层维护一个字典,字典的键就是每个频道,字典的值是一个链表,链表中保存了所有订阅这个频道的客户端,当字典的一个键上发布消息后,值对应的所有客户端都会接收消息,最明显的用法就是具有实时性,如,即时聊天,群聊、关注等,
- 进一步复杂的场景一般使用消息中间件 MQ
4.Redis 主从复制
概念
默认情况下,每台Redis服务器都一个主节点
通常主节点用来读操作,从机用来写操作,
主从复制作用:
- 数据冗余,实现了数据双击热备,是持久化之外的一种冗余方式
- 故障恢复,主节点出现问题,从节节点可以提供服务,快速恢复
- 负载均衡,多个从节点可以实现负载均衡,分担访问压力,提高Redis服务器的并发量
- 高可用,主从复制是哨兵模式、Redis集群实施的前提
结构上,Redis服务器可能发生故障,一台服务器不够用,且访问压力大;
容量上,单台服务器内存容量有限,也不应当将所有内存用作Redis存储,通常,单台Redis最大使用内存不应超过20G
配置
只需要配置从机,主机默认就是,不需要单独配置
以默认方式启动Redis服务,连接客户端,查看主从复制信息
可得知当前服务身份为主机,从机连接数为0,
Redis集群的搭建至少需要三台服务器
Redis的启动需要配置文件,因此我们准备多个不同的配置文件,分别根据各个配置文件启动,就可以得到多个Redis服务器
准备四个连接窗口,一个观察,三个模拟三台Redis服务器
拷贝redis.conf配置文件,
分别进入各个配置文件,修改配置信息,这里以redis79.conf为例
端口号
后台运行
pid
日志
rdb
保存退出,80,81根据各自端口号修改
启动
修改完配置文件,分别用6379,6380,6381配置文件启动Redis服务端
当前状态每台服务器都是主节点
三个窗口分别连接6379,6380,6381客户端,查看连接信息
通常,从机不用改,只需配置从机,指定从机的主机即可
给从机指定主机,从机客户端中使用命令,80,81都执行
slaveof 127.0.0.1 6379
查看主机信息,多了两个从机
以上方式通过命令搭建主从,这是暂时的,实际生产中应该通过配置文件搭建主从,这是永久操作,开机即按照主从信息启动
注意事项
- 主机可以写,从机不能写只能读,写了也会报错
- 主从复制状态下,主机中的所有信息和数据,都会自动被从机保存
测试
- 主机宕机,主从关系没有变化,从机依旧无法写,主机恢复,主从关系不变,从机依旧无法写
- 从机宕机,重启后变回主机,如果重新指定成刚才的从机,依旧可以获取主机所有数据,包括宕机期间主机的操作
复制原理
- slave启动成功连接到master后会发送一个sync同步命令
- master接到命令,启动后台存盘进程,同时收集所有接收到的修改数据的命令,后台进程执行完毕后,master将传送整个数据文件给slave,并完成一次完全同步
- 全量复制:slave在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:master继续将新的所有手机到的修改命令一次传给slave,完成同步
- 只要是重新连接master,一次完全同步(全量复制)就会自动执行,从机恢复所有数据
主从链路
79做主机,80做79的从机,81做80的从机
此时的80依旧是从机,但是81依旧可以获取全部数据
这个模型下,如果79宕机,可以指定其中一个从机成为主机,进入一个从机客户端,执行命令,这个从机就会成为主机
slaveof no one
然后,其他从机再指定这个新的主机
最初的主机重新启动,也失去之前那些从机了
5.哨兵模式(重点)
概念
之前的主从切换,当主机宕机后,需要手动将一台服务器切换为主机,费事费力,服务器也会有一段时间不可用,
Redis从2.8开始出现哨兵模式,Redis提供了哨兵命令,哨兵是一个独立的进程,会独立运行,哨兵通过发送命令,等待Redis服务器响应,从而监控多个Redis实例
后台监控主机是否故障,如果宕机,将根据投票自动将从机切换为主机
模型
基本模型
为了确保哨兵也高可用,可以使用多哨兵模式
如果主机宕机,哨兵1先发现,系统不会马上进行切换,只是哨兵1主观认为,这个现象叫做主观下线。如果后续其他哨兵也发现主机不可用,且数量达到一定值时,哨兵之间就会进行投票,投票结果由一个哨兵发起,进行故障转移操作,切换成功后,通过发布订阅模式,通知各个哨兵把监控的从机切换为主机,者叫客观下线。
配置
当前主从状态,79主机,80,81从机
在配置文件的目录中,新建哨兵配置文件sentinel.conf
sentinel monitor不能变,sen1是名字自己取,127.0.0.1 6379 是监控的主机,1表示如果主机宕机,哨兵会进行投票
这是最基本的配置,完成后,保存退出
sentinel monitor sen1 127.0.0.1 6379 1
启动哨兵
哨兵启动,根据配置文件,当前监控的6379成为主机,还有两个从机6380,6381
测试
打开新窗口,模拟主机6379宕机,进入6379客户端,shutdown
过一段时间,哨兵会通过心跳检测机制发现主机宕机,开始投票,推选出新的主机,这个例子中6381成了新主机
查看6381服务器信息,也验证了6381是新主机
这里投票的机制,我们暂且不讨论,但是一定会从生下的从机选出主机
如果之前的主机6379重新连接,发现自己只能作为6381的从机,观察哨兵的投票记录我们可以看到,其实6379下线时,就已经沦为从机的位置了
登录6379客户端,查看信息
优点:
- 哨兵集群,基于主从复制模式,拥有其全部优点
- 可以主从切换,实现故障转移
- 哨兵模式就是主从模式的升级版,实现自动切换,更加健壮
缺点
- Redis不好实现在线扩容,集群容量一旦到达上限,在线扩容十分困难
- 实现哨兵模式配置的过程其实比较麻烦,我们的举例只是最简单的
- 真正的全部配置,有很多,如果有哨兵集群更加复杂,
后续补充哨兵的详细配置
6.Redis 缓存穿透、击穿、雪崩(面试重点)
缓存穿透
概念
用户查询数据,发现Redis内存数据库中没有,也就是缓存没有命中,请求就会想持久层数据库查询,也没有,查询失败;
当用户量很大,或者恶意攻击,大量的请求进入到持久层数据库,会给数据库造成很大的压力,形成了缓存穿透
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先校验,不符合则丢弃,避免对底层数据库直接查询
具体原理可以自行查询
缓存空对象
当存储层不命中后,即使返回一个空对象,也可以将其缓存起来,同时设置一个过期时间,在一定时间内也可以将请求拦截在缓存中
缺点
- 存储空对象,以为这缓存中需要存储更多的键值对,浪费资源
- 即使空对象缓存有生命周期,到了过期时间,仍然会出现缓存层和存储层数据不一致,对业务也有影响
缓存击穿
概念
某些数据key非常热点,不停抗住大量并发,由于某种原因(宕机或过期),当这个key在失效的瞬间,持续大量的并发会将缓存击穿,直接请求数据库,导致数据库瞬间压力过大
缓存穿透与缓存击穿区别:穿透是查不到,击穿是本来能查到,突然失效了,发生穿透
解决方案
设置热点数据永不过期
也有缺点,最终还是需要定期处理
加互斥锁
分布式锁:使用分布式锁,确保每个key同时只有一个线程去查询后端服务,其他线程没有获取分布式锁的权限,只能等待,这种方式将高并发的压力转移到了分布式锁上面,对分布式锁的考验很大
缓存雪崩
概念
在某一个时间段内,大量缓存集中失效(比如宕机、过期),导致访问查询都落到数据库上,对于数据库而言,产生了周期性的压力波峰,存储层调用量暴增,造成存储层挂掉
解决方案
Redis高可用
搭建Redis集群
大厂比如阿里,在双十一会停掉部分业务,保障主业务
限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,如,一个key只允许一个线程查询和写缓存,其他线程等待
数据预热
正式部署之前,先把可能的数据访问一遍,可以使一大部分访问的数据加载到缓存中,提前加载不同的数据;
设置不同的过期时间,让缓存失效的时间均匀分散
部分资料来源于网络,版权属其原著者所有,只供学习交流之用。如有侵犯您的权益,请联系【公众号:码农印象】删除,可在下方评论,亦可邮件至ysluckly.520@qq.com。互动交流时请遵守宽容、换位思考的原则。