本文主要是介绍一篇文章教你学会并使用Redis-转自狂神,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、Nosql概述
为什么使用Nosql
1、单机Mysql时代
90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题
- 数据量增加到一定程度,单机数据库就放不下了
- 数据的索引(B+ Tree),一个机器内存也存放不下
- 访问量变大后(读写混合),一台服务器承受不住。
2、Memcached(缓存) + Mysql + 垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!
优化过程经历了以下几个过程:
- 优化数据库的数据结构和索引(难度大)
- 文件缓存,通过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
二、Redis入门
概述
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能该干什么?
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
- 。。。
特性
-
多样的数据类型
-
持久化
-
集群
-
事务
…
环境搭建
官网:https://redis.io/
推荐使用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
-
redis默认安装路径
/usr/local/bin
-
将redis的配置文件复制到 程序安装目录
/usr/local/bin/kconfig
下 -
redis默认不是后台启动的,需要修改配置文件!
-
通过制定的配置文件启动redis服务
-
使用redis-cli连接指定的端口号测试,Redis的默认端口6379
-
查看redis进程是否开启
-
关闭Redis服务
shutdown
-
再次查看进程是否存在
-
后面我们会使用单机多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许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
Redis-key
在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
下面学习的命令:
exists key
:判断键是否存在del key
:删除键值对move key db
:将键值对移动到指定数据库expire key second
:设置键值对的过期时间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 # 将键值对移动到指定数据库
(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/
String(字符串)
# 语法:
# APPEND key appendValue 对指定key实现字符串拼接,如果key不存在,等同于set
# STRLEN key 查看指定key的长度
# INCR key 对指定key进行自增,类似于Java中的i++
# DECR key 自减,类似于Java的i--
# INCRBY key n 对指定key按照指定的步长值进行自增
# DECRBY key n 按照指定的步长值自减
# SETRANGE key index value 从指定key的索引开始,插入指定的value值。
# 如果key不存在且索引>1,那么当前的索引之前的数据,会用\x00代替并占用一个索引位置,相当于ASCII码中的null
# GETRANGE key startIndex endIndex 将指定key按照索引的开始和结束范围进行截取,成为一个新的key
# SETEX key time value 设置一个有存活时间的key
# SETNX key value 如果这个key不存在,即创建
# MSET key value ... 设置多个key value
# MGET key ... 获取多个key指定的value
# GETSET key value 先获取指定的key,然后再设置指定的value
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> KEYS * # 获取当前数据库中所有的key
1) "k1"
127.0.0.1:6379> APPEND k1 hello #给k1再继续追加value值
(integer) 7
127.0.0.1:6379> get k1
"v1hello"
127.0.0.1:6379> APPEND k1 ,xiaohuang
(integer) 17
127.0.0.1:6379> STRLEN k1 # 查看当前k1的长度
(integer) 17
127.0.0.1:6379> get k1
"v1hello,xiaohuang"
127.0.0.1:6379> KEYS *
1) "k1"
127.0.0.1:6379> APPEND name xiaohuang
(integer) 9
127.0.0.1:6379> get name
"xiaohuang"
127.0.0.1:6379> KEYS *
1) "k1"
2) "name"
###################实现自增自减效果################
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
0
127.0.0.1:6379> INCR views # 设置value的自增效果
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> DECR views # 设置value的自减效果
(integer) 1
127.0.0.1:6379> DECR views
(integer) 0
127.0.0.1:6379> get views
"0"
############可以在自增自减时设置步长##############
127.0.0.1:6379> INCRBY views 2 # 自增,设置步长为2
(integer) 2
127.0.0.1:6379> INCRBY views 2
(integer) 4
127.0.0.1:6379> get views
"4"
127.0.0.1:6379> DECRBY views 3 # 自减,设置步长为3
(integer) 1
127.0.0.1:6379> DECRBY views 3
(integer) -2
127.0.0.1:6379> get views
"-2"
# 注意:value的自增和自减只适用于Integer类型
127.0.0.1:6379> incr name
(error) ERR value is not an integer or out of range
#################实现字符串截取效果#################
127.0.0.1:6379> set k1 hello,xiaohuang
OK
127.0.0.1:6379> get k1
"hello,xiaohuang"
127.0.0.1:6379> GETRANGE k1 0 3 # 实现字符串截取,有起始索引和结束索引,相当于Java中的subString()
"hell"
# 如果结束索引为-1,则表示当前截取的字符串为全部
127.0.0.1:6379> GETRANGE k1 0 -1
"hello,xiaohuang"
###############实现字符串的替换效果#################
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 2 hello # 实现字符串的替换效果,命令中的数字“2”表示从索引2的位置开始将其替换为指定字符串
(integer) 7
127.0.0.1:6379> get key2
"abhello"
##################################################
# setex(set with expire) # 设置过期时间
# setnx(set with not exist) # 如果key不存在,创建(分布式锁中常用)
127.0.0.1:6379> setex k3 10 v3 # 设置一个k3,过期时间为10秒
OK
127.0.0.1:6379> keys *
1) "k1"
2) "key2"
3) "name"
4) "views"
5) "k3"
# 10秒之后会自动删除
127.0.0.1:6379> keys *
1) "k1"
2) "key2"
3) "name"
4) "views"
127.0.0.1:6379> setnx lan redis # 如果key不存在,即创建
(integer) 1
127.0.0.1:6379> setnx lan mongodb
(integer) 0 # 0表示没有设置成功,也可理解为“有0行受到影响”
127.0.0.1:6379> get lan
"redis"
######################一次性设置(获取)多个键值对#####################
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> KEYS *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
# 也可以在这边的语法前面加上一个m,代表设置多个
127.0.0.1:6379> msetnx k1 vv1 k4 v4
(integer) 0
# 但是这边同时设置多个值,如果有一个key已经存在,那么这一条设置语句便不会执行成功,
# 因为Redis单条语句是原子操作,要么同时成功,要么同时失败
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
# 在Redis中,还推荐了一个比较有意思的东西
# 这是Redis中关于key的命名,可以用“:”来代表层次结构,可以对指定的key进行分类存储
127.0.0.1:6379> mset user:1:name xiaohuang user:1:age 21
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "xiaohuang"
2) "21"
127.0.0.1:6379> getset sqlan redis # 先获取当前key指定的value,如果不存在,会返回nil(null),然后设置新值
(nil)
127.0.0.1:6379> get sqlan
"redis"
127.0.0.1:6379> getset sqlan hbase
"redis"
127.0.0.1:6379> get sqlan
"hbase"
####################################################################
类似于Redis中String这样的使用场景,value值可以是字符串,也可以是其他类型
String的存储的字符串长度最大可以达到512M
主要用途
- 计数器
- 统计多单位的数量
- 一个用户的粉丝数
- 一个有过期时间的验证码
List(列表)
Redis中的List列表可以做很多事情,可以将其看成数据结构中的栈,也可以是队列,或者阻塞队列
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
首先我们列表,可以经过规则定义将其变为队列、栈、双端队列等
正如图Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类,有时候L也表示List例如LLEN
# 命令:
# LPUSH key value1 value2 ... 设置一个key,从头部插入数据(头插法)
# RPUSH key value1 value2 ... 设置一个key,从尾部插入数据(尾插法)
# LRANGE key startIndex endIndex 返回列表中从开始索引到结束索引位置的value值
# LPOP key 从key头部弹出一个value
# RPOP key 从尾部弹出一个value
# LINDEX index 返回key中指定索引的value值
# LREM key n value 删除key中的指定的value值,n代表删除几个
# LLEN key 返回key的长度
# LTRIM key startIndex endIndex 截取key,截取的范围从开始索引到结束索引
# LSET key index value 从当前key的索引开始插入指定的value值
# RPOPLPUSH key1 key2 从key1的尾部弹出一个元素,将此元素从key2的头部插入
# LINSERT key BEFORE|AFTER oldValue newValue 从指定key中已存在的value的前面或者后面插入一个指定的value
127.0.0.1:6379> LPUSH list one # 从list头部插入一个或者多个元素(从左边插入,看命令首字母)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
# 返回存储在列表中指定范围的元素,0和-1代表开始索引和结束索引,
# -1不代表实际位置的索引,它表示需要返回到这个列表的最后一个元素
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1 # 返回list中指定位置的元素
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list right # 从list尾部插入一个或多个元素(从右边插入,同第一行一样)
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> LPOP list # 从list头部(左)弹出(删除)一个元素
"three"
127.0.0.1:6379> RPOP list # 从list末尾(右)弹出一个元素
"right"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
######################################################################################################################
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LINDEX list 0 # 从头部查询list中指定索引的元素
"two"
127.0.0.1:6379> LINDEX list 1
"one"
######################################################################################################################
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LLEN list # 返回list的长度
(integer) 3
127.0.0.1:6379> LPUSH list three
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
# ================================================================================================================================
# 表示删除key中的value值
# 语法为 LREM key 删除个数 value
# ================================================================================================================================
127.0.0.1:6379> LREM list 1 three # 删除list中的1个three
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
##################信息截取操作#########################################################################################
127.0.0.1:6379> LRANGE mylist 0 -1
1) "zero"
2) "one"
3) "two"
4) "three"
5) "four"
127.0.0.1:6379> LTRIM mylist 0 2 # 截取mylist列表,只保留从开始索引到结束索引的元素
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "zero"
2) "one"
3) "two"
127.0.0.1:6379>
######################################################################################################################
# 复杂命令:rpoplpush
# 语法:rpoplpush source(源列表,必须存在) destination(目标列表,如不存在,即创建)
127.0.0.1:6379> LRANGE mylist 0 -1
1) "zero"
2) "one"
3) "two"
4) "three"
5) "four"
# 表示移除mylist中的最后一个元素,将这个被删除掉的元素从头部进入一个list
127.0.0.1:6379> RPOPLPUSH mylist list
"four"
127.0.0.1:6379> lrange list 0 -1
1) "four"
127.0.0.1:6379> lrange mylist 0 -1
1) "zero"
2) "one"
3) "two"
4) "three"
######################################################################################################################
# lset 命令,类似于关系型数据库中的Update语句,将指定索引的value值修改为其他的指定value值
127.0.0.1:6379> EXISTS list # 判断key是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果当前这个key不存在,无法修改,报错
(error) ERR no such key
127.0.0.1:6379> LPUSH list v1
(integer) 1
127.0.0.1:6379> LRANGE list 0 0
1) "v1"
127.0.0.1:6379> lset list 0 item01 # lset命令只对已存在的列表操作才是有效的
OK
127.0.0.1:6379> LRANGE list 0 0
1) "item01"
127.0.0.1:6379> lset list 1 item02 # 如果要更新的索引超过列表的长度,那么它也会报错
(error) ERR index out of range
######################################################################################################################
# linsert 命令,从列表中的某一个value值的前面或后面插入一个新的value值
# 语法:linsert key before|after 列表中的value值 新value值
127.0.0.1:6379> LPUSH mylist hello
(integer) 1
127.0.0.1:6379> LPUSH mylist xiaohuang
(integer) 2
127.0.0.1:6379> LRANGE mylist 0 -1
1) "xiaohuang"
2) "hello"
127.0.0.1:6379> LINSERT mylist after hello world
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "xiaohuang"
2) "hello"
3) "world"
127.0.0.1:6379> LINSERT mylist before xiaohuang nihao
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1
1) "nihao"
2) "xiaohuang"
3) "hello"
4) "world"
小结
List列表实际上它是一个数据结构的链表
- 可以在Node节点的before或者after,left或者right插入值
- key不存在,创建新链表
- key存在,新增内容
- 如果移除了所有了value,也代表不存在
- 在Node节点的两边插入,效率最高!中间元素效率较低
Redis中可以将这个列表灵活的使用
栈(lpush lpop | rpush rpop),队列(lpush rpop | rpush lpop)
应用:
消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)
Set(集合)
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
# 命令:
# SADD key value1 value2 ... 设置一个key
# SMEMBERS key 查看当前key
# SISMEMBER key value 查看key中指定的value是否存在
# SCARD key 查看key的长度
# SREM key value 删除key中的指定value
# SPOP key 随机删除key中的一个value
# SRANDMEMBER key [n] 随机查看当前key中的一个或者多个value
# SMOVE key1 key2 key1Value 将key1中的value移动到key2中
# SDIFF key1 key2 两个key相交,求第一个key的补集
# SINTER key1 key2 两个key相交,求交集
# SUNION key1 key2 两个key相交,求并集
127.0.0.1:6379> SADD myset hello # myset集合添加元素
(integer) 1
127.0.0.1:6379> SADD myset xiaohuang
(integer) 1
127.0.0.1:6379> SADD myset love
(integer) 1
127.0.0.1:6379> SMEMBERS myset # 查看myset的所有值
1) "love"
2) "hello"
3) "xiaohuang"
127.0.0.1:6379> SISMEMBER myset hello # 判断hello是否在set集合中
(integer) 1
127.0.0.1:6379> SISMEMBER myset world
(integer) 0
127.0.0.1:6379> SADD myset hello # 如果往set集合中添加一个重复的值,不会报错,但是也不会插入成功,因为set集合无序且不重复
(integer) 0
127.0.0.1:6379> SCARD myset # 查看myset集合中的元素个数
(integer) 3
127.0.0.1:6379> SREM myset hello # 删除set集合中hello
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "love"
2) "xiaohuang"
######################################################################################################################
# 在一个无序集合中,随机抽取一个数
127.0.0.1:6379> SMEMBERS nums
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
127.0.0.1:6379> SRANDMEMBER nums # 随机抽取
"8"
127.0.0.1:6379> SRANDMEMBER nums
"7"
127.0.0.1:6379> SRANDMEMBER nums
"5"
127.0.0.1:6379> SRANDMEMBER nums 2 # 随机抽取指定个数的元素
1) "5"
2) "4"
######################################################################################################################
# 删除指定key,随机删除key
# 以上面的数字集合为例
127.0.0.1:6379> SPOP nums # 随机删除一个set集合元素
"5"
127.0.0.1:6379> SPOP nums
"8"
127.0.0.1:6379> SPOP nums
"7"
127.0.0.1:6379> SMEMBERS nums
1) "1"
2) "2"
3) "3"
4) "4"
5) "6"
6) "9"
######################################################################################################################
# 一个set集合中的值移动到另外一个set集合中
# 语法: smove source(源set集合,必须存在) destination(目标集合,被添加元素) value(必须是源set集合中存在的value)
127.0.0.1:6379> SMEMBERS set01
1) "hello"
2) "xiaohuang"
3) "world"
127.0.0.1:6379> SMEMBERS set02
1) "me"
127.0.0.1:6379> SMOVE set01 set02 xiaohuang # 将set01中的xiaohuang放入到set02中
(integer) 1
127.0.0.1:6379> SMEMBERS set02
1) "xiaohuang"
2) "me"
127.0.0.1:6379> SMEMBERS set01
1) "hello"
2) "world"
######################################################################################################################
# 生活中的一个小现象,就比如说微信公众号,会有共同关注,还有QQ的共同好友
# 数学集合关系中的:交、并、补。微信公众号中的共同关注,以及QQ的共同好友,就是关系中的交!
127.0.0.1:6379> SMEMBERS k1
1) "b"
2) "c"
3) "a"
127.0.0.1:6379> SMEMBERS k2
1) "e"
2) "d"
3) "c"
# 上面的两个集合都有c这个元素
127.0.0.1:6379> SDIFF k1 k2 # k1 与 k2 之间的差集(以k1为主)
1) "b"
2) "a"
127.0.0.1:6379> SINTER k1 k2 # k1 和 k2 之间的交集,公众号的共同关注,QQ中的共同好友就可以这么来实现
1) "c"
127.0.0.1:6379> SUNION k1 k2 # k1 和 k2 之间的并集
1) "c"
2) "a"
3) "e"
4) "b"
5) "d"
# 在多聊一嘴,其实这些命令都可以在英语单词中找到一些规律
# 把SDIFF、SINTER还有SUNION这三个单词首字母去掉,可以得到
# DIFF:different,它代表不同的,用一句Redis官网的翻译来描述:返回的集合元素是第一个key的集合与后面所有key的集合的差集
# INTER:intersection,翻译过来为交叉,同样的,意指数学关系中的交集
# UNION:union,翻译为联合,与数学关系中的并集也是可以沾边的
######################################################################################################################
这里的命令,实际上也可以和生活中的东西都有关系,上面提到的共同关注,共同爱好,还有QQ当中的同一个星座,或者是“二度好友”,什么是二度好友,就是类似于QQ当中给你推荐QQ用户的意思,起源于六度分割理论(什么是六度分割理论,简单地说:就是你和任何一个陌生人之间所间隔的人不会超过五个,最多通过六个人你就能够认识任何一个陌生人)
Hash(哈希)
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Set就是一种简化的Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。
# 命令: # HSET key field value 设置单个hash # HGET key field 获取单个 # HMSET key field1 v1 field2 v2 设置多个 # HMGET key field 获取多个 # HGETALL key 获取hash中全部的field-value # HLEN key 获取hash长度 # HEXISTS key field 查询hash中指定的field是否存在 # HKEYS key 只获取hash中的field # HVALS key 只获取hash中value # HINCRBY key field n 对hash中指定的field设置自增自减 127.0.0.1:6379> HSET myhash k1 v1 (integer) 1 127.0.0.1:6379> HGET myhash k1 "v1" 127.0.0.1:6379> HMSET myhash k2 v2 k3 v3 k4 v4 OK 127.0.0.1:6379> hmget myhash k1 k2 k3 k4 1) "v1" 2) "v2" 3) "v3" 4) "v4" 127.0.0.1:6379> HGETALL myhash # 获取myhash中全部的kv键值对 1) "k1" 2) "v1" 3) "k2" 4) "v2" 5) "k3" 6) "v3" 7) "k4" 8) "v4" 127.0.0.1:6379> HDEL myhash k4 # 删除myhash一个指定的键值对元素 (integer) 1 127.0.0.1:6379> HGETALL myhash 1) "k1" 2) "v1" 3) "k2" 4) "v2" 5) "k3" 6) "v3" ###################################################################################################################### 127.0.0.1:6379> HLEN myhash # 获取当前hash中的value长度 (integer) 3 127.0.0.1:6379> HEXISTS myhash k1 # 判断当前hash中的kv键值对是否存在 (integer) 1 127.0.0.1:6379> HEXISTS myhash k4 (integer) 0 127.0.0.1:6379> HKEYS myhash # 只获取hash中的key 1) "k1" 2) "k2" 3) "k3" 127.0.0.1:6379> HVALS myhash # 只获取hash中key对应的value 1) "v1" 2) "v2" 3) "v3" ###################################################################################################################### # 备注:Redis中,有自增,没有自减,即使是没有自减,也可以在自增的步长当中设置一个负数即可 # hincrby key field 步长值 127.0.0.1:6379> hset myhash num 3 (integer) 1 127.0.0.1:6379> HINCRBY myhash num 2 (integer) 5 127.0.0.1:6379> HINCRBY myhash num 2 (integer) 7 127.0.0.1:6379> HSETNX myhash k4 v4 # 如果hash中的一个元素不存在,即可创建 (integer) 1 127.0.0.1:6379> HSETNX myhash k4 vv4 # 存在,即创建失败 (integer) 0 ###################################################################################################################### # 可以使用hash做一些临时变更的数据,可以是用户信息,或者是经常变动的信息 # 上面的String也提到了使用“:”进行层次分割,不过hash更适合对象存储,String适合于文本的存储 127.0.0.1:6379> HMSET user:1 name xiaohuang age 21 sex boy OK 127.0.0.1:6379> HGETALL user:1 1) "name" 2) "xiaohuang" 3) "age" 4) "21" 5) "sex" 6) "boy"
Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!Hash更适合于对象的存储,Sring更加适合字符串存储!
Zset(有序集合)
不同的是每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。
score相同:按字典顺序排序
有序集合的成员是唯一的,但分数(score)却可以重复。
# 命令: # ZADD key score1 value1 score2 value2 ... zset中添加一个或多个元素 # ZRANGE key startIndex endIndex 查询从开始到结束索引的zset集合 # ZRANGEBYSCORE key min max [WITHSCORES] 对hash中按照指定数值进行升序排列 # ZREVRANGE key startIndex endIndex 对指定开始和结束索引进行降序排列 # ZREM key field 删除hash中指定的field # ZCARD key 查询hash长度 # ZCOUNT key [min max] 查询hash数量,还可以增加最大值和最小值的范围 127.0.0.1:6379> ZADD myset 1 one (integer) 1 127.0.0.1:6379> ZADD myset 2 two # zset集合添加一个值 (integer) 1 127.0.0.1:6379> ZRANGE myset 0 -1 1) "one" 2) "two" 127.0.0.1:6379> ZADD myset 3 three 4 four (integer) 2 127.0.0.1:6379> ZRANGE myset 0 -1 1) "one" 2) "two" 3) "three" 4) "four" ###################################################################################################################### # 实现元素的排序 # 根据zset中score的值来实现元素的排序 127.0.0.1:6379> ZADD salary 3500 xiaohong 6500 xiaohuang 3900 zhangsan (integer) 3 # 当前命令,inf在Unix系统中代表的意思是无穷,所以当前命令是指,将当前zset,以从小到大的形式进行排列 127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf 1) "xiaohong" 2) "zhangsan" 3) "xiaohuang" 127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 在排列的同时,将score和指定的元素全部展示 1) "xiaohong" 2) "3500" 3) "zhangsan" 4) "3900" 5) "xiaohuang" 6) "6500" 127.0.0.1:6379> ZREVRANGE salary 0 -1 withscores # 将数据从大到小进行排列 1) "xiaohuang" 2) "6500" 3) "zhangsan" 4) "3900" 5) "xiaohong" 6) "3500" 127.0.0.1:6379> ZRANGEBYSCORE salary -inf 4000 withscores # 展示的同时还可以指示score的查询最大值,指定查询范围 1) "xiaohong" 2) "3500" 3) "zhangsan" 4) "3900" 127.0.0.1:6379> ZREM salary zhangsan # 删除zset中的一个元素 (integer) 1 127.0.0.1:6379> ZRANGE salary 0 -1 1) "xiaohong" 2) "xiaohuang" 127.0.0.1:6379> ZCARD salary (integer) 2 ###################################################################################################################### 127.0.0.1:6379> ZADD myset 1 hello 2 world 3 xiaohuang 4 xiaohei 5 xiaolan (integer) 5 # 语法:ZCOUNT key min max ,min和max包左也包右,它是一个闭区间 127.0.0.1:6379> ZCOUNT myset 2 5 # 获取指定区间的成员数量 (integer) 4 127.0.0.1:6379>
其他的API,如果说在工作中出现了,可以查看Redis的官方文档:http://www.redis.cn/commands.html
案例:
- zset是Redis的数据类型,可以排序,生活中也有案例,班级成绩,员工工资
- 设置权重,1、普通消息;2、重要消息;添加权重进行消息判断其重要性
- 来一个更接地气的案例,可以打开B站,排行榜,B站会根据视频的浏览量和弹幕量进行综合评分,进行排名
四、三种特殊数据类型
Geospatial(地理位置)
使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用
微信朋友圈中的朋友的位置,或者是QQ中也有的附近的人,饿了么中外卖小哥的位置距离
这个在Redis中被定为特殊的数据类型可叫做Geo,它是Redis3.2正式推出的一个特性,可以推导出两个地方的地理位置,两地之间的距离,方圆几千米之内的人。
对于这个关于地理的数据类型,它有6个命令
- GEOADD
- GEODIST
- GEOHASH
- GEOPOS
- GEORADIUS
- GEORADIUSBYMEMBER
因为这个特殊的数据类型和地理相关,需要用到地理的经纬度,可以推荐一个网站查看指定城市的经纬度:http://www.jsons.cn/lngcode/
GEOADD
# geoadd 添加地理位置
# 语法:GEOADD key 经度 纬度 城市名称 ...
# 注意:南北极无法直接添加。用添加城市数据来说,一般都会使用Java的Jedis来操作,而这些城市数据都是被下载下来通过JavaAPI调用
# 有效经度从-180到180度
# 有效纬度从-85.05112878 到 85.05112878 度。超过范围会出现(error) ERR invalid longitude,latitude pair
127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing 121.47 31.23 shanghai
(integer) 2
127.0.0.1:6379> GEOADD china:city 113.28 23.12 guangzhou 114.08 22.54 shenzhen
(integer) 2
127.0.0.1:6379> GEOADD china:city 119.30 26.07 fuzhou 118.11 24.49 xiamen
(integer) 2
GEOPOS
# 语法:GEOPOS key member1 member2 ...
127.0.0.1:6379> GEOPOS china:city beijing
1) 1) "116.39999896287918091"2) "39.90000009167092543"
127.0.0.1:6379> GEOPOS china:city shanghai guangzhou # 获取一个或多个地理信息
1) 1) "121.47000163793563843"2) "31.22999903975783553"
2) 1) "113.27999979257583618"2) "23.1199990030198208"
GEODIST
GEODIST命令表示指定两个位置的距离
单位:
- m表示单位米
- km表示千米
- mi表示英里
- ft表示英尺
# 语法:GEODIST key member1 member2 [unit]
# 后面的unit加了中括号表示可选操作,即当前这个命令计算出来的结果为m(米)
127.0.0.1:6379> GEODIST china:city beijing shanghai # 查看beijing和shanghai两个位置的直线距离
"1067378.7564"
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1067.3788"
127.0.0.1:6379> GEODIST china:city beijing fuzhou km
"1561.6135"
127.0.0.1:6379>
GEORADIUS
对于社交软件来说,附近的人,就相当于,你现在所在的地址,再加上一定的半径来进行查找
# 语法:GEORADIUS key 经度 纬度 半径 [单位] [WITHCOORD(搜寻到的目标的经纬度)] [WITHDIST(直线距离)] [count]
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km # 以111经度31纬度为中心,1000km为半径搜寻在器范围之内的城市
1) "shenzhen"
2) "guangzhou"
3) "fuzhou"
4) "shanghai"
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km WITHCOORD WITHDIST # 追加参数,目标经纬度,直线距离
1) 1) "shenzhen"2) "989.2821"3) 1) "114.08000081777572632"2) "22.53999903789756587"
2) 1) "guangzhou"2) "905.0108"3) 1) "113.27999979257583618"2) "23.1199990030198208"
3) 1) "fuzhou"2) "978.4847"3) 1) "119.29999798536300659"2) "26.06999873822022806"
4) 1) "shanghai"2) "996.9549"3) 1) "121.47000163793563843"2) "31.22999903975783553"
127.0.0.1:6379> GEORADIUS china:city 111 31 1000 km WITHCOORD WITHDIST count 2 # 还可以限制查询的结果条数,只显示两条
1) 1) "guangzhou"2) "905.0108"3) 1) "113.27999979257583618"2) "23.1199990030198208"
2) 1) "fuzhou"2) "978.4847"3) 1) "119.29999798536300659"2) "26.06999873822022806"
GEORADIUSBYMEMBER
找出指定元素周围的其他元素,就是以城市为中心,一定长度为半径搜索
# 语法:GEORADIUSBYMEMBER key member 长度 [unit]单位
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 2500 km
1) "shenzhen"
2) "guangzhou"
3) "xiamen"
4) "fuzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 1000 km # 找出以shanghai为中心,1000km为半径搜索
1) "xiamen"
2) "fuzhou"
3) "shanghai"
127.0.0.1:6379>
GEOHASH
返回一个或多个元素的GeoHash表示,该命令返回11个字符组成的GeoHash字符串
# GEOHASH key member
127.0.0.1:6379> GEOHASH china:city beijing
1) "wx4fbxxfke0"
# 多聊一嘴GeoHash,Redis原先是将二维的经纬度通过一定的策略转换为一维的52位的字符串编码,这个时候的编码描述的地理位置是准确的,这种操作俗称“降维打击”
# 当前命令相比于52位的字符串来说,它砍掉了右边的大多数字符串,这也意味着它失去了一定的精度,但是地理位置的指向不变
geospatial的底层,实际上它就是一个zset集合,geospatial数据类型是zset的一层封装
可以使用zset的基本命令来查看当前的key
127.0.0.1:6379> ZRANGE china:city 0 -1 # 使用zset命令查看geospatial
1) "shenzhen"
2) "guangzhou"
3) "xiamen"
4) "fuzhou"
5) "shanghai"
6) "beijing"
并且在这个特殊的数据类型中,并没有删除操作,因为使用zset的基本命令即可删除
127.0.0.1:6379> ZREM china:city beijing
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "shenzhen"
2) "guangzhou"
3) "xiamen"
4) "fuzhou"
5) "shanghai"
Hyperloglog(基数统计)
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
其底层使用string数据类型
什么是基数?
数据集中不重复的元素的个数。
应用场景:
网页的访问量(UV):一个用户多次访问,也只能算作一个人。
传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog就能帮助我们利用最小的空间完成。
Hyperloglog计数的错误率在0.81%,用来执行UV任务,可以忽略不计
# 语法:
# PFADD key value1 value2... 创建一组数据集,如果数据集中有相同的元素就会有去重效果
# PFCOUNT key 查看元素的长度
# PFMERGE key3 key1 key2 将两组元素合并成一个新数组,并带有去重效果,相当于数学中的并集
127.0.0.1:6379> PFADD k1 a b c d e f g h i j
(integer) 1
127.0.0.1:6379> PFCOUNT k1
(integer) 10
127.0.0.1:6379> PFADD k2 h i j k l n m o p q
(integer) 1
127.0.0.1:6379> PFCOUNT k2
(integer) 10
127.0.0.1:6379> PFMERGE k3 k1 k2
OK
127.0.0.1:6379> PFCOUNT k3
(integer) 17
127.0.0.1:6379>
如果在项目中允许容错,可以使用Hyperloglog
如果不行,就可以直接使用set或者Java的HashMap来实现
BitMaps(位图)
使用位存储,信息状态只有 0 和 1
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
应用场景
签到统计、状态统计
# 语法:
# SETBIT key offset value 设置一个key,在指定的offset位置上设置一个value,这个value只能是0或者1
# GETBIT key offset 获取指定key上的offset位的value值
# BITCOUNT key [start] [end] 在指定key中计算被设置为 1 的比特位的数量。
# BITOP operation destKey key1 key2 ... 对一个或者多个key进行二进制的逻辑运算
# BiTPOS key bit [start] [end] 指定key中返回value中第一个出现0或1的offset
127.0.0.1:6379> SETBIT week 1 0
(integer) 0
127.0.0.1:6379> SETBIT week 2 0
(integer) 0
127.0.0.1:6379> SETBIT week 3 0
(integer) 0
127.0.0.1:6379> SETBIT week 4 1
(integer) 0
127.0.0.1:6379> SETBIT week 5 1
(integer) 0
127.0.0.1:6379> SETBIT week 6 1
(integer) 0
127.0.0.1:6379> SETBIT week 7 1
(integer) 0
127.0.0.1:6379> GETBIT week 3
(integer) 0
127.0.0.1:6379> GETBIT week 5
(integer) 1
127.0.0.1:6379> BITCOUNT week
(integer) 4
127.0.0.1:6379> BITPOS week 1 0 -1 # 在week中返回第一个出现1的value值
(integer) 4
########################################################################################################################################
# BITOP逻辑运算
# 一共有4种逻辑运算,AND、OR、NOT、XOR,分别代表 并、或、非、异或
127.0.0.1:6379> SETBIT bit-1 0 1
(integer) 0
127.0.0.1:6379> SETBIT bit-1 1 1
(integer) 0
127.0.0.1:6379> SETBIT bit-1 3 1
(integer) 0
# bit-1的二进制数为1011,为什么嘞,这和基本数据类型中的list有点相似。
# list列表的LPUSH和LPOP组合起来可以作为一个栈的数据结构,在指定的比特位当中设置一个0或1,先进先出,如果没有设置值,就默认为0来进行处理
127.0.0.1:6379> SETBIT bit-2 0 1
(integer) 0
127.0.0.1:6379> SETBIT bit-2 3 1
(integer) 0
# bit-2的二进制数为1001
# 对bit-1和bit-2进行 并 操作
127.0.0.1:6379> BITOP AND and-bit bit-1 bit-2
(integer) 1
127.0.0.1:6379> GETBIT and-bit 0
(integer) 1
127.0.0.1:6379> GETBIT and-bit 1
(integer) 0
127.0.0.1:6379> GETBIT and-bit 2
(integer) 0
127.0.0.1:6379> GETBIT and-bit 3
(integer) 1
# and-bit:1001# 对bit-1和bit-2进行 或 操作
127.0.0.1:6379> BITOP OR or-bit bit-1 bit-2
(integer) 1
127.0.0.1:6379> GETBIT or-bit 0
(integer) 1
127.0.0.1:6379> GETBIT or-bit 1
(integer) 1
127.0.0.1:6379> GETBIT or-bit 2
(integer) 0
127.0.0.1:6379> GETBIT or-bit 3
(integer) 1
# or-bit:1011# 对bit-1进行 非 操作,注意:非操作只针对一个key
127.0.0.1:6379> BITOP NOT not-bit bit-1
(integer) 1
127.0.0.1:6379> GETBIT not-bit 0
(integer) 0
127.0.0.1:6379> GETBIT not-bit 1
(integer) 0
127.0.0.1:6379> GETBIT not-bit 2
(integer) 1
127.0.0.1:6379> GETBIT not-bit 3
(integer) 0
# not-bit:0100
127.0.0.1:6379> BITOP NOT not-bit2 bit-1 bit-2
(error) ERR BITOP NOT must be called with a single source key.# 对bit-1和bit-2进行 异或 操作
127.0.0.1:6379> BITOP XOR xor-bit bit-1 bit-2
(integer) 1
127.0.0.1:6379> GETBIT xor-bit 0
(integer) 0
127.0.0.1:6379> GETBIT xor-bit 1
(integer) 1
127.0.0.1:6379> GETBIT xor-bit 2
(integer) 0
127.0.0.1:6379> GETBIT xor-bit 3
(integer) 0
# xor-bit:0010
# 备注:BITOP执行命令较慢,因为其时间复杂度为O(n)。
# 在进行计数时,如果数据量过大,建议直接将其指派到master-slave中的slave节点进行处理,避免阻塞master
########################################################################################################################################
五、事务
Redis的单条命令是保证原子性的,但是redis事务不能保证原子性
Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
一次性
顺序性
排他性
Redis事务没有隔离级别的概念
Redis单条命令是保证原子性的,但是事务不保证原子性!
Redis事务操作过程
- 开启事务(
multi
) - 命令入队
- 执行事务(
exec
)
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
事务错误
- 代码语法错误(编译时异常)所有的命令都不执行
- 代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** >>> 所以不保证事务原子性
# 命令:
# MULTI 开启事务
# EXEC 执行事务
# DISCARD 关闭事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
QUEUED # 表示命令入队,等待客户端执行事务
127.0.0.1:6379> MGET k1 k2 k3
QUEUED
127.0.0.1:6379> SADD set1 s1 s2 s3 s4 s5
QUEUED
127.0.0.1:6379> SMEMBERS set1
QUEUED
127.0.0.1:6379> LPUSH list l1 l2 l3 l4 l5
QUEUED
127.0.0.1:6379> LRANGE list 0 -1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) 1) "v1"2) "v2"3) "v3"
3) (integer) 5
4) 1) "s3"2) "s2"3) "s1"4) "s4"5) "s5"
5) (integer) 5
6) 1) "l5"2) "l4"3) "l3"4) "l2"5) "l1"
###################################################放弃事务###############################################################
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> MSET k1 v1 k2 v2
QUEUED
127.0.0.1:6379> SET k3 v3
QUEUED
127.0.0.1:6379> DISCARD # 一旦放弃事务,之前入队的全部命令都不会执行
OK
127.0.0.1:6379> GET k3
(nil)
####################################################编译型异常##############################################################
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> mset k1 v1 k2 v2
QUEUED
127.0.0.1:6379> LPUSH list l1 l2 l3
QUEUED
127.0.0.1:6379> GETSET k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> mget k1 k2
QUEUED
127.0.0.1:6379> LRANGE list 0 -1
QUEUED
127.0.0.1:6379> EXEC # 事务执行失败
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 # 编译型异常(又叫入队错误)的特点:事务中有错误的命令,会导致默认放弃事务,所有的命令都不会执行
(nil)
###################################################运行时异常################################################################
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> INCR k1 # 语法正确,但是对一个String类型的k1执行了错误的操作
QUEUED
127.0.0.1:6379> SADD set s1 s2 s3
QUEUED
127.0.0.1:6379> LPUSH list l1 l2 l3
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer or out of range # 第二条命令报错,但是不影响事务整体的运行
3) (integer) 3
4) (integer) 3
5) OK
127.0.0.1:6379> SMEMBERS set
1) "s3"
2) "s2"
3) "s1"
# 运行时异常(又叫执行错误):在事务执行的过程中语法没有出现任何问题,但是它对不同类型的key执行了错误的操作,
# Redis只会将返回的报错信息包含在执行事务的结果中,并不会影响Redis事务的一致性
监控
悲观锁:
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
乐观锁:
- 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
使用watch key
监控指定数据,相当于乐观锁加锁。
正常执行
# 模拟客户转账
127.0.0.1:6379> WATCH money # 语法:WATCH key ... # 对指定key进行监控,监控这个key在事务执行之前是否被修改
OK
127.0.0.1:6379> MULTI # 如果没有被修改,那么这个事务是可以正常执行成功的
OK
127.0.0.1:6379> DECRBY money 20 # 转账20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>
如果被监控的key在事务之外被修改了
127.0.0.1:6379> WATCH money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 30
QUEUED
127.0.0.1:6379> INCRBY out 30
QUEUED
# 这个时候开始模拟另外一个客户端恶意修改被监控的key
# =======================================================表示另一个客户端==============================================================
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> INCRBY money 200 # 修改被监控的数据
(integer) 280
127.0.0.1:6379> get money
"280"
# ===================================================================================================================================
# 再次执行事务,会直接返回nil,代表执行失败
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get money # 再次查看,当前监控的key已经被修改
"280"
# 多聊一嘴:实际上关于WATCH,还有一个命令,UNWATCH,意思是解除所有监控,但是官网的原话是,一旦你执行了DISCARD或者EXEC,就没必要在执行UNWATCH
127.0.0.1:6379> MGET money out
1) "280"
2) "20"
127.0.0.1:6379> WATCH money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 30
QUEUED
127.0.0.1:6379> INCRBY out 30
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 250
2) (integer) 50
解锁获取最新值,然后再加锁进行事务。
unwatch
进行解锁。
注意:每次提交执行exec后都会自动释放锁,不管是否成功
六、Jedis
Jedis是Redis官方推荐的Java连接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>
编码测试
- 连接数据库
- 操作命令
- 断开连接
public class TestPing {public static void main(String[] args) {// new一个Jedis对象Jedis jedis = new Jedis("192.168.1.107", 6379);// Jedis中的API就是之前学习的命令System.out.println(jedis.ping());}
}
不过这边有一个小问题,如果你的Redis是远程连接的话,会出现连接超时或者是拒绝访问的问题,在这边需要做两件事情,当然,防火墙的关闭也是必不可少的
打开redis.conf配置文件
随后进行API测试
public class JedisType {public static void main(String[] args) {Jedis jedis = new Jedis("192.168.1.107", 6379);System.out.println(jedis.ping());// StringSystem.out.println(jedis.set("k1", "v1"));System.out.println(jedis.get("k1")); // v1System.out.println(jedis.append("k1", "+value"));System.out.println(jedis.get("k1")); // v1+valueSystem.out.println(jedis.strlen("k1")); // 8System.out.println("=====================================================");// ListSystem.out.println(jedis.lpush("listKey", "l1", "l2", "l3"));System.out.println(jedis.lrange("listKey", 0, -1)); // [l3, l2, l1]System.out.println(jedis.llen("listKey"));System.out.println("=====================================================");// HashSystem.out.println(jedis.hset("hashKey", "k1", "v1"));System.out.println(jedis.hset("hashKey", "k2", "v2"));System.out.println(jedis.hset("hashKey", "k3", "v3"));System.out.println(jedis.hmget("hashKey", "k1", "k2", "k3")); // [v1, v2, v3]System.out.println(jedis.hgetAll("hashKey")); // {k3=v3, k2=v2, k1=v1}System.out.println("=====================================================");// SetSystem.out.println(jedis.sadd("setKey", "s1", "s2", "s3", "s4"));System.out.println(jedis.smembers("setKey")); // [s2, s1, s4, s3]System.out.println(jedis.scard("setKey"));System.out.println("=====================================================");// ZSetSystem.out.println(jedis.zadd("ZKey", 90, "z1"));System.out.println(jedis.zadd("ZKey", 80, "z2"));System.out.println(jedis.zadd("ZKey", 85, "z3"));System.out.println(jedis.zrange("ZKey", 0, -1)); // [z2, z3, z1]}
}
测试事务
public class TestTX {public static void main(String[] args) {Jedis jedis = new Jedis("192.168.1.107", 6379);jedis.flushDB();JSONObject jsonObject = new JSONObject();jsonObject.put("name", "xiaohuang");jsonObject.put("age", "21");jsonObject.put("sex", "boy");Transaction multi = jedis.multi(); // 开启事务String user = jsonObject.toJSONString();try {multi.set("user1", user);multi.set("user2", user);multi.exec();} catch (Exception e) {multi.discard(); // 出现问题,放弃事务e.printStackTrace();} finally {System.out.println(jedis.mget("user1", "user2"));jedis.close(); // 关闭连接}}
}
七、SpringBoot整合
备注:从SpringBoot2.x之后,原先使用的Jedis被lettuce替代
Jedis:采用直连,模拟多个线程操作会出现安全问题。为避免此问题,需要使用Jedis Pool连接池!类似于BIO模式
lettuce:采用netty网络框架,对象可以在多个线程中被共享,完美避免线程安全问题,减少线程数据,类似于NIO模式
首先先查看RedisAutoConfiguration中的源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {@Bean@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {// 默认的RedisTemplate直接使用此类内部默认设置操作数据,但是Redis对象需要序列化// 泛型都是Object,后面使用的话,大都是RedisTemplate<String, Object>RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);return template;}@Bean@ConditionalOnMissingBeanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}
}
上面的@Import注解导入了两个配置类,有Lettuce和Jedis,可以点开这两个类查看
对比一下可以发现,Jedis配置类中有两个类是默认不存在的,不存在就无法使用
1、导入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、属性配置
# 配置Redis
spring.redis.host=192.168.1.107
spring.redis.port=6379
备注:这边的配置,需要注意的是,SpringBoot整合的是Lettuce,如果在配置文件中添加额外的配置,比如Redis的最大等待时间、超时时间等,在对应的RedisProperties类所映射的配置文件中,属性名称一定要加上带有lettuce,如果加上jedis,它默认不会生效
3、测试连接
@Test
void contextLoads() {ValueOperations ops = redisTemplate.opsForValue();redisTemplate.opsForGeo();ops.set("k1", "xiaohuang");Object o = ops.get("k1");System.out.println(o);
}
测试了之后在控制台可以成功获取
但是在Linux中的客户端取到的却是乱码
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x02k1"
这与RedisTemplate默认序列化有关
先展示RedisTemplate的部分源码
// 这些是RedisTemplate的序列化配置
private @Nullable RedisSerializer keySerializer = null;
private @Nullable RedisSerializer valueSerializer = null;
private @Nullable RedisSerializer hashKeySerializer = null;
private @Nullable RedisSerializer hashValueSerializer = null;@Override
public void afterPropertiesSet() {super.afterPropertiesSet();boolean defaultUsed = false;if (defaultSerializer == null) {// 这边默认使用JDK的序列化方式,可以自定义一个配置类,采用其他的序列化方式defaultSerializer = new JdkSerializationRedisSerializer(classLoader != null ? classLoader : this.getClass().getClassLoader());}}
}
自定义RedisTemplate
// 自定义RedisTemplate
// 这是RedisTemplate的一个模板
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {// 为了开发方便,可以直接使用<String, Object>RedisTemplate<String, Object> template = new RedisTemplate<>();// 序列化配置Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);template.setDefaultSerializer(serializer);template.setConnectionFactory(redisConnectionFactory);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);serializer.setObjectMapper(om);StringRedisSerializer srs = new StringRedisSerializer();// 对于String和Hash类型的Key,可以采用String的序列化方式template.setKeySerializer(srs);template.setHashKeySerializer(srs);// String和Hash类型的value可以使用json的方式进行序列化template.setValueSerializer(serializer);template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;
}
八、自定义Redis工具类
使用RedisTemplate需要频繁调用.opForxxx
然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。
工具类参考博客:
https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html
https://www.cnblogs.com/zhzhlong/p/11434284.html
九、Redis.conf
1、Units
单位,Redis配置文件中的单位对大小写不敏感
2、includes
包含,可以在Redis启动的时候再加载一些除了Redis.conf之外的其他的配置文件,和Spring的import,jsp的include类似
3、NETWORK
网络,表示Redis启动时开放的端口默认与本机绑定
bind 127.0.0.1
Redis指定监听端口,默认为6379
port 6379
表示服务器闲置多长时间(秒)后被关闭,如果这个这个数值为0,表示这个功能不起作用
timeout 300
是否开启保护模式,Redis默认开启,如果没有设置bind的IP地址和Redis密码,那么服务就会默认只能在本机运行
protected-mode yes
4、GENERAL
是否以守护进程的方式运行,即后台运行,一般默认为no,需要手动改为yes
daemonize yes
如果以守护进程的方式运行,就需要指定一个pid文件,在Redis启动时创建,退出时删除
pidfile /var/run/redis_6379.pid
配置日志等级,日志等级的可选项如下(翻译自配置文件,有改动):
- debug:打印的信息较多,在工作中主要用于开发和测试
- verbose:打印的信息仅次于debug,但是格式较为工整
- notice:Redis默认配置,在生产环境中使用
- warning:只打印一些重要信息,比如警告和错误
loglevel notice
打印的日志文件名称,如果为空,表示标准输出,在配置守护进程的模式下会将输出信息保存到/dev/null
logfile ""
数据库支持数量,16个
databases 16
5、SNAPSHOTTING
中文翻译为快照,如果在规定的时间内,数据发生了几次更新,那么就会将数据同步备份到一个文件中
Redis的持久化有两种方式,一种是RDB,一种是AOF。SNAPSHOTTING主要针对的是Redis持久化中的RDB
Redis是一个内存数据库,如果不采用持久化对数据进行保存,那么就会出现断电即失的尴尬场面
# 在900秒内,至少有一个key被修改(添加),就会进行持久化操作
save 900 1
# 在300秒内,至少有10个key被修改,就会进行持久化操作
save 300 10
# 在60秒内,至少有1万个key被修改,就会进行持久化操作
save 60 10000
如果Redis在进行持久化的时候出现错误,是否停止写入,默认为是
top-writes-on-bgsave-error yes
是否在进行数据备份时压缩持久化文件,默认为是,这个操作会耗费CPU资源,可以设置为no
rdbcompression yes
在保存持久化文件的同时,对文件内容进行数据校验
rdbchecksum yes
持久化文件保存的目录,默认保存在当前目录下
dir ./
6、REPLICATION
复制主机上的数据,当前配置所指定的IP和端口号即为主机
# Redis在配置文件中将此配置注释,默认不使用,下同
# replicaof <masterip> <masterport>
如果配置的主机有密码,需要配置此密码以通过master的验证
# masterauth <master-password>
7、SECRULITY
安全,可以在配置文件中设置Redis的登录密码
8、CLIENT
Redis允许存在的客户端的最大数量,默认有一万个
maxclients 10000
Redis配置最大的内存容量
maxmemory <bytes>
内存达到上限之后默认的处理策略
maxmemory-policy noeviction
处理策略有以下几种
- noeviction:默认策略,不淘汰,如果内存已满,添加数据是报错。
- allkeys-lru:在所有键中,选取最近最少使用的数据抛弃。
- volatile-lru:在设置了过期时间的所有键中,选取最近最少使用的数据抛弃。
- allkeys-random: 在所有键中,随机抛弃。
- volatile-random: 在设置了过期时间的所有键,随机抛弃。
- volatile-ttl:在设置了过期时间的所有键,抛弃存活时间最短的数据
9、APPEND ONLY MODE
这是Redis持久化的另一种方式,AOF,AOF模式默认不开启,Redis默认开启的是持久化模式是RDB,在大部分情况下,RDB的模式完全够用
appendonly no
AOF持久化的文件名称
appendfilename "appendonly.aof"
每秒执行一次同步,但是可能会丢失这一秒的数据
# 对于 appendfsync 它有以下几个属性
# appendfsync always 表示每次修改都会进行数据同步,速度较慢,消耗性能
# appendfsync no 不执行同步,不消耗性能
appendfsync everysec # 数据不同步,每秒记录一次
十、持久化—RDB
RDB,全称Redis DataBase。
什么是RDB
在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;
默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。
工作原理
在进行 RDB
的时候,redis
的主线程是不会做 io
操作的,主线程会 fork
一个子线程来完成该操作;
- Redis 调用forks。同时拥有父进程和子进程。
- 子进程将数据集写入到一个临时 RDB 文件中。
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)
触发机制
- save的规则满足的情况下,会自动触发rdb原则
- 执行flushall命令,也会触发我们的rdb原则
- 退出redis,也会自动产生rdb文件
save
使用 save
命令,会立刻对当前内存中的数据进行持久化 ,但是会阻塞,也就是不接受其他操作了;
由于
save
命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save
命令执行速度会非常慢,阻塞所有客户端的请求。
flushall命令
flushall
命令也会触发持久化 ;
触发持久化规则
满足配置条件中的触发条件 ;
可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。
bgsave
bgsave
是异步进行,进行持久化的时候,redis
还可以将继续响应客户端请求 ;
bgsave和save对比
优缺点
优点:
- 适合大规模的数据恢复
- 对数据的完整性要求不高
缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。
- fork进程的时候,会占用一定的内容空间。
十一、持久化AOF
Append Only File
Redis的另一种持久化方式,AOF,全名为Append Only File,它用日志的形式来记录每一个写操作,将Redis执行过的命令进行记录(读操作不记录),只追加文件,不改写文件。Redis在启动时会自动加载AOF持久化的文件重新构建数据,也就是Redis重启会把AOF持久化文件中的信息从前到后执行一次以完成数据的恢复
AOF持久化对应的配置文件的位置是APPEND ONLY MODE
如何启动AOF,看下图
修改完配置之后,只需重新启动就可
这里有一个小细节需要注意:如果AOF和RDB模式在配置文件中都有开启的话,为了保证数据的安全性,在Redis启动时会优先使用AOF
Redis的AOF持久化保存的文件名称就叫做appendonly.aof
如果说appendonly.aof文件的内容发生了一些错误,那么在Redis进行启动时,会出现问题
[root@bogon bin]# redis-server myconfig/redis.conf
1574:C 16 Nov 2020 03:47:18.350 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1574:C 16 Nov 2020 03:47:18.350 # Redis version=5.0.10, bits=64, commit=00000000, modified=0, pid=1574, just started
1574:C 16 Nov 2020 03:47:18.350 # Configuration loaded
[root@bogon bin]# redis-cli -p 6379
# 下面的这一个返回结果,表示连接被拒绝
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> exit
如果appendonly.aof内部发生错误,咋办?Redis中提供了一个可以修复aof文件的修复工具叫做redis-check-aof
怎么使用呢,下面有一条命令
redis-check-aof --fix appendonly.aof[root@bogon bin]# redis-check-aof --fix appendonly.aof
0x 167: Expected \r\n, got: 6769
AOF analyzed: size=383, ok_up_to=351, diff=32
This will shrink the AOF from 383 bytes, with 32 bytes, to 351 bytes
Continue? [y/N]: y
Successfully truncated AOF
Redis优缺点
优点:
它支持 同步记录 和 异步记录 ,对应的配置文件的属性分别是下面两个
appendfsync always # 同步记录,客户端中一有写操作,即刻记录,数据的完整性好,但是性能较差
appendfsync everysec # 异步记录,每秒记录一次,但是服务器如果在这一秒之内宕机,这一秒的数据就会丢失
appendfsync no # 不记录
缺点:
从恢复数据的角度来说,AOF所恢复的数据量一定是比RDB来得大的,从恢复数据的时间的角度来说,AOF的时间也是大于RDB的
AOF的重写机制
AOF持久化本质就是采用日志的形式对文件内容进行追加,为了防止追加之后这个文件变得越大,所以Redis推出了一种针对于AOF文件的重写机制,如果AOF文件的大小超过配置文件中所设定的阈值时,会自动触发重写机制对文件内容进行压缩,只对可以恢复数据的命令进行保留,针对于这种重写机制,也可以在客户端中对这种重写机制进行手动触发,只需要一个命令
bgrewriteaof
原理
当AOF文件持久追加并且越来越大时,Redis会fork出一条新进程来对文件进行重写,和RDB一样,AOF也是先写临时文件再将其替换掉。Redis会对新进程中的数据进行遍历,每次都会遍历set和set有关的命令。重写并没有读取原来的appendonly.aof文件,而是使用命令将内存中的数据库内容进行重写得到一个新的文件
Redis会将上一次重写的AOF文件大小进行记录,如果当前文件的大小超过源文件的一倍并且大小大于64M时就会触发重写操作
可以在配置文件中查看重写的信息
总结
- RDB可以在指定的时间间隔内对数据集进行持久化快照存储
- AOF持久化记录每次客户端发送给Redis服务器的写操作,服务器中重启时会重新执行命令恢复原始数据,AOF持久化的每一次记录都会追加在文件的末尾,并且Redis有重写机制的存在使得AOF的文件被控制在合理的大小
- 如果Redis只做缓存,如果说只希望数据在服务器启动的时候存在,可以不使用任何的持久化方式
- 刚刚上面讲到一个小细节,如果两种持久化同时开启,Redis服务器会默认先找AOF持久化,因为AOF的保存数据集要比RDB要完整,这也就是Redis考虑安全的原因
十三、Redis发布与订阅
Redis的发布订阅(publish/subscribe)是一种消息通信模式,发送者(publish)发送消息,订阅者(subscribe)接收消息
Redis客户端可以订阅任意数量的频道
如图,有三个客户端订阅了一个Channel1
当Channel1的后台发送了一个数据到Channel1的频道中,这三个订阅了Channel1的客户端就会同时收到这个数据
命令:
这些都是用来实现数据通信的命令,现实中的场景可以是网络聊天室,广播等
订阅端
127.0.0.1:6379> SUBSCRIBE smallyellow # 订阅一个频道叫smallyellow
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "smallyellow"
3) (integer) 1
# 一旦开始订阅,会立即占用当前进程去监听自己所订阅的那个Channel
1) "message"
2) "smallyellow"
3) "Hello!I love Java!!"
1) "message"
2) "smallyellow"
3) "Hello!I love Redis!!"
发送端
127.0.0.1:6379> PUBLISH smallyellow "Hello!I love Java!!" # 往频道smallyellow中发布一条消息
(integer) 1
127.0.0.1:6379> PUBLISH smallyellow "Hello!I love Redis!!"
(integer) 1
127.0.0.1:6379>
原理:
Redis是C语言编写,在实现消息的发布和订阅的时候,Redis将其封装在一个后缀名为点c的文件中,pubsub.c
通过subscribe和publish命令实现消息的发布和订阅,当用户订阅了一个频道之后,redis-server里面维护了一个字典,字典里有很多个Channel(频道),字典的值就是一个链表,链表中保存的是订阅了这个频道的用户,使用publish命令往频道发送数据之后,redis-server使用此频道作为key,去遍历这个指定的value链表,将信息依次发送过去
发布订阅的实现场景
1、实时沟通消息系统
2、微信公众号(点击关注,后台发送一篇博客,订阅的用户就可以监听到)
…
还有一些比较复杂的场景,可以使用消息中间件来做,RabbitMQ,RocketMQ,kafka…
十四、Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
- 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
- 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
- 高可用基石:主从复制还是哨兵和集群能够实施的基础。
为什么使用集群
- 单台服务器难以负载大量的请求
- 单台服务器故障率高,系统崩坏概率大
- 单台服务器内存容量有限。
环境配置
我们在讲解配置文件的时候,注意到有一个replication
模块 (见Redis.conf中第8条)
查看当前库的信息: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
既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息:
- 端口号
- pid文件名
- 日志文件名
- rdb文件名
启动单机多服务集群:
一主二从配置
==默认情况下,每台Redis服务器都是主节点;==我们一般情况下只用配置从机就好了!
认老大!一主(79)二从(80,81)
使用SLAVEOF host port
就可以为从机配置主机了。
然后主机上也能看到从机的状态:
我们这里是使用命令搭建,是暂时的,==真实开发中应该在从机的配置文件中进行配置,==这样的话是永久的。
使用规则
-
从机只能读,不能写,主机可读可写但是多用于写。
127.0.0.1:6381> set name sakura # 从机6381写入失败 (error) READONLY You can't write against a read only replica.127.0.0.1:6380> set name sakura # 从机6380写入失败 (error) READONLY You can't write against a read only replica.127.0.0.1:6379> set name sakura OK 127.0.0.1:6379> get name "sakura"
-
当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
-
当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。
-
第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
- 从机手动执行命令
slaveof no one
,这样执行以后从机会独立出来成为一个主机 - 使用哨兵模式(自动选举)
- 从机手动执行命令
如果没有老大了,这个时候能不能选择出来一个老大呢?手动!
如果主机断开了连接,我们可以使用SLAVEOF no one
让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么久重新连接!
十五、哨兵模式
从原本的主从复制来说,如果主机宕掉了,需要手动设置一台从机为一个新的主机,但是这种手动的配置需要人工干预,耗费时间和人力,并且在人工干预的时间内还会造成服务不可用的情况,对于开发者来说这不是一种好的解决方案。Redis从2.8开始出现了哨兵模式
哨兵模式可以理解为谋权篡位的自动版,可以在后台监视主机是否故障,如果发生了故障就会以投票的方式在从机中选出一个作为主机Redis提供了哨兵的命令,并且它是一个特殊的模式,它会创建出一个完全独立于Redis服务器的进程,一个哨兵可以对多台服务器进行监控,并且可以有多个哨兵,每个哨兵会定时发送PING命令给服务器,并且还要在一定时间内得到服务器的响应,得到响应之后哨兵模式才会判定你现在状态正常。如果在规定的时间内它发送的请求主机没有得到响应,那么哨兵便会初步判断,当前主机是主观下线,其余的哨兵发现这台主机没有在规定时间内响应数据,那么便会以每秒一次的频率对主机进行判断,它确实是主观下线了,那么主机就会被标记为客观下线,主机挂掉之后,哨兵便会通过投票的方式在挂掉的主机下的从机中选出一个作为新主机
测试哨兵模式
在Linux中使用编辑器编写一个文件,文件名称叫sentinel.conf,这里需要注意,配置文件名称定死,不能乱写
# 哨兵服务默认端口
port 26379# 哨兵模式默认工作目录
dir /tmp# 属性格式:sentinel monitor 主机名称 IP port 1 后面的数字1代表主机挂了,Slave投票让谁成为新主机
sentinel monitor myredis 127.0.0.1 6379 1
启动哨兵服务
redis-sentinel sentinel.conf
[root@bogon bin]# redis-sentinel sentinel.conf
1616:X 17 Nov 2020 11:11:25.752 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1616:X 17 Nov 2020 11:11:25.752 # Redis version=5.0.10, bits=64, commit=00000000, modified=0, pid=1616, just started
1616:X 17 Nov 2020 11:11:25.752 # Configuration loaded
1616:X 17 Nov 2020 11:11:25.753 * Increased maximum number of open files to 10032 (it was originally set to 1024)._._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 5.0.10 (00000000/0) 64 bit.-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in sentinel mode|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379| `-._ `._ / _.-' | PID: 1616`-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' 1616:X 17 Nov 2020 11:11:25.753 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
1616:X 17 Nov 2020 11:11:25.753 # Sentinel ID is 8cfc91b9eb7651b632c2dc77dd1af983c6ccdb78
1616:X 17 Nov 2020 11:11:25.753 # +monitor master myredis 127.0.0.1 6379 quorum 1
1616:X 17 Nov 2020 11:11:25.755 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
1616:X 17 Nov 2020 11:11:25.759 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
如果Master主节点挂掉了,从机中就会随机选择一个出来成为一个新的Master,这是C语言的内部算法,可以在哨兵服务的进程日志中查看新的Master是谁
就算此时原来的Master回来了,在6380面前也只能是个弟弟
哨兵模式的优劣
优点:
1、哨兵集群是基于主从复制来实现的,主从复制的优点全部具备
2、主从可以切换,故障可以转移,提升系统可用性
3、哨兵模式就是主从模式的升级,谋权篡位的手动到自动,更加健壮
缺点:
1、在线扩容比较麻烦,集群的数量达到上限,就会变得十分繁琐
2、实现哨兵模式的配置较为麻烦,如果出现故障,还会涉及到一些shell脚本的运行,这些都是非常麻烦的操作
完整的哨兵模式配置文件 sentinel.conf
# Example sentinel.conf# 哨兵sentinel实例运行的端口 默认26379
port 26379# 哨兵sentinel的工作目录
dir /tmp# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000# SCRIPTS EXECUTION#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>sentinel 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>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
关于哨兵模式的全部配置,有兴趣的建议你们看看这篇博客:https://blog.csdn.net/u012441222/article/details/80751390
十六、缓存穿透与雪崩
缓存穿透
概念
在大多数场景中,数据库里的id字段一般来说都是自增。如果说,用户发送了一个请求,会首先进入缓存,查不到,就进入数据库,进了数据库,也查不到,查不到的话,如果说访问量较少,那还好,直接返回不存在嘛,因为这种少量的缓存穿透实际上是不可避免的,但是,一旦有一些不怀好意的坏蛋,在发送请求查询数据库的时候,主键的id字段故意给你来一个负数或者是一些远大于数据库最大id的数,而且进行巨大量的并发访问,这时候缓存中肯定是没有的,那这些请求就直接压给数据库了,数据库扛不住这么大的东西呀,那咋办,不解决数据库就只能挂掉呀。对此,有三种解决方案
1、在进行项目的整合时需要使用到API接口层,在接口层中定义自己的规则,对于不合法的参数可以直接返回,对于调用此接口的API的对象进行严查,任何可能发生的情况都要考虑到
2、在缓存中设置一个空对象,使用空对象完成后续请求
3、使用一个东西,叫做布隆过滤器(Bloom Filter),布隆过滤器使用的是一个bit数组和一个hash算法实现的数据结构,并且内存的使用率极低。使用布隆过滤器可以快速判断当前key是否存在,和Java的Optional类有点相似,布隆过滤器告诉你这个key不存在,那么它就一定不存在
缓存击穿
概念
缓存击穿,它是缓存穿透的一种特殊情况,一般情况下没有公司会去实现这样的业务,因为没有这样一条非常非常高频的热点数据能够搞垮一台服务器,可能性是非常小的
举个栗子,如果有一个非常高频的热点key,在某一个时刻过期,与此同时又有非常非常多的请求并发访问这个key,因为缓存时间已过,现在全部的请求又开始全部压在数据库上面了,很容易导致服务器挂掉
解决方案
1.设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
2.加互斥锁(分布式锁)
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
缓存雪崩
概念
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
- redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
- 限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
参考:https://blog.csdn.net/weixin_43873227/article/details/106107270
整理不易,如果对您有帮助,请点赞+关注,一起进步,Fighting!
这篇关于一篇文章教你学会并使用Redis-转自狂神的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!