一、Nosql概述
为什么使用Nosql
1、单机Mysql时代
90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题
- 数据量增加到一定程度,单机数据库就放不下了
- 数据的索引(B+ Tree),一个机器内存也存放不下
- 访问量变大后(读写混合),一台服务器承受不住。
2.Memcached(缓存) + Mysql + 垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BFoSuIHd-1648450745517)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220306111054229.png)]
优化过程经历了以下几个过程:
- 优化数据库的数据结构和索引(难度大)
- 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了
- MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。
3、分库分表 + 水平拆分 + Mysql集群
4、如今最近的年代
如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。
目前一个基本的互联网项目
为什么要用NoSQL ?
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用NoSQL数据库的,Nosql可以很好的处理以上的情况!
什么是Nosql
NoSQL = Not Only SQL(不仅仅是SQL)
Not Only Structured Query Language
关系型数据库:列+行,同一个表下数据的结构是一样的。
非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。
NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。
Nosql特点
- 方便扩展(数据之间没有关系,很好扩展!)
- 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样型的!(不需要事先设计数据库,随取随用)
- 传统的 RDBMS 和 NoSQL
传统的 RDBMS(关系型数据库)
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
- ...
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展
- ...
了解:3V + 3高
大数据时代的3V :主要是描述问题的
- 海量Velume
- 多样Variety
- 实时Velocity
大数据时代的3高 : 主要是对程序的要求
- 高并发
- 高可扩
- 高性能
真正在公司中的实践:NoSQL + RDBMS (关系型数据库)一起使用才是最强的。
阿里巴巴演进分析
推荐阅读:阿里云的这群疯子https://yq.aliyun.com/articles/653511
# 商品信息
- 一般存放在关系型数据库:Mysql,阿里巴巴使用的Mysql都是经过内部改动的。
# 商品描述、评论(文字居多)
- 文档型数据库:MongoDB
# 图片
- 分布式文件系统 FastDFS
- 淘宝:TFS
- Google: GFS
- Hadoop: HDFS
- 阿里云: oss
# 商品关键字 用于搜索
- 搜索引擎:solr,elasticsearch
- 阿里:Isearch 多隆
# 商品热门的波段信息
- 内存数据库:Redis,Memcache
# 商品交易,外部支付接口
- 第三方应用
Nosql的四大分类
KV键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度:Redis + Memcache
文档型数据库(bson数据格式):
- MongoDB(掌握)
- 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
- MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的,NoSQL中最像关系型数据库的数据库。
- ConthDB
列存储数据库
- HBase(大数据必学)
- 分布式文件系统
图关系数据库
用于广告推荐,社交网络
- Neo4j、InfoGrid
分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值对(key-value) | Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | Key 指向 Value 的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB, MongoDb | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群 |
二、Redis入门
概述
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能该干什么?
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
- 。。。
特性
-
多样的数据类型
-
持久化
-
集群
-
事务
…
环境搭建
官网:https://redis.io/
中文网:http://www.redis.cn/
推荐使用Linux服务器学习。
windows版本的Redis已经停更很久了…
Windows安装
https://github.com/dmajkic/redis
- 解压安装包
- 开启redis-server.exe
- 启动redis-cli.exe测试
Linux安装
-
下载安装包!
redis-5.0.8.tar.gz
-
解压Redis的安装包!程序一般放在
/opt
目录下 -
基本环境安装
yum install gcc-c++ # 然后进入redis目录下执行 make # 然后执行 make install 12345
4.redis默认安装路径 /usr/local/bin
5.将redis的配置文件复制到 程序安装目录 /usr/local/bin/kconfig
下
6.redis默认不是后台启动的,需要修改配置文件!
7.通过制定的配置文件启动redis服务
8.使用redis-cli连接指定的端口号测试,Redis的默认端口6379
9.查看redis进程是否开启
10.关闭Redis服务 shutdown
11.再次查看进程是否存在
12.后面我们会使用单机多Redis启动集群测试
测试性能
**redis-benchmark:**Redis官方提供的性能测试工具,参数选项如下:
简单测试:
# 测试:100个并发连接 100000请求
./redis-benchmark -h localhost -p 6379 -c 100 -n 100000
基础知识
redis默认有16个数据库
默认使用的第0个;
16个数据库为:DB 0~DB 15
默认使用DB 0 ,可以使用select n
切换到DB n,dbsize
可以查看当前数据库的大小,与key数量相关。
127.0.0.1:6379> config get databases # 命令行查看数据库数量databases
1) "databases"
2) "16"
127.0.0.1:6379> select 8 # 切换数据库 DB 8
OK
127.0.0.1:6379[8]> dbsize # 查看数据库大小
(integer) 0
# 不同数据库之间 数据是不能互通的,并且dbsize 是根据库中key的个数。
127.0.0.1:6379> set name sakura
OK
127.0.0.1:6379> SELECT 8
OK
127.0.0.1:6379[8]> get name # db8中并不能获取db0中的键值对。
(nil)
127.0.0.1:6379[8]> DBSIZE
(integer) 0
127.0.0.1:6379[8]> SELECT 0
OK
127.0.0.1:6379> keys *
1) "counter:__rand_int__"
2) "mylist"
3) "name"
4) "key:__rand_int__"
5) "myset:__rand_int__"
127.0.0.1:6379> DBSIZE # size和key个数相关
(integer) 5
keys *
:查看当前数据库中所有的key。
flushdb
:清空当前数据库中的键值对。
flushall
:清空所有数据库的键值对。
Redis是单线程的,Redis是基于内存操作的。
所以Redis的性能瓶颈不是CPU,而是机器内存和网络带宽。
那么为什么Redis的速度如此快呢,性能这么高呢?QPS达到10W+
Redis为什么单线程还这么快?
- 误区1:高性能的服务器一定是多线程的?
- 误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
核心:Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。
三、五大数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
Redis-key
在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
下面学习的命令:
exists key
:判断键是否存在del key
:删除键值对move key db
:将键值对移动到指定数据库expire key second
:设置键值对的过期时间(单位是秒)ttl key
:查看key的过期剩余时间(-1表示key未设置过期时间,-2 表示key过期)type key
:查看value的数据类型
127.0.0.1:6379> keys * # 查看当前数据库所有key
(empty list or set)
127.0.0.1:6379> set name qinjiang # set key
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> move age 1 # 将键值对移动到指定数据库(这里的意思表示将age移动到1号数据库)
(integer) 1
127.0.0.1:6379> EXISTS age # 判断键是否存在
(integer) 0 # 不存在
127.0.0.1:6379> EXISTS name
(integer) 1 # 存在
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> keys *
1) "age"
127.0.0.1:6379[1]> del age # 删除键值对
(integer) 1 # 删除个数
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> EXPIRE age 15 # 设置键值对的过期时间
(integer) 1 # 设置成功 开始计数
127.0.0.1:6379> ttl age # 查看key的过期剩余时间
(integer) 13
127.0.0.1:6379> ttl age
(integer) 11
127.0.0.1:6379> ttl age
(integer) 9
127.0.0.1:6379> ttl age
(integer) -2 # -2 表示key过期,-1表示key未设置过期时间
127.0.0.1:6379> get age # 过期的key 会被自动delete
(nil)
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> type name # 查看value的数据类型
string
关于TTL
命令
Redis的key,通过TTL命令返回key的过期时间,一般来说有3种:
- 当前key没有设置过期时间,所以会返回-1.
- 当前key有设置过期时间,而且key已经过期,所以会返回-2.
- 当前key有设置过期时间,且key还没有过期,故会返回key的正常剩余时间.
关于重命名RENAME
和RENAMENX
RENAME key newkey
修改 key 的名称RENAMENX key newkey
仅当 newkey 不存在时,将 key 改名为 newkey 。
更多命令学习:https://www.redis.net.cn/order/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z8VchfSw-1648450745519)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220306221440041.png)]
String(字符串)
普通的set、get直接略过。
命令 | 描述 | 示例 |
---|---|---|
APPEND key value | 向指定的key的value后追加字符串,如果当前key不存在,就相当于set key | 127.0.0.1:6379> set msg hello OK 127.0.0.1:6379> append msg “world” (integer) 10 127.0.0.1:6379> get msg “helloworld” |
DECR/INCR key | 将指定key的value数值进行+1/-1(仅对于数字) | 127.0.0.1:6379> set age 20 OK 127.0.0.1:6379> incr age (integer) 21 127.0.0.1:6379> decr age (integer) 20 |
INCRBY/DECRBY key n | 按指定的步长对数值进行加减(仅对于数字) | 127.0.0.1:6379> INCRBY age 5 (integer) 25 127.0.0.1:6379> DECRBY age 10 (integer) 15 |
INCRBYFLOAT key n | 为数值加上浮点型数值 | 127.0.0.1:6379> INCRBYFLOAT age 5.2 “20.2” |
STRLEN key | 获取key保存值的字符串长度 | 127.0.0.1:6379> get msg “helloworld” 127.0.0.1:6379> STRLEN msg (integer) 10 |
GETRANGE key start end | 按起止位置获取字符串(闭区间,起止位置都取),当输入GETRANGE key 0 -1时,表示获取所有字符串,相当于get key | 127.0.0.1:6379> get msg “helloworld” 127.0.0.1:6379> GETRANGE msg 3 9 “loworld” |
SETRANGE key offset value | 用指定的value 替换key中 offset开始的值 | 127.0.0.1:6379> SETRANGE msg 2 hello (integer) 7 127.0.0.1:6379> get msg “tehello” |
GETSET key value | 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 | 127.0.0.1:6379> GETSET msg test “helloworld” |
SETNX key value | 仅当key不存在时进行set(设置成功返回值是1;设置失败返回值是0) | 127.0.0.1:6379> SETNX msg test (integer) 0 127.0.0.1:6379> SETNX name sakura (integer) 1 |
SETEX key seconds value | set 键值对并设置过期时间 | 127.0.0.1:6379> setex name 10 root OK 127.0.0.1:6379> get name (nil) |
MSET key1 value1 [key2 value2..] | 批量set键值对 | 127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 OK |
MSETNX key1 value1 [key2 value2..] | 批量设置键值对,仅当参数中所有的key都不存在时执行 | 127.0.0.1:6379> MSETNX k1 v1 k4 v4 (integer) 0 |
MGET key1 [key2..] | 批量获取多个key保存的值 | 127.0.0.1:6379> MGET k1 k2 k3 1) “v1” 2) “v2” 3) “v3” |
PSETEX key milliseconds value | 和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间, | |
getset key value | 如果不存在值,则返回nil,如果存在值,获取原来的值,并设置新的值 | 127.0.0.1:6379> GETSET name zhangsan (nil) 127.0.0.1:6379> GET name "zhangsan" 127.0.0.1:6379> GETSET name lisi "zhangsan" 127.0.0.1:6379> GET name "lisi" |
127.0.0.1:6379> set msg hello #设置值
OK
127.0.0.1:6379> get msg #获取值
"hello"
127.0.0.1:6379> APPEND msg world #向指定的key的value后追加字符串
(integer) 10
127.0.0.1:6379> get msg
"helloworld"
127.0.0.1:6379> STRLEN msg #获取key保存值的字符串长度
(integer) 10
############################################################################################################
127.0.0.1:6379> keys *
1)"msg"
127.0.0.1:6379> APPEND name zhangsan #当前数据库中不存在name,APPEND name zhangsan在这里相当于set name zhangsan
(integer) 8
127.0.0.1:6379> keys *
1) "msg"
2) "name"
127.0.0.1:6379> get name
"zhangsan"
############################################################################################################
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> INCR age #将指定key的value数值进行+1(仅对于数字)
(integer) 21
127.0.0.1:6379> get age
"21"
127.0.0.1:6379> DECR age #将指定key的value数值进行-1(仅对于数字)
(integer) 20
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> INCRBY age 5 #对数值进行加5操作
(integer) 25
127.0.0.1:6379> get age
"25"
127.0.0.1:6379> DECRBY age 5 #对数值进行减5操作
(integer) 20
127.0.0.1:6379> get age
"20"
############################################################################################################
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set msg helloworld
OK
127.0.0.1:6379> get msg
"helloworld"
127.0.0.1:6379> GETRANGE msg 3 9 #截取下标为3到9的字符串(闭区间,起止位置都取)
"loworld"
127.0.0.1:6379> GETRANGE msg 0 -1 #0到-1表示获取所有字符串,相当于get key
"helloworld"
127.0.0.1:6379> get msg
"helloworld"
127.0.0.1:6379> SETRANGE msg 2 xx #从下标2开始(包括下标2),将后面的字符串替换为xx
(integer) 10
127.0.0.1:6379> get msg
"hexxoworld"
############################################################################################################
#SETEX(set with expire) #设置过期时间
#SETNX(set if not exist) #不存在时再设置(在分布式锁中会常常使用)
127.0.0.1:6379> keys *
1) "msg"
127.0.0.1:6379> SETEX name 30 zhangsan #设置name的值为zhangsan,并设置过期时间为30秒
OK
127.0.0.1:6379> keys *
1) "msg"
2) "name"
127.0.0.1:6379> ttl name #查看name的过期时间
(integer) 9
127.0.0.1:6379> ttl name
(integer) -2 #-2表示name已过期
127.0.0.1:6379> keys *
1) "msg"
127.0.0.1:6379> SETNX mykey redis #mykey不存在,设置值为redis
(integer) 1 #返回值为1,说明设置成功
127.0.0.1:6379> keys *
1) "msg"
2) "mykey"
127.0.0.1:6379> SETNX mykey mongoDB #mykey已存在,创建失败
(integer) 0 #返回值为0,说明设置失败
127.0.0.1:6379> get mykey
"redis"
############################################################################################################
#MSET #批量设置key和value
#MGET #批量获取对应key的值
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 #MSET批量设置key和value
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> MGET k1 k2 k3 #MGET批量获取对应key的值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> MSETNX k1 v1 k4 v4 #批量设置键值对,仅当参数中所有的key都不存在时执行
(integer) 0 #返回值为0,说明设置失败,原因:因为k1已经存在
#对象
SET user:1{name:zhangsan,age:2} #设置一个user对象,1假设为id,用json字符来保存一个对象
#这里的key是一个巧妙的设计,user:{id}:{filed},user表示对象,id表示唯一标识,filed表示属性
127.0.0.1:6379> MSET user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> MGET user:1:name user:1:age
1) "zhangsan"
2) "2"
############################################################################################################
#GETSET #先get然后再set,如果不存在值,则返回nil,如果存在值,获取原来的值,并设置新的值
127.0.0.1:6379> keys *
1) "k2"
2) "user:1:name"
3) "k3"
4) "k1"
5) "user:1:age"
127.0.0.1:6379> GETSET name zhangsan #先get name,发现name不存在,返回nil,并设置name的值为zhangsan
(nil)
127.0.0.1:6379> GET name
"zhangsan"
127.0.0.1:6379> GETSET name lisi #先get name,发现name存在,返回name原来的值,并重新设置name的值为lisi
"zhangsan"
127.0.0.1:6379> GET name
"lisi"
String类似的使用场景:value除了是字符串还可以是数字,用途举例:
- 计数器
- 统计多单位的数量:uid:123666:follow 0
- 粉丝数
- 对象存储缓存
List(列表)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
首先我们列表,可以经过规则定义将其变为队列、栈、双端队列等
正如图Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类,有时候L也表示List例如LLEN
命令 | 描述 |
---|---|
LPUSH/RPUSH key value1[value2..] | 从左边/右边向列表中PUSH值(一个或者多个值,这些值可以重复)。 |
LRANGE key start end | 获取list 起止元素(索引从左往右 递增,当输入LRANGE key 0 -1时,表示获取该列表的所有值) |
LPUSHX/RPUSHX key value | 向已存在的列名中push值(一个或者多个) |
LINSERT key BEFORE|AFTER pivot value | 在指定列表元素的前/后 插入value |
LLEN key | 查看列表长度 |
LINDEX key index | 通过索引获取列表元素 |
LSET key index value | 通过索引为元素设值,若key不存在,则会报错 |
LPOP/RPOP key | 从最左边/最右边移除值, 并返回移除掉的值 |
RPOPLPUSH source destination | 将列表的尾部(右)最后一个值弹出,并返回,然后加到另一个列表的头部 |
LTRIM key start end | 通过下标截取指定范围内的列表 |
LREM key count value | List中是允许value重复的, count > 0 :从头部开始搜索 然后删除指定的value 至多删除count个 count < 0 :从尾部开始搜索… count = 0 :删除列表中所有的指定value。 |
BLPOP/BRPOP key1[key2] timout | 移出并获取列表的第一个/最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
BRPOPLPUSH source destination timeout | 和RPOPLPUSH 功能相同,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
############################################################################################################
#LPUSH #向列表左边插入值
#RPUSH #向列表右边插入值
#LRANGE #按照指定索引获取列表的值,索引从左到右递增
127.0.0.1:6379> LPUSH list one #将一个或多个值放入列表的头部(左边),list=>{one}
(integer) 1
127.0.0.1:6379> LPUSH list two #将值放入列表的左边,list=>{two,one}
(integer) 2
127.0.0.1:6379> LPUSH list three #将值放入列表的左边,list=>{three,two,one}
(integer) 3
127.0.0.1:6379> get list #普通的get是无法获取list值的
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> LRANGE list 0 -1 #起止位置为0到-1,表示获取list列表的所有值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1 #起止位置为0到1表示获取列表左边下表为0到1的值。list=>{three,two,one}
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list four #将一个或多个值放入列表的尾部(右边),list=>{three,two,one,four}
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
############################################################################################################
#LPOP 从列表左边移除值,并返回移除掉的值
#RPOP 从列表右边移除值,并返回移除掉的值
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> LPOP list #移除列表的第一个元素(最左边),并返回值
"three"
127.0.0.1:6379> RPOP list #移除列表的最后一个元素(最右边),并返回值
"four"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
############################################################################################################
#LINDEX 获取列表指定下表的值
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LINDEX list 1 #获取列表中下标为1的值
"one"
127.0.0.1:6379> LINDEX list 0 #获取列表中下标为0的值
"two"
############################################################################################################
#LLEN 获取列表的长度
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LLEN list #获取list列表的长度
(integer) 2
############################################################################################################
#LREM 移除指定的值
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LPUSH list three three #可以用LPUSH向列表中放入多个值,这些值可以有重复
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> LREM list 1 one #从列表头部(左边)开始检索,删除1个值为one的value
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> LREM list 2 three #从列表头部(左边)开始检索,删除2个值为three的value
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
############################################################################################################
#LTRIM 仅保留列表指定索引区间(闭区间)的值,其他的值全部删除
127.0.0.1:6379> flushdb #清空数据库
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> RPUSH mylist hello hello1 hello2 hello3
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> LTRIM mylist 1 2 #保留mylist列表下标为1到2的值,其他的值都删除
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello1"
2) "hello2"
############################################################################################################
#RPOPLPUSH 移除列表中最后一个元素(最右边),并将它移动到新的列表中
127.0.0.1:6379> flushdb #清空数据库
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> RPUSH mylist hello1 hello2 hello3 hello4 hello5
(integer) 5
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
5) "hello5"
127.0.0.1:6379> RPOPLPUSH mylist myotherlist #移除mylist列表的最后一个元素,将其移动到myotherlist列表中
"hello5"
127.0.0.1:6379> keys *
1) "mylist"
2) "myotherlist"
127.0.0.1:6379> LRANGE mylist 0 -1 #查看mylist列表的所有值
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> LRANGE myotherlist 0 -1 #查看myotherlist列表的所有值
1) "hello5"
############################################################################################################
#LSET 通过索引将元素替换为新的值,相当于更新操作。若键或者索引不存在,则会报错
127.0.0.1:6379> FLUSHDB #清空数据库
OK
127.0.0.1:6379> EXISTS list #判断list是否存在
(integer) 0 #返回0,说明不存在
127.0.0.1:6379> LSET list 0 v1 #向list列表索引为0的元素设置值为v1
(error) ERR no such key #报错,原因:list这个键不存在
127.0.0.1:6379> LPUSH list hello #向list列表中插入值为hello
(integer) 1
127.0.0.1:6379> LRANGE list 0 0
1) "hello"
127.0.0.1:6379> LSET list 0 v1 #将list列表中索引为0的元素设置值为v1
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "v1"
127.0.0.1:6379> LSET list 1 v2 #将list列表中索引为1的元素设置值为v2
(error) ERR index out of range #报错,原因:在list列表中不存在索引为1的元素
############################################################################################################
#LINSERT key BEFORE|AFTER pivot value 在指定列表元素的前/后 插入value
127.0.0.1:6379> FLUSHDB #清空数据库
OK
127.0.0.1:6379> RPUSH mylist a girl
(integer) 2
127.0.0.1:6379> LINSERT mylist before girl beautiful
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "a"
2) "beautiful"
3) "girl"
127.0.0.1:6379> LINSERT mylist after a very
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1
1) "a"
2) "very"
3) "beautiful"
4) "girl"
小结
- list实际上是一个链表,before Node after , left, right 都可以插入值
- 如果key不存在,则创建新的链表
- 如果key存在,新增内容
- 如果移除了所有值,空链表,也代表不存在
- 在两边插入或者改动值,效率最高!修改中间元素,效率相对较低
应用:
消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)
Set(集合)
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
命令 | 描述 |
---|---|
SADD key member1[member2..] | 向集合中无序增加一个/多个成员 |
SCARD key | 获取集合的成员数 |
SMEMBERS key | 返回集合中所有的成员 |
SISMEMBER key member | 查询member元素是否是集合的成员,结果是无序的 |
SRANDMEMBER key [count] | 随机返回集合中count个成员,count缺省值为1 |
SPOP key [count] | 随机移除并返回集合中count个成员,count缺省值为1 |
SMOVE source destination member | 将source集合的成员member移动到destination集合 |
SREM key member1[member2..] | 移除集合中一个/多个成员 |
SDIFF key1[key2..] | 返回所有集合的差集 key1- key2 - … |
SDIFFSTORE destination key1[key2..] | 在SDIFF的基础上,将结果保存到集合中==(覆盖)==。不能保存到其他类型key噢! |
SINTER key1 [key2..] | 返回所有集合的交集 |
SINTERSTORE destination key1[key2..] | 在SINTER的基础上,存储结果到集合中。覆盖 |
SUNION key1 [key2..] | 返回所有集合的并集 |
SUNIONSTORE destination key1 [key2..] | 在SUNION的基础上,存储结果到及和张。覆盖 |
SSCAN KEY [MATCH pattern] [COUNT count] | 在大量数据环境下,使用此命令遍历集合中元素,每次遍历部分 |
############################################################################################################
#SADD 向set集合中添加元素
#SMEMBERS 查看集合中的所有元素
#SISMEMBER 判断集合中是否存在指定元素(返回1表示存在,返回0表示不存在)
#SCARD 查看集合中的元素个数
#SREM 移除集合中指定的元素
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SADD myset hello lovely longlong #向set集合中添加元素
(integer) 3
127.0.0.1:6379> SMEMBERS myset #查看myset集合中的所有元素
1) "lovely"
2) "hello"
3) "longlong"
127.0.0.1:6379> SISMEMBER myset hello #判断集合中是否存在hello这个元素(返回1表示存在,返回0表示不存在)
(integer) 1
127.0.0.1:6379> SISMEMBER myset world
(integer) 0
127.0.0.1:6379> SCARD myset #查看myset中的元素个数
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "lovely"
2) "hello"
3) "longlong"
127.0.0.1:6379> SADD myset hello #向myset集合中插入重复的值,返回值为0,说明插入失败
(integer) 0
127.0.0.1:6379> SREM myset hello #移除myset集合中的hello元素
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "lovely"
2) "longlong"
############################################################################################################
#SRANDMEMBER 随机抽选出一个或多个元素
#SPOP 随机移除一个或多个元素
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SADD myset s1 s2 s3 s4 s5 s6
(integer) 6
127.0.0.1:6379> SRANDMEMBER myset 1 #从myset集合中随机选取一个元素
1) "s4"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "s1"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "s4"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "s3"
127.0.0.1:6379> SMEMBERS myset #可以看到set集合中的元素是无序排列的
1) "s4"
2) "s6"
3) "s5"
4) "s1"
5) "s3"
6) "s2"
127.0.0.1:6379> SPOP myset 1 #随机移除一个元素
1) "s5"
127.0.0.1:6379> SPOP myset 1
1) "s2"
############################################################################################################
#SMOVE 将集合中的指定元素移动到另外一个集合
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SADD myset m1 m2 m3 #向myset集合中插入元素
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "m2"
2) "m3"
3) "m1"
127.0.0.1:6379> SADD myset2 v1 #向myset2集合中插入元素
(integer) 1
127.0.0.1:6379> SMOVE myset myset2 m1 #将myset集合中值为m1的元素移动到myset2集合
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "m2"
2) "m3"
127.0.0.1:6379> SMEMBERS myset2
1) "v1"
2) "m1"
############################################################################################################
#SDIFF 以第一个集合为中心,查看其他集合对于第一个集合的差集
#SINTER 以第一个集合为中心,查看其他集合与第一个集合的交集
#SUNION 查看所有集合的并集
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SADD set1 a b c d #向set1中插入元素
(integer) 4
127.0.0.1:6379> SADD set2 c d e f #向set2中插入元素
(integer) 4
127.0.0.1:6379> SDIFF set1 set2 #查看set1集合相对于set2集合的差集
1) "a"
2) "b"
127.0.0.1:6379> SINTER set1 set2 #查看set1集合和set2集合的交集
1) "d"
2) "c"
127.0.0.1:6379> SUNION set1 set2 #查看set1集合和set2集合的并集
1) "d"
2) "c"
3) "a"
4) "b"
5) "f"
6) "e"
Hash(哈希)
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Set就是一种简化的Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。
命令 | 描述 |
---|---|
HSET key field value | 将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个field会覆盖,返回0 |
HMSET key field1 value1 [field2 value2..] | 同时将多个 field-value (域-值)对设置到哈希表 key 中。 |
HSETNX key field value | 只有在字段 field 不存在时,设置哈希表字段的值。 |
HEXISTS key field | 查看哈希表 key 中,指定的字段是否存在(返回1代表存在;返回0代表不存在) |
HGET key field value | 获取存储在哈希表中指定字段的值 |
HMGET key field1 [field2..] | 获取所有给定字段的值 |
HGETALL key | 获取在哈希表key 的所有字段和值 |
HKEYS key | 获取哈希表key中所有的字段 |
HLEN key | 获取哈希表中字段的数量 |
HVALS key | 获取哈希表中所有值 |
HDEL key field1 [field2..] | 删除哈希表key中一个/多个field字段 |
HINCRBY key field n | 为哈希表 key 中的指定字段的整数值加上增量n,并返回增量后结果 一样只适用于整数型字段 |
HINCRBYFLOAT key field n | 为哈希表 key 中的指定字段的浮点数值加上增量 n |
HSCAN key cursor [MATCH pattern] [COUNT count] | 迭代哈希表中的键值对 |
############################################################################################################
#HSET 设置指定哈希表的指定字段的值
#HGET 获取指定哈希表的指定字段的值
#HMSET 批量设置指定哈希表的指定字段的值
#HMGET 批量获取指定哈希表的指定字段的值
#HGETALL 获取在哈希表key的所有字段和值
#HDEL 删除哈希表中的指定字段,字段对应的值也就跟着删除了
127.0.0.1:6379> HSET myhash field1 lovely_ll #设置myhash哈希表中field1字段的值为lovely_ll
(integer) 1
127.0.0.1:6379> HGET myhash field1 #获取myhash哈希表中field1字段的值
"lovely_ll"
127.0.0.1:6379> HMSET myhash field1 hello field2 world #批量设置myhash哈希表中指定字段的值
OK
127.0.0.1:6379> HMGET myhash field1 field2 #批量获取myhash哈希表中指定字段的值
1) "hello"
2) "world"
127.0.0.1:6379> HGETALL myhash #获取myhash哈希表中所有字段及其所有值
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> HDEL myhash field1 #删除myhash哈希表中field1字段,同时删除其对应的值
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "world"
############################################################################################################
#HLEN 获取指定哈希表的字段数量
127.0.0.1:6379> HSET myhash field1 hello
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> HLEN myhash #获取myhash哈希表中的字段数量
(integer) 2
#############################################################################################################HEXISTS 判断哈希表中指定字段是否存在(返回1代表存在,返回0代表不存在)
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> HEXISTS myhash field1 #判断field1字段是否在myhash哈希表中
(integer) 1 #返回1,代表存在
127.0.0.1:6379> HEXISTS myhash field3 #判断field3字段是否在myhash哈希表中
(integer) 0 #返回0,代表不存在
#############################################################################################################HKEYS 只获取哈希表中所有的字段
#HVALS 只获取哈希表中所有的值
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> HKEYS myhash #只获取myhash哈希表中所有的字段
1) "field2"
2) "field1"
127.0.0.1:6379> HVALS myhash #只获取myhash哈希表中所有的值
1) "world"
2) "hello"
############################################################################################################
#HINCRBY 向哈希表中指定字段的值进行加或减操作(只针对于整数型字段)
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> HSET myhash field3 5
(integer) 1
127.0.0.1:6379> HINCRBY myhash field3 6 #向myhash哈希表中field3字段的值进行加6操作
(integer) 11 #返回字段的值
127.0.0.1:6379> HGET myhash field3
"11"
127.0.0.1:6379> HINCRBY myhash field3 -7 #向myhash哈希表中field3字段的值进行减7操作
(integer) 4
127.0.0.1:6379> HGET myhash field3
"4"
#############################################################################################################HSETNX 向哈希表中插入字段和值,当且仅当字段不存在时(返回值为1,说明操作成功;返回值为0,说明操作失败)
127.0.0.1:6379> HKEYS myhash
1) "field2"
2) "field1"
3) "field3"
127.0.0.1:6379> HSETNX myhash field4 hello #向myhash哈希表中添加字段field4,并设置值为hello
(integer) 1 #返回1,表示操作成功
127.0.0.1:6379> HSETNX myhash field4 world ##向myhash哈希表中添加字段field4,并设置值为world
(integer) 0 #返回0,表示操作失败
Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!Hash更适合于对象的存储,Sring更加适合字符串存储!
Zset(有序集合)
不同的是每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。
score相同:按字典顺序排序
有序集合的成员是唯一的,但分数(score)却可以重复。
命令 | 描述 |
---|---|
ZADD key score member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
ZCARD key | 获取有序集合的成员数 |
ZCOUNT key min max | 计算在有序集合中指定区间score的成员数 |
ZINCRBY key n member | 有序集合中对指定成员的分数加上增量 n |
ZSCORE key member | 返回有序集中,成员的分数值 |
ZRANK key member | 返回有序集合中指定成员的索引 |
ZRANGE key start end | 通过索引区间返回有序集合成指定区间内的成员 |
ZRANGEBYLEX key min max | ZRANGEBYLEX key min max |
ZRANGEBYSCORE key min max [WITHSCORES] | 通过分数返回有序集合指定区间内的成员**-inf 和 +inf分别表示负无穷和正无穷,只支持开区间()**,后面加上WITHSCORES指令表示显示附带score |
ZLEXCOUNT key min max | 在有序集合中计算指定字典区间内成员数量 |
ZREM key member1 [member2..] | 移除有序集合中一个/多个成员 |
ZREMRANGEBYLEX key min max | 移除有序集合中给定的字典区间的所有成员 |
ZREMRANGEBYRANK key start stop | 移除有序集合中给定的排名区间的所有成员 |
ZREMRANGEBYSCORE key min max | 移除有序集合中给定的分数区间的所有成员 |
ZREVRANGE key start end | 返回有序集中指定区间内的成员,通过索引,分数从高到底 |
ZREVRANGEBYSCORRE key max min | 返回有序集中指定分数区间内的成员,分数从高到低排序 |
ZREVRANGEBYLEX key max min | 返回有序集中指定字典区间内的成员,按字典顺序倒序 |
ZREVRANK key member | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
ZINTERSTORE destination numkeys key1 [key2 ..] | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中,numkeys:表示参与运算的集合数,将score相加作为结果的score |
ZUNIONSTORE destination numkeys key1 [key2..] | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 |
ZSCAN key cursor [MATCH pattern\] [COUNT count] | 迭代有序集合中的元素(包括元素成员和元素分值) |
############################################################################################################
#ZADD 添加一个或多个值
#ZRANGE 查看指定索引区间的值
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> ZADD myset 1 one 2 two 3 three #设置多个值
(integer) 3
127.0.0.1:6379> ZRANGE myset 0 -1 #查看所有值
1) "one"
2) "two"
3) "three"
############################################################################################################
#ZRANGEBYSCORE KEY MIN MAX [WITHSCORES] 按照score排序(从小到大),后面加上WITHSCORES指令表示显示附带score
#ZREVRANGE KEY START STOP [WITHSCORES] 按照索引排序(从大到小),后面加上WITHSCORES指令表示显示附带score
127.0.0.1:6379> ZADD mark 90 zhangsan 60 lisi 70 wangwu
(integer) 3
127.0.0.1:6379> ZRANGEBYSCORE mark -inf +inf #按照score排序(从小到大),-inf表示负无穷,+inf表示正无穷
1) "lisi"
2) "wangwu"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE mark -inf +inf WITHSCORES #按照score排序(从小到大),并附带score
1) "lisi"
2) "60"
3) "wangwu"
4) "70"
5) "zhangsan"
6) "90"
127.0.0.1:6379> ZREVRANGE mark 0 -1 WITHSCORES #按照索引排序(从大到小),并附带score
1) "zhangsan"
2) "90"
3) "wangwu"
4) "70"
5) "lisi"
6) "60"
#############################################################################################################ZREM 移除一个或多个元素
#ZCARD 查看有序集合中的元素个数
127.0.0.1:6379> ZRANGE mark 0 -1
1) "lisi"
2) "wangwu"
3) "zhangsan"
127.0.0.1:6379> ZREM mark wangwu #移除wangwu这个元素
(integer) 1
127.0.0.1:6379> ZRANGE mark 0 -1
1) "lisi"
2) "zhangsan"
127.0.0.1:6379> ZCARD mark #查看有序集合中的元素个数
(integer) 2
############################################################################################################
#ZCOUNT key min max 计算在有序集合中指定区间score的成员数
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> ZADD myset 1 hello 2 world 3 zhangsan
(integer) 3
127.0.0.1:6379> ZCOUNT myset 1 3 #获取myset有序集合中1到3之间的成员数量
(integer) 3
127.0.0.1:6379> ZCOUNT myset 1 2
(integer) 2
应用案例:
- set排序 存储班级成绩表 工资表排序!
- 普通消息,1.重要消息 2.带权重进行判断
- 排行榜应用实现,取Top N测试
四、三种特殊数据类型
Geospatial(地理位置)
使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用
命令 | 描述 |
---|---|
GEOADD key longitud(经度) latitude(纬度) member [..] | 将具体经纬度的坐标存入一个有序集合 |
GEOPOS key member [member..] | 获取集合中的一个/多个成员坐标 |
GEODIST key member1 member2 [unit] | 返回两个给定位置之间的距离。默认以米作为单位。 |
GEORADIUS key longitude(经度) latitude(纬度) radius(半径) m|km|mi|ft [WITHCOORD][WITHDIST] [WITHHASH] [COUNT count][ASC|DESC] [STORE key] [STOREDIST key] | 以给定的经纬度为中心, 返回集合包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。 |
GEORADIUSBYMEMBER key member radius... | 功能与GEORADIUS相同,只是中心位置不是具体的经纬度,而是使用结合中已有的成员作为中心点。 |
GEOHASH key member1 [member2..] | 返回一个或多个位置元素的Geohash表示。该命令将返回11个字符的Geohash字符串。使用Geohash位置52点整数编码。 |
有效经纬度
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
关于GEORADIUS的参数
通过
georadius
就可以完成 附近的人功能withcoord:带上坐标
withdist:带上距离,单位与半径单位相同
COUNT n : 只显示前n个(按距离递增排序)
############################################################################################################ #GEOADD key longitud(经度) latitude(纬度) member [..] 添加一个或多个城市经纬度位置
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
# 参数: key 值(经度、纬度、名称)
#GEOPOS 查询一个或多个城市的经度和纬度
127.0.0.1:6379> GEOADD China:city 39.90 166.40 beijing #报错,因为经度纬度超过规定范围
(error) ERR invalid longitude,latitude pair 39.900000,166.400000
127.0.0.1:6379> GEOADD China:city 166.40 39.90 beijing 121.47 31.23 shanghai 106.50 29.53 chongqing 114.05 22.52 shenzhen 120.16 30.24 hangzhou 108.96 34.26 xian #添加城市经度和纬度
(integer) 6
127.0.0.1:6379> GEOPOS China:city beijing chongqing #获取北京和重庆的经纬度
1) 1) "166.40000134706497192"
2) "39.90000009167092543"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
#############################################################################################################GEODIST 返回两个城市之间的距离
127.0.0.1:6379> GEODIST China:city beijing shanghai km #查询北京和上海两个城市的直线距离
"1067.3788"
127.0.0.1:6379> GEODIST China:city beijing chongqing km #查询北京和重庆两个城市的直线距离
"1464.0708"
#############################################################################################################GEORADIUS 以给定的经纬度为中心,查询在给定的半径范围内所有的元素
127.0.0.1:6379> GEORADIUS China:city 110 30 1000 km #查询在以经度为110,纬度为30的中心,1000km范围内的城市
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379> GEORADIUS China:city 110 30 500 km WITHCOORD #查询在以经度为110,纬度为30的中心,500km范围内的城市,包含城市的经纬度
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> GEORADIUS China:city 110 30 500 km WITHCOORD WITHDIST COUNT 1 #查询在以经度为110,纬度为30的中心,500km范围内的城市,包含城市的经纬度,距离,并限制查询数量为1
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
############################################################################################################
#GEORADIUSBYMEMBER 以一个元素的位置为中心,查找给定半径的所有元素
127.0.0.1:6379> GEORADIUSBYMEMBER China:city beijing 1000 km #以北京为中心,查询半径1000km范围内的城市
1) "beijing"
2) "xian"
#############################################################################################################GEOHASH 将二维的经纬度转化为一维的字符串,如果两个字符串越接近,则距离越近!
127.0.0.1:6379> GEOHASH China:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
GEO底层的实现原理其实就是Zset,我们可以使用Zset命令来操作GEO
127.0.0.1:6379> type China:city #查看China:city的类型为zset
zset
127.0.0.1:6379> ZRANGE China:city 0 -1 #查看China:city的所有元素
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> ZREM China:city beijing #移除beijing元素
(integer) 1
127.0.0.1:6379> ZRANGE China:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
Hyperloglog(基数统计)
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
其底层使用string数据类型
什么是基数?
数据集中不重复的元素的个数。
应用场景:
网页的访问量(UV):一个用户多次访问,也只能算作一个人。
传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog就能帮助我们利用最小的空间完成。
命令 | 描述 |
---|---|
PFADD key element1 [elememt2..] | 添加指定元素到 HyperLogLog 中 |
PFCOUNT key [key] | 返回给定 HyperLogLog 的基数估算值。 |
PFMERGE destkey sourcekey [sourcekey..] | 将多个 HyperLogLog 合并为一个 HyperLogLog |
#############################################################################################################PFADD 添加指定元素到 HyperLogLog 中
#PFCOUNT 返回给定 HyperLogLog 的基数估算值
#PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog
127.0.0.1:6379> PFADD mykey a b c d e f g h i j #创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey #统计mykey中的基数数量(即不重复元素的数量)
(integer) 10
127.0.0.1:6379> PFADD mykey2 i j z x c v b n m #创建第二组元素 mykey2
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 #合并mykey和mykey2这两组元素到mykey3
OK
127.0.0.1:6379> PFCOUNT mykey3 #查看mykey3中的基数数量
(integer) 15
如果允许容错,那么一定可以使用Hyperloglog !
如果不允许容错,就使用set或者自己的数据类型即可 !
BitMaps(位图)
使用位存储,信息状态只有 0 和 1
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
应用场景
签到统计、状态统计
命令 | 描述 |
---|---|
SETBIT key offset value | 为指定key的offset位设置值 |
GETBIT key offset | 获取offset位的值 |
bitcount key [start end] | 统计字符串被设置为1的bit数,也可以指定统计范围按字节 |
bitop operration destkey key[key..] | 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 |
BITPOS key bit [start] [end] | 返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位 |
#############################################################################################################SETBIT 为指定key的offset位设置值
#GETBIT 获取offset位的值
----使用bitmap来记录周一到周日的打卡!(value为0代表未打卡,value为1代表已打卡)----
127.0.0.1:6379> SETBIT sign 0 1 # 设置sign的第0位为 1 (周一:1)
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0 # 设置sign的第2位为 1 不设置默认 是0 (周二:0)
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0 # (周三:0)
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1 # (周四:1)
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1 # (周五:1)
(integer) 0
127.0.0.1:6379> SETBIT sign 5 0 # (周六:0)
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0 # (周日:0)
(integer) 0
----查看某一天是否打卡----
127.0.0.1:6379> GETBIT sign 1 # 获取offset位的值(查看周二是否打卡)
(integer) 0
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 4
(integer) 1
#############################################################################################################BITCOUNT 统计字符串被设置为1的bit数,也可以指定统计范围按字节
----统计操作,统计打卡天数----
127.0.0.1:6379> BITCOUNT sign #统计这周的打卡记录,就可以看到是否有全勤
(integer) 3
五、事务
Redis的单条命令是保证原子性的,但是redis事务不能保证原子性
Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
- 一次性
- 顺序性
- 排他性
- Redis事务没有隔离级别的概念
- Redis单条命令是保证原子性的,但是事务不保证原子性!
Redis事务操作过程
- 开启事务(
multi
) - 命令入队
- 执行事务(
exec
)
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379> SET k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> SET k3 v3
QUEUED
127.0.0.1:6379> EXEC # 事务执行
1) OK
2) OK
3) "v2"
4) OK
取消事务(discurd
)
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> SET k4 v4
QUEUED
127.0.0.1:6379> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI # 当前未开启事务
127.0.0.1:6379> get k4 # 被放弃事务中命令并未执行
(nil)
事务错误
代码语法错误(编译时异常)所有的命令都不执行
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> SET k3 v3
QUEUED
127.0.0.1:6379> error k1 # 这是一条语法错误命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors. # 执行报错
127.0.0.1:6379> get k4
(nil) # 其他命令并没有被执行
代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** >>> 所以不保证事务原子性
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SET k1 v1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR k1 # 这条命令逻辑错误(对字符串进行增量)
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> EXEC
1) (error) ERR value is not an integer or out of range # 运行时报错
2) OK
3) OK
4) "v3" # 其他命令正常执行
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
监控
悲观锁:
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
乐观锁:
- 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
使用watch key
监控指定数据,相当于乐观锁加锁。
正常执行
127.0.0.1:6379> set money 100 # 设置余额:100
OK
127.0.0.1:6379> set use 0 # 支出使用:0
OK
127.0.0.1:6379> watch money # 监视money (上锁)
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion)
我们启动另外一个客户端模拟插队线程。
线程1:
127.0.0.1:6379> watch money # money上锁
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> # 此时事务并没有执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Z555PVk-1648450745521)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220324164247154.png)]
模拟线程插队,线程2:
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> SET money 1000 # 修改了线程一中监视的money
OK
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xcnn9aY5-1648450745522)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220324164312006.png)]
回到线程1,执行事务:
127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败
(nil) # 没有结果,说明事务执行失败
127.0.0.1:6379> get money # 线程2 修改生效
"1000"
127.0.0.1:6379> get out # 线程1事务执行失败,数值没有被修改
"20"
解锁获取最新值,然后再加锁进行事务。
unwatch
进行解锁。
注意:每次提交执行exec后都会自动释放锁,不管是否成功
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lqoBlVYS-1648450745523)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220324170714093.png)]
六、Jedis
使用Java来操作Redis,Jedis是Redis官方推荐使用的Java连接redis的客户端。
- 导入依赖
<!--导入jredis的包-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
-
编码测试
-
连接数据库
1)修改redis的配置文件
1.将只绑定本地注释
2.保护模式改为 no
3.允许后台运行
2)查看远程服务器的防火墙状态
systemctl status firewalld
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZyB6u3Ze-1648450745524)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220324212255948.png)]
防火墙为开启状态;
3)按照步骤执行一下命令
//查看防火墙状态 firewall-cmd --state //开放6379端口 firewall-cmd --permanent --add-port=6379/tcp //查看端口,执行 firewall-cmd --permanent --query-port=6379/tcp firewall-cmd --permanent --list-ports //重启防火墙 firewall-cmd --reload
4)重启redis
./redis-cli -p 6379 shutdown ./bin/redis-server redis.conf
注意:
./bin/redis-server
是我存放的地方,请根据自己的实际情况执行-
操作命令
TestPing.java
public class TestPing { public static void main(String[] args) { //1.new Jedis 对象即可 Jedis jedis = new Jedis("192.168.218.134", 6379);//参数为虚拟机的ip地址以及端口号 //jedis.auth("123456"); //注意:若redis.config文件中设置了密码,请加入语句,参数为redis.config中的密码 String response = jedis.ping(); System.out.println(response); // PONG } }
输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCfoS9Pq-1648450745525)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220324220431635.png)]
- 断开连接
-
3.事务
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.218.134", 6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "lovely_ll");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
// jedis.watch(result)
try {
multi.set("user1", result);
multi.set("user2", result);
// 执行事务
multi.exec();
}catch (Exception e){
// 放弃事务
multi.discard();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();// 关闭连接
}
}
}
七、SpringBoot整合
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。
jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式
lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
我们在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名。Redis也不例外。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PcuYIDQw-1648450745526)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220325154018764.png)]
那么就一定还存在一个RedisProperties类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzJeNyjf-1648450745528)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220325154343100.png)]
之前我们说SpringBoot2.x后默认使用Lettuce来替换Jedis,现在我们就能来验证了。
先看Jedis:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iz8zmQ4o-1648450745528)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220325155057959.png)]
@ConditionalOnClass注解中有两个类是默认不存在的,所以Jedis是无法生效的
然后再看Lettuce:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ejr9kvVm-1648450745529)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220325155245226.png)]
完美生效。
现在我们回到RedisAutoConfiguratio
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
) //我们可以自己定义一个redisTemplelate来替换这个默认的!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的RedisTemplate没有过多的配置,redis对象都是需要序列化!
//两个泛型都是Object,Object的类型,我们后使用需要强制转换<String,Object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean//由于String是redis中最常用的类型,所以说单独提出来了一个bean!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
只有两个简单的Bean
- RedisTemplate
- StringRedisTemplate
当看到xxTemplate时可以对比RestTemplat、SqlSessionTemplate,通过使用这些Template来间接操作组件。那么这俩也不会例外。分别用于操作Redis和Redis中的String数据类型。
在RedisTemplate上也有一个条件注解,说明我们是可以对其进行定制化的
说完这些,我们需要知道如何编写配置文件然后连接Redis,就需要阅读RedisProperties
2.编写配置文件
# 配置redis
spring.redis.host=192.168.218.134
spring.redis.port=6379
3.使用RedisTemplate
package com;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
//redisTemplate 操作不同的数据类型,
//redisTemplate.opsForValue() 操作字符串 类似String
//redisTemplate.opsForList() 操作List 类似于List
//redisTemplate.opsForSet()
//redisTemplate.opsForHash()
//redisTemplate.opsForZSet()
//redisTemplate.opsForGeo()
//redisTemplate.opsForHyperLogLog()
// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
//获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
redisTemplate.opsForValue().set("mykey","可爱龙龙爱学习");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
4.测试结果
此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2fkh9q13-1648450745530)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220325164335183.png)]
这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。
我们转到看那个默认的RedisTemplate内部什么样子:
在最开始就能看到几个关于序列化的参数。
默认的序列化器是采用JDK序列化器
而默认的RedisTemplate中的所有序列化器都是使用这个序列化器:
后续我们定制RedisTemplate就可以对其进行修改。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KybgLk1v-1648450745532)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220325173004812.png)]
RedisSerializer
提供了多种序列化方案:
1.直接调用RedisSerializer的静态方法来返回序列化器,然后set
2.自己new 相应的实现类,然后set
5.定制RedisTemplate的模板:
我们创建一个Bean加入容器,就会触发RedisTemplate上的条件注解使默认的RedisTemplate失效。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 将template 泛型设置为 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
/*
* 序列化设置
*/
// key、hash的key 采用 String序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// value、hash的value 采用 Jackson 序列化方式
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
这样一来,只要实体类进行了序列化,我们存什么都不会有乱码的担忧了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FszvVrgH-1648450745534)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220326152143609.png)]
八、自定义Redis工具类
使用RedisTemplate需要频繁调用.opForxxx
然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。
package com.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 在我们真实地开发中,或者在公司,一般都可以看到自己封装的RedisUtil
*/
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
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;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
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)));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
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;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
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;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 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;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 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;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(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;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
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=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
九、Redis.conf
容量单位不区分大小写,G和GB有区别
可以使用 include 组合多个配置问题
网络配置
通用GENERAL
日志输出级别
日志输出文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fySJhjFU-1648450745535)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220326161832732.png)]
数据库数量
持久化规则
由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中
持久化方式:
- RDB
- AOF
RDB文件相关
主从复制
Security模块中进行密码设置
客户端连接相关
maxclients 10000 #设置能连接上redis的最大客户端的数量
maxmemory <bytes> #redis配置最大的内存限制
maxmemory-policy noeviction #内存到达上限的处理策略
redis 中的默认的过期策略是 volatile-lru 。
设置方式
config set maxmemory-policy volatile-lru
maxmemory-policy 六种方式
**1、volatile-lru:**只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
**3、volatile-random:**随机删除即将过期key
**4、allkeys-random:**随机删除
5、volatile-ttl : 删除即将过期的
**6、noeviction :**永不过期,返回错误
AOF相关部分
十、持久化—RDB
Redis是内存数据库,如果不讲内存中的数据状态保存到磁盘中,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以redis提供了持久化的功能!
RDB:Redis Databases
什么是RDB
在主从复制中,rgb就是备用的,放在从机上面
!
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话将的快照,它恢复时是将快照文件直接读到内存中。
Redis会单独创建一个子进程来进行持久化,会先将数据写入到一个临时文件中,持久化过程都结束了,在用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何io操作的。这就保证了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的持久化方式是RDB,一般情况下不需要修改这个配置!
RDB 保存的文件时 dump.rdb
触发机制
1、save的规则满足的情况下,会自动触发rdb规则
2、执行flushall命令,也会触发我们的rdb
3、退出redis,也会产生rdb文件。
如何恢复rdb文件
1、只需要讲rdb文件放在我们的redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据
2、查看需要存放的位置
127.0.0.1:6379> config get dir
1) "dir"
2) "/home/aubin/desktop/redis/6379" #如果在这个目录下存在dump.rdb文件,启动就会自动恢复其中的数据
优点:
1、适合大规模的数据恢复!dump.rdb
2、对数据的完整性要求不高
缺点:
1、需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了
2、fork进程的时候,会占用一定的内存空间
十一、持久化AOF
Append Only File(追加文件)
什么是AOF
一句话AOF概括:
将我们的所有命令都记录下来,类似于history.恢复的时候就把这个文件全部执行一遍。
以日志的形式记录每个写操作,将redis执行过的所有指令记录下来(读操作不记录),只允许追加文件但不可改写文件。
redis在启动之初会读取该文件重新构建数据。
换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF 保存的是 appendonly.aof 文件
配置方式:在redis.conf配置文件里
默认是不开启的,我们需要手动开启。改为yes就开启了AOF!
重启redis就生效了!
如果这个aof文件有错误,这个时候redis是启动不起来的,我们需要修复这个aof文件
redis给我们提供了一个工具 redis-check-aof --fix
如果文件正常,重启就可以直接恢复了!
重写的规则
aof默认是文件的无限制追加,文件会越来越大
aof默认是文件的无限制追加,文件会越来越大
如果aof文件超过64M,文件太大了,会fork一个新进程将我们的文件重写
优点和缺点
appendonly yes # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用!
appendfilename "appendonly.aof" #持久化文件的名字
# appendfsync always # 每次修改都会sync。消耗性能
appendfsync everysec # 每秒执行一次 sync,可能会丢失这一秒的数据
# appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快!
优点:
1、每一次修改都同步,文件的完整性会更加好!
2、每秒同步一次,可能会丢失一秒的数据
3、从不同步,效率最高!
缺点:
1、相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
2、aof运行效率也要比rdb慢,所以我们redis默认的配置是rdb持久化
扩展:
1.RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
2.AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
3.只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
4.同时开启两种持久化方式
- 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
- RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
5.性能建议
- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则
- 如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。
- 如果不Enable AOF ,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构
十三、Redis发布与订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!
Redis客户端可以订阅任意数量的频道。
订阅/发布模式图:
消息发送者:UP主,博主
频道:平台
接受者:粉丝
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cU3tOzc1-1648450745536)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220326214334004.png)]
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播,实时提醒等。
命令 | 描述 |
---|---|
PSUBSCRIBE pattern [pattern …] | 订阅一个或多个符合给定模式的频道。 |
PUBSUB subcommand [argument [argument …]] | 查看订阅与发布系统状态。 |
PUBLISH channel message | 将信息发送到指定的频道。 |
PUNSUBSCRIBE [pattern [pattern …]] | 退订所有给定模式的频道。 |
SUBSCRIBE channel [channel …] | 订阅给定的一个或多个频道的信息。 |
UNSUBSCRIBE [channel [channel …]] | 指退订给定的频道。 |
测试
订阅端开启,她在等待读取我们的messages信息,我订阅了lovely_ll
他就在等这个lovely_ll推送的信息
127.0.0.1:6379> SUBSCRIBE lovely_ll #订阅一个频道 lovely_ll
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lovely_ll"
3) (integer) 1
#等待读取推送的信息
我们在开一个连接,模拟发送消息,我发消息到这个频道
127.0.0.1:6379> PUBLISH lovely_ll helloWorld #发布者发布消息到频道!
(integer) 1
这个时候我们就能监听到了,我接收到了up主的帖子
127.0.0.1:6379> SUBSCRIBE lovely_ll
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lovely_ll"
3) (integer) 1
1) "message" #消息
2) "lovely_ll" #哪个频道的消息
3) "helloWorld" #消息的具体内容
原理
Redis是使用C实现的,通过分析Redis源码里的pubsgb.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis的理解。
Redis通过PUBLISH、SUBSCRIBE 和PSUBSCRIBE等命令实现发布和订阅功能。
通过SUBSCRIBE命令订阅某频道后, redis-server 里维护了一个字典,字典的键就是一个个channel , 而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定channel的订阅链表中。比如说微信公众号
通过PUBLISH命令向订阅者发送消息, redis-server 会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布( Publish )与订阅( Subscribe ) , 在Redis中,你可以设定对某一个key值进行消息发 布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这-功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景
实时消息系统
实时聊天系统(频道当做聊天室,在将信息回显给所有人)
订阅,关注系统
稍微复杂的场景,我们就会使用消息中间件MQ处理。
十四、Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,
一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
- 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
- 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
- 高可用(集群)基石:主从复制还是哨兵和集群能够实施的基础。
一般来说,要将Redis运用于工程项目中.只使用一台Redis是万万不能的,原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且-台服务器需要处理所有的请求负载,压力较大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G ,也不能将所有内存用作Redis存储内存,
一般来说, 单台Redis最大使用内存不应该超过20G。
电商网站上的商品,一般都是一 次上传,无数次浏览的,说专业点也就是”多读少写"。
对于这种场景,我们可以使如下这种架构:
主从复制,读写分离!80%的情况下都是在进行读操作!减缓服务器压力!架构中经常使用!一主二从!
只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!
环境配置
我们在讲解配置文件的时候,注意到有一个replication
模块
只配置从库,不用配置主库!!
查看当前库的信息:info replication
127.0.0.1:6379> info replication
# Replication
role:master # 角色
connected_slaves:0 # 从机数量
master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
复制3个对应文件修改对应的信息
1.端口
2pid名字
3.log文件名字
4.durm.rdb名字
5.依次启动服务:
启动第一个服务:
启动第二个服务:
启动第三个服务:
查看进程信息:
[root@localhost 6379]# ls
6379.log 6380.log 6381.log bin dump.rdb redis79.conf redis80.conf redis81.conf redis.conf
[root@localhost 6379]# ps -ef|grep redis
root 3161 1 0 15:47 ? 00:00:00 ./bin/redis-server *:6379
root 3176 1 0 15:49 ? 00:00:00 ./bin/redis-server *:6380
root 3192 1 0 15:49 ? 00:00:00 ./bin/redis-server *:6381
root 3212 2773 0 15:50 pts/3 00:00:00 grep --color=auto redis
一主二从
默认情况下,每台服务器都是主节点;我们一般情况下只用配置从机就好了!
认老大!一主(79)二从(80,81)
使用SLAVEOF host port
就可以为从机配置主机了。
1、配置第一个从机(6380端口):
2、配置第二个从机(6381端口):
3、查看主机信息(6379端口):
我们这里是使用命令搭建,是暂时的,真实开发中应该在从机的配置文件中进行配置,这样的话是永久的。
配置文件配置
细节
主机可以写,从机只能读
主机中的所有的信息和数据,都会自动被从机保存
#79主机写操作
127.0.0.1:6379> set k1 v1
OK
#两个从机读操作
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6381> keys *
1) "k1"
####从机写操作 报错
127.0.0.1:6381> set k2 v2
(error) READONLY You can't write against a read only replica.
###################
###主节点崩了,但是从机存储的信息和数据还在
主机宕机
当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
#关闭主机
127.0.0.1:6379> SHUTDOWN
not connected> exit
############################################################################################################
#只剩下了两个从机80,81
[root@localhost 6379]# ps -ef|grep redis
root 2384 1 0 16:48 ? 00:00:01 redis-server *:6380
root 2389 1 0 16:48 ? 00:00:01 redis-server *:6381
root 2401 2293 0 16:51 pts/2 00:00:00 redis-cli -p 6380
root 2403 2340 0 16:51 pts/3 00:00:00 redis-cli -p 6381
root 2432 2119 0 17:04 pts/1 00:00:00 grep --color=auto redis
############################################################################################################
#再次查看主从关系,从机依然是从机
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
现在我们主机活过来,会怎样呢?
############################################################################################################
#主机写操作
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set k2 v2
OK
############################################################################################################
#从机读操作
127.0.0.1:6380> get k2
"v2"
测试:主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!
从机宕机
当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。
#############################################################################################################我们关闭6381端口的从机
127.0.0.1:6381> shutdown
not connected> exit
#############################################################################################################查看主机6379的状态信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 #发现从机只剩下一个
slave0:ip=127.0.0.1,port=6380,state=online,offset=504,lag=0
master_replid:308db2b152de5b221fc51e1e4e07e3f85f18b33c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:504
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:504
#############################################################################################################现在开始往6379主机内set值
127.0.0.1:6379> set k3 v3
OK
#############################################################################################################当然从机6380没有宕机,肯定能拿到k3的值
127.0.0.1:6380> get k3
"v3"
#############################################################################################################接下来重新恢复从机6381
[root@localhost 6379]# ./bin/redis-server redis81.conf
[root@localhost 6379]# ./bin/redis-cli -p 6381
#现在来获取一下k3,发现这个从机断开的时候,主机写的操作他拿不到了
127.0.0.1:6381> get k3
(nil)
#这是为什么呢?
#看一下从机6381的状态
127.0.0.1:6381> info replication
# Replication
role:master #发现6381端口变成了主机
connected_slaves:0
master_replid:3ce3c9c1ecd8b74c9e41d09bf22f68943a6a639e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
#现在用命令让它重新变成从机
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
#再次去获取一下k3,发现获取到了
127.0.0.1:6381> get k3
"v3"
如果是使用命令行,来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从主机中获取值!
从机复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master将传送整个数据文件到slave ,并完成一次完全同步。
**全量复制:**而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制: Master继续将新的所有收集到的修改命令依次传给slave ,完成同步
但是只要是重新连接master , 一次完全同步(全量复制)将被自动执行,我们的数据一定可以再从机中看到
进阶
我们可不可以这样设置主从关系,80这个机子即当主机又当从机,81作为80的从机,而不直接是79的从机,那如果79主机宕机,80这个机子能进行主机才能进行的写操作么
#############################################################################################################81变成80的从机
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
#############################################################################################################79以前的两个从机变成了一个从机
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=3944,lag=0
master_replid:308db2b152de5b221fc51e1e4e07e3f85f18b33c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:3944
#############################################################################################################此时的80依旧是一个从节点,不能进行写操作
127.0.0.1:6380> info replication
# Replication
role:slave #80依然是从节点
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:4056
slave_priority:100
slave_read_only:1
connected_slaves:1 #有一个从节点81
slave0:ip=127.0.0.1,port=6381,state=online,offset=4056,lag=0
master_replid:308db2b152de5b221fc51e1e4e07e3f85f18b33c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:4056
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:4056
#############################################################################################################那我们现在在79端口写操作,81从机还能获取到么
127.0.0.1:6379> set k4 v4
OK
#81拿数据,依旧能拿到
127.0.0.1:6381> get k4
"v4"
层层链路,上一个主节点连接下一个从节点,这个时候也可以完成主从复制
如果没有老大了,这个时候能不能选择出来一个老大呢?
默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
- 从机手动执行命令
slaveof no one
,这样执行以后从机会独立出来成为一个主机 - 使用哨兵模式(自动选举)
手动方法:
如果主机断开了连接,我们可以使用SLAVEOF no one
让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么就重新连接!
#SLAVEOF no one 篡位命令
127.0.0.1:6381> SLAVEOF no one
OK
#登基称帝
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_replid:8730bf7197a01429d102e41ddd73d6025c4c10c6
master_replid2:4555410e900c0be6c1cee3ee2507d496c9f5db82
十五、哨兵模式
(自动选举老大的模式)
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。 这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供 了Sentinel (哨兵)架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应.从而监控运行的多个Redis实例。
这里的哨兵有两个作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
那如果我们的哨兵也挂了呢?
多哨兵监控
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票 ,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
测试
还是上面的一主二从状态
1、配置哨兵配置文件
在配置文件夹下新建sentinel.conf
[root@localhost 6379]# vim sentinel.conf
在sentinel.conf配置文件中输入以下内容:
#sentinel monitor被监控的名称 主机地址 端口
sentinel monitor myredis 127.0.0.1 6379 1
- 数字1表示 :当一个哨兵主观认为主机断开,就可以客观认为主机故障,然后开始选举新的主机。
2、启动哨兵
[root@localhost 6379]# ./bin/redis-sentinel sentinel.conf
4351:X 27 Mar 2022 22:01:44.865 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
4351:X 27 Mar 2022 22:01:44.865 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=4351, just started
4351:X 27 Mar 2022 22:01:44.865 # Configuration loaded
4351:X 27 Mar 2022 22:01:44.866 * Increased maximum number of open files to 10032 (it was originally set to 1024).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.8 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 4351
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
4351:X 27 Mar 2022 22:01:44.870 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
4351:X 27 Mar 2022 22:01:44.871 # Sentinel ID is 22574b7eed231893a6856196744925e2977adba0
## 监控127.0.0.1 6379 后面有1 说明他是1票是主机
4351:X 27 Mar 2022 22:01:44.871 # +monitor master myredis 127.0.0.1 6379 quorum 1
## 发现两个从机127.0.0.1:6380 127.0.0.1:6381
4351:X 27 Mar 2022 22:01:44.875 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
4351:X 27 Mar 2020 22:01:44.878 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
现在我们将主机关掉
#关闭主机操作
127.0.0.1:6379> shutdown
not connected> exit
[root@localhost 6379]#
##### 80还是从机
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:12060
##### 81还是从机
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:2
############################################################################################################
# 我们发现他开始发送选举6379崩了,故障转移
# 我们发现81自动成为了主机
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=48361,lag=0
master_replid:a5da4a28672682bd095a4921346fe1b5f2231885
master_replid2:308db2b152de5b221fc51e1e4e07e3f85f18b33c
master_repl_offset:48493
second_repl_offset:45511
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2027
repl_backlog_histlen:46467
#在我们的哨兵最后一行我们亦可以看到
4401:X 27 Mar 2022 22:11:58.579 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381
#6381选举成为了新主机
哨兵日志
如果Master节点断开了,这个时候就会在从机中随机选择一个服务器!()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLdcg6oY-1648450745537)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220327223104269.png)]
现在以前的主机在连接回来呢?光杆司令,想要称帝,还得我们手动配置
不然的话,自动给81节点当从机,已经是81的天下了
哨兵模式优缺点
优点:
- 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
- 主从可以切换,故障可以转移,系统的可用性更好
- 哨兵模式是主从模式的升级,手动到自动,更加健壮
缺点:
- Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
- 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项
哨兵模式的全部配置
# Example sentine1. conf
#哨兵sentine1实例运行的端口默认26379
#如果有哨兵集群,我们还需要配置每个哨兵的端口
port 26379
#哨兵sentine1的工作目录
dir /tmp
#哨兵sentine]监控的redis主节点的ip port
# master-name可以自己命名的主节点名字 只能由字母A-z、 数字0-9、这三个字符".-- "组成。
# quorum配置多少个sentine1哨兵统- -认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor <master-name> 、 cip> <redis-port> <quorum>
sentine1 monitor mymaster 127.0.0.1 6379 2
#当在Redis实例中开启 了requirepass foobared 授权密码这样所有连接Redis实例的客户端都要提供密码
#设置哨兵sentinel连接主从的密码注意必须为主从设置- -一样的验证密码
# sentine1 auth-pass <master-name> <password>
sentine1 auth-pass mymaster MySUPER--secret -0123passwOrd
#指定多少毫秒之后主节点没有应答哨兵sentine1此时哨兵主观上认为主节点下线默认30秒
# sentine1 down-after-milliseconds <master-name> <mi 11iseconds>
sentine1 down-after-mi1li seconds mymaster 30000
#这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越多的slave因为replication而不可用。
可以通过将这个值设为1来保证每次只有一-个slave处于不能处理命令请求的状态。
# sentine1 parallel-syncs <master-name> <nums laves>
sentine1 paralle1-syncs mymaster 1
#故障转移的超时时间failover-timeout 可以用在以下这些方面:
#1.同一个sentine1对同一 个master两次failover之间的间隔时间。
#2.当一个slave从一个错 误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的fai lover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves 依然会被正确配置为指向
master,但是就不按para1le1-syncs所配置的规则来了
#默认三分钟
# sentine1 fai lover-timeout <master-name> <mi 11iseconds>
sentine1 failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事 件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
并对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信 号终止,之后重新执行。
#通知型脚本:当sentine1有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个
脚本应该通过邮件,SMS 等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,- - 个是事件的类型,一
个是事件的描述。如果sentinel. conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则
sentine1无法正常启动成功。
#通知脚本
#shell编程
# sentine1 notification-script <master-name> <script-path>
sentine1 notification-script mymaster /var/redis/notify. sh
#客户端重新配置主节点参数脚本
#当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
#以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#目前<state> 总是“failover",
# <role> 是“Teader"或者“observer"中的-一个。
#参数from-ip,from-port, to-ip, to-port是用来和旧的master和新的master (即旧的slave)通信的
#这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentine1 client-reconfig-script <master-name> <script-path>
sentine1 client-reconfig-script mymaster /var/redis/reconfig. sh
#一般运维来配置
十六、缓存穿透与雪崩
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一-些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
user2谁都没有,他就不停的向Mysql中查询,容易洪水攻击导致崩溃,这就是典型的缓存穿透
缓存穿透
查不到导致的
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构 ,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿
量太大 缓存过期
微博服务器宕机(热点词频 60s时宕机,60.1s恢复,0.1s大量请求直接访问mysql,mysql会被击穿)
概念
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点 ,在不停的扛着大并发,大并发集中对这一个点进行访问 ,当这个key在失效的瞬间,**(这个key对应的数据过期了)**持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障 上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一-般是热点数据 ,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
-
设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
2.加互斥锁(分布式锁)
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同 时刻只有一个线程访问。这样对锁的要求就十分高。
缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一 ,比如马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候 ,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
**缓存集中过期,倒不是非常致命,**比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存, 这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决方案
Redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis ,这样一台挂掉之后其他的还可以继续工作 ,其实就是搭建的集群。(异地多活)
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问- -遍 ,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key ,设置不同的过期时间,让缓存失效的时间点尽量均匀。