【狂神说】 Redis 笔记分享

Nosql概述

为什么使用Nosql

1、单机Mysql时代

在这里插入图片描述

90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题

  1. 数据量增加到一定程度,单机数据库就放不下了
  2. 数据的索引(B+ Tree),一个机器内存也存放不下
  3. 访问量变大后(读写混合),一台服务器承受不住。

2、Memcached(缓存) + Mysql + 垂直拆分(读写分离

网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!

在这里插入图片描述

优化过程经历了以下几个过程:

  1. 优化数据库的数据结构和索引(难度大)

  2. 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了

  3. MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。

3、分库分表 + 水平拆分 + MySQL集群

本质:数据库的读+写

在这里插入图片描述

4、如今最近的年代

如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。

目前一个基本的互联网项目

在这里插入图片描述

为什么要用NoSQL ?

用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用NoSQL数据库的,Nosql可以很好的处理以上的情况!

什么是Nosql

**NoSQL = Not Only SQL(不仅仅是SQL)**Q

Not Only Structured Query Language

关系型数据库:列+行,同一个表下数据的结构是一样的。

非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。

NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。

Nosql特点

  1. 方便扩展(数据之间没有关系,很好扩展!)
  2. 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
  3. 数据类型是多样型的!(不需要事先设计数据库,随取随用)
  4. 传统的 RDBMS 和 NoSQL

传统的 RDBMS(关系型数据库)

  • 结构化组织
  • SQL
  • 数据和关系都存在单独的表中 row col
  • 操作,数据定义语言
  • 严格的一致性
  • 基础的事务

Nosql

  • 不仅仅是数据
  • 没有固定的查询语言
  • 键值对存储,列存储,文档存储,图形数据库(社交关系)
  • 最终一致性
  • CAP定理和BASE
  • 高性能,高可用,高扩展

了解3V与3高

大数据时代的3V :主要是描述问题的

海量Velume

多样Variety

实时Velocity

大数据时代的3高 : 主要是对程序的要求

高并发

高可扩

高性能

真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的

阿里巴巴演进分析

推荐阅读:阿里云的这群疯子https://yq.aliyun.com/articles/653511

1

在这里插入图片描述

# 商品信息
- 一般存放在关系型数据库: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是关系型数据库和非关系型数据库的中间产品。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的安装

Windows安装Redis

https://github.com/dmajkic/redis

  1. 解压安装包
    在这里插入图片描述
  2. 开启redis-server.exe
  3. 启动redis-cli.exe测试在这里插入图片描述

Linux安装

  1. 下载安装包!redis-6.2.6tar.gz

  2. 解压Redis的安装包!程序一般放在 /opt 目录下

    在这里插入图片描述

  3. 基本环境安装

    yum install gcc-c++
    # 然后进入redis目录下执行
    make
    # 然后执行
    make install
    

在这里插入图片描述

  1. redis默认安装路径 /usr/local/bin在这里插入图片描述

  2. 将redis的配置文件复制到 程序安装目录 /usr/local/bin/my_config

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hxvGQ47d-1597890996509)(狂神说 Redis.assets/image-20200813114000868.png)]

  3. redis默认不是后台启动的,需要修改配置文件!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dDdKTUgd-1597890996510)(狂神说 Redis.assets/image-20200813114019063.png)]

  4. 通过指定的配置文件启动redis服务

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jOypL57Z-1597890996511)(狂神说 Redis.assets/image-20200813114030597.png)]

  5. 使用redis-cli连接指定的端口号测试,Redis的默认端口6379,-p选项能够指定连接的端口号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnDaISQ4-1597890996512)(狂神说 Redis.assets/image-20200813114045299.png)]

  1. 查看redis进程是否开启

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9PhN1jC1-1597890996513)(狂神说 Redis.assets/image-20200813114103769.png)]

  2. 关闭Redis服务 shutdown

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y54EuOYm-1597890996514)(狂神说 Redis.assets/image-20200813114116691.png)]

  3. 再次查看进程是否存在

  4. 后面我们会使用单机多Redis启动集群测试

测试性能

**redis-benchmark:**Redis官方提供的性能测试工具,参数选项如下:

img

简单测试:

# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plMshjFg-1597890996515)(狂神说 Redis.assets/image-20200813114143365.png)]

基础知识

redis默认有16个数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v2S3n3Si-1597890996516)(狂神说 Redis.assets/image-20200813114158322.png)]

默认使用的第0个数据库

16个数据库为:DB 0~DB 15
可以使用select n切换到DB n,dbsize可以查看当前数据库的大小,与key数量相关,get database查看当前数据库

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许可)的,内存中的数据结构存储系统,它可以用作数据库缓存消息中间件MQ

它支持多种类型的数据结构,如 字符串(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中所有的键

keys *  # 查看当前所有的key

设置键值

set key value   # 设置键为一个新的值

判断key是否存在

EXISTS key # 判断当前的key是否存在,如果存在返回 1

移除一个key

move key database # 选择一个数据库 移除该库中的key值

设置一个key的过期时间,单位是秒

EXPIRE key time # 让一个key的剩余时间为time,剩余时间为0是该key被删除

查看一个key的过期时间

ttl key # 如果该key没有设置过期时间,返回一个负数

查看key的具体类型

type key # 返回key的具体类型

举例说明

127 .0.0.1:6379> keys *  # 查看所有的key
(empty list or set)
127 .0.0.1:6379> set name lisen  # set key
OK
127 .0.0.1:6379> keys *
1 ) "name"
127 .0.0.1:6379> set age 1
OK
127 .0.0.1:6379> keys *
1 ) "age"
2 ) "name"
127 .0.0.1:6379> EXISTS name  # 判断当前的key是否存在
(integer) 1
127 .0.0.1:6379> EXISTS name
(integer) 0
127 .0.0.1:6379> move name 1 # 移除当前的key
(integer) 1
127 .0.0.1:6379> keys *
1 ) "age"
127 .0.0.1:6379> set name lisen
OK
127 .0.0.1:6379> keys *
1 ) "age"
2 ) "name"
127 .0.0.1:6379> clear
127 .0.0.1:6379> keys *
1 ) "age"
2 ) "name"
127 .0.0.1:6379> get name
"qinjiang"
127 .0.0.1:6379> EXPIRE name 10 #设置key的过期时间,单位是秒
(integer) 1
127 .0.0.1:6379> ttl name  # 查看当前key的剩余时间
(integer) 4
127 .0.0.1:6379> ttl name
(integer) 3
127 .0.0.1:6379> ttl name
(integer) 2
127 .0.0.1:6379> ttl name
(integer) 1
127 .0.0.1:6379> ttl name
(integer) -
127 .0.0.1:6379> get name # 就会自动删除 keys *也就没了
(nil)
127 .0.0.1:6379> type name  # 查看当前key的一个类型!string
127 .0.0.1:6379> type age   # string

五大基本数据类型

String(字符串)

String类型的使用场景:value除了字符串还可以是数字,进行数字的操作与运算!

  • 计数器
  • 统计多单位的数量 uid
  • 粉丝数zasszz

基本使用

追加字符串

APPEND key str # 将str追加到key键对应字符串之后

获取字符串长度

STRLEN key # 获取字符串长

数字操作

incr key	# key对应的值++
decr key	# key对应的值--

INCRBY key value #  key对应的值+=value
DECRBY key value #	key对应的值-=value

截取字符串

GETRANGE key start end 

该命令能够截取子串,范围为:[start,end]如果end=-1代表截取至最后一个字符

字符串替换

SETRANGE key offset value

从offset索引位置开始的字符串替换为value

声明与赋值

setex key time value # 声明一个key其值为value,设置time秒后过期
setnx key value # 如果key这个键不存在,则创建key

setnx命令通常在分布式中使用,作为乐观锁的一种实现方法!

mset key1 value1 key2 value2 .... # mset 能够一次声明多个key-value
msetnx key1 value1 key2 value2 ... # 同样是多次声明

# 与之对应的 一次性获取多个键值
mget key1 key2 key3 ...

msetnx是一个原子操作,如果其中有一个key在数据库中存在,那么其余声明均不会执行!

getset key new_value # 先获取key的value,再将new_value修改为key的value

如果原来key不存在,getset 返回null

举例说明

127.0.0.1:6379> set key1 v1 # 设置值
OK
127.0.0.1:6379> get key1 # 获得值
"v1"
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> EXISTS key1 #判断是否存在
(integer) 1
127.0.0.1:6379> APPEND key1 hello,world # 追加到末尾
(integer) 13
127.0.0.1:6379> get key1
"v1hello,world"
127.0.0.1:6379> STRLEN key1 # 获取该字符串的长度
(integer) 13
127.0.0.1:6379> 
#######################################################################
# 数值计算
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> incr views	# 增加1
(integer) 1
127.0.0.1:6379> decr views	# 减少1
(integer) 0
127.0.0.1:6379> incrby views 10 # 增加10
(integer) 10
127.0.0.1:6379> decrby views 10	# 减少10
(integer) 0
#######################################################################
# 截取字符
127.0.0.1:6379> set key1 hello,world
OK
127.0.0.1:6379> get key1
"hello,world"
127.0.0.1:6379> getrange key1 0 1
"he"
127.0.0.1:6379> getrange key1 0 -1
"hello,world"
#######################################################################
# 替换字符
127.0.0.1:6379> setrange key1 0 HELLO
(integer) 11
127.0.0.1:6379> get key1
"HELLO,world"
127.0.0.1:6379> 
#######################################################################
# 声明字符串
127.0.0.1:6379> setex name 100 cjx # 声明一个key 'name' 其值为cjx,在100s后将过期
OK
127.0.0.1:6379> ttl name
(integer) 91

127.0.0.1:6379> keys *
1) "name"
2) "key1"
127.0.0.1:6379> setnx name cjx2 # 此时name仍然存在,声明失败
(integer) 0
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> setnx name cjx2 # 此时name过期,声明成功!
(integer) 1
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
################
127.0.0.1:6379> keys *
1) "name"
2) "key1"
3) "k3"
4) "k2"
5) "k1"
127.0.0.1:6379> mget k1 k2 k3	# 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k3 v33 k4 v4 # 可以看到msetnx是一个原子操作,要么都成功要么都不成功
(integer) 0
127.0.0.1:6379> keys *
1) "name"
2) "key1"
3) "k3"
4) "k2"
5) "k1"
################
127.0.0.1:6379> getset db INNODB # 先获取值,在设置值
(nil)
127.0.0.1:6379> get db # 获取新的值
"INNODB"

List(列表)

redis中,List可以作为:堆栈、队列、阻塞队列!

所有的List命令都是以 ’ l '开头的

插入元素

LPUSH key value # 将value推入key对应的list的头部,类似于push_front
RPUSH key value # 将vlaue值推入list的尾部,类似于push_back
# 将value插入到preValue之前或之后,只会在第一个匹配元素后面插入
Linsert key before|after preValue value 

获取元素

LINDEX key index # 获取index下标的值,不存在即返回nil
LRANGE key start end # 获取指定范围的值

LRANGE 类似于getRange命令,获取到[start,end]的元素,如果end==-1,则获取至队尾的元素。

移除元素

# 边界移除
LPOP key # 移除key对应list的头部元素
RPOP key # 移除list的尾部元素

# 删除指定的value,删除个数为count
lrem key count value

rpoplpush list1 list2 # 移除list1右侧元素添加到list2左侧

使用LPOPRPOP命令移除的元素会被返回!

lrem指令会删除 count 个value元素,如果count为0则删除list中所有值为value元素

返回长度

LLen key # 能够返回list的长度

截取元素

ltrim key start end

ltrim命令能够让原列表截取[start,end]的部分并赋值给原list

替换元素

LSET key index value # 将key对应list列表指定index值替换为value

需要注意:如果列表不存在或列表index索引位置为空,则会报错!

举例说明

######################################################################
# 获取元素与插入元素
127.0.0.1:6379> LPUSH list1 one two three # 在头部添加 one two three 元素
(integer) 3
127.0.0.1:6379> LRANGE list1 0 -1 # 查看所有元素
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> RPUSH list1 zero # 在尾部添加 zero 元素
(integer) 4
127.0.0.1:6379> LRANGE list1 0 -1
1) "three"
2) "two"
3) "one"
4) "zero"
# Lindex
127.0.0.1:6379> LINDEX list1 1
"two"
# Linsert 
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "three"
3) "fout"
127.0.0.1:6379> linsert list before "two" one # 在two之前插入one
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "three"
4) "fout"
#######################################################################
# 移除元素
127.0.0.1:6379> LPOP list1
"three"
127.0.0.1:6379> LRANGE list1 0 -1
1) "two"
2) "one"
3) "zero"
127.0.0.1:6379> RPOP list1 
"zero"
127.0.0.1:6379> LRANGE list1 0 -1
1) "two"
2) "one"
# 	匹配删除
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "twoo"
3) "twoo"
4) "twoo"
127.0.0.1:6379> lrem list 0 twoo # 删除所有值为 twoo 的项
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "two"

#######################################################################
# 截取元素
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "three"
3) "fout"
4) "five"
127.0.0.1:6379> ltrim list 0 2
OK
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "three"
3) "fout"

Set(集合)

与Java一样,Redis中的set无序、不重复

添加元素

sadd key value... # 添加任意个value元素

查看元素

smembers key  # 查看指定Set的所有值
sismember key value # 判断某一个值是否在set中

查看元素个数

scard key # 查看set中的元素个数

移除元素

srem key value # 移除set中的value元素

随机操作

srandmember key # 随机抽出一个元素
spop key # 随机删除一个元素并返回

移动元素

smove set1 set2 value # 将set1中的value移动到set2

集合操作

# 差集
SDIFF key1 key2 # 返回key1拥有但是key2没有的元素
# 并集
SINTER key1 key2 
# 交集
SUNION key1 key2 

集合操作允许多集合运算,如果只有一个集合作为参数那么其比较对象就是自己。

举例说明

---------------SADD--SCARD--SMEMBERS--SISMEMBER--------------------

127.0.0.1:6379> SADD myset m1 m2 m3 m4 # 向myset中增加成员 m1~m4
(integer) 4
127.0.0.1:6379> SCARD myset # 获取集合的成员数目
(integer) 4
127.0.0.1:6379> smembers myset # 获取集合中所有成员
1) "m4"
2) "m3"
3) "m2"
4) "m1"
127.0.0.1:6379> SISMEMBER myset m5 # 查询m5是否是myset的成员
(integer) 0 # 不是,返回0
127.0.0.1:6379> SISMEMBER myset m2
(integer) 1 # 是,返回1
127.0.0.1:6379> SISMEMBER myset m3
(integer) 1

---------------------SRANDMEMBER--SPOP----------------------------------

127.0.0.1:6379> SRANDMEMBER myset 3 # 随机返回3个成员
1) "m2"
2) "m3"
3) "m4"
127.0.0.1:6379> SRANDMEMBER myset # 随机返回1个成员
"m3"
127.0.0.1:6379> SPOP myset 2 # 随机移除并返回2个成员
1) "m1"
2) "m4"
# 将set还原到{m1,m2,m3,m4}

---------------------SMOVE--SREM----------------------------------------

127.0.0.1:6379> SMOVE myset newset m3 # 将myset中m3成员移动到newset集合
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "m4"
2) "m2"
3) "m1"
127.0.0.1:6379> SMEMBERS newset
1) "m3"
127.0.0.1:6379> SREM newset m3 # 从newset中移除m3元素
(integer) 1
127.0.0.1:6379> SMEMBERS newset
(empty list or set)

-----------------------------SDIFF------------------------------------
# 下面开始是多集合操作,多集合操作中若只有一个参数默认和自身进行运算
# setx=>{m1,m2,m4,m6}, sety=>{m2,m5,m6}, setz=>{m1,m3,m6}
127.0.0.1:6379> SDIFF setx sety setz # 等价于setx-sety-setz
1) "m4" #这就是看setx中与其他集合中有不一样的列出来 只针对setx中的列出来
127.0.0.1:6379> SDIFF setx sety # setx - sety
1) "m4"
2) "m1"
127.0.0.1:6379> SDIFF sety setx # sety - setx
1) "m5"

-------------------------SINTER---------------------------------------
# 共同关注(交集)

127.0.0.1:6379> SINTER setx sety setz # 求 setx、sety、setx的交集
1) "m6"
127.0.0.1:6379> SINTER setx sety # 求setx sety的交集
1) "m2"
2) "m6"

-------------------------SUNION---------------------------------------

127.0.0.1:6379> SUNION setx sety setz # setx sety setz的并集
1) "m4"
2) "m6"
3) "m3"
4) "m2"
5) "m1"
6) "m5"
127.0.0.1:6379> SUNION setx sety # setx sety 并集
1) "m4"
2) "m6"
3) "m2"
4) "m1"
5) "m5"

Hash(哈希集合)

Map集合,key-Map集合,这时候值是一个Map集合。每一个key对应着一个HashMap,HashMap存放的键值对均是String类型

添加元素

hset key key_map value # 添加一个键值对 key_map--value 添加到key对应的HashMap中
hmset key key_map1 value1 key_map2 value2 ... # 允许添加多个键值对
hsetnx key key_map value  # 如果该键不存在则设置

获取元素

# 键值对获取
hget key key_map # 获取key对应的Map中获取key_map对应的键值对
hmget key key_map1 key_map2 ... # 获取多个键值对
hgetall key # 获取全部的键值对

# 键获取
hkeys key # 获取全部的键

# 值获取
hvals key # 获取全部的值

删除元素

hdel key key_map # 删除hash指定的字段,其value也会被删除

获取数量

hlen key #  获取hash表的字段数量

判断是否存在

HEXISTS key key_map # 判断hash中key_map 是否存在

数字操作

HINCRBY key key_map value #  key_map对应的值+=value
HDECRBY key key_map value #	 key_map对应的值-=value
------------------------HSET--HMSET--HSETNX----------------

127.0.0.1:6379> HSET studentx name sakura # 将studentx哈希表作为一个对象,设置name为sakura
(integer) 1
127.0.0.1:6379> HSET studentx name gyc # 重复设置field进行覆盖,并返回0
(integer) 0
127.0.0.1:6379> HSET studentx age 20 # 设置studentx的age为20
(integer) 1
127.0.0.1:6379> HMSET studentx sex 1 tel 15623667886 # 设置sex为1,tel为15623667886
OK
127.0.0.1:6379> HSETNX studentx name gyc # HSETNX 设置已存在的field
(integer) 0 # 失败
127.0.0.1:6379> HSETNX studentx email 12345@qq.com
(integer) 1 # 成功

----------------------HEXISTS--------------------------------
127.0.0.1:6379> HEXISTS studentx name # name字段在studentx中是否存在
(integer) 1 # 存在
127.0.0.1:6379> HEXISTS studentx addr
(integer) 0 # 不存在

-------------------HGET--HMGET--HGETALL-----------
127.0.0.1:6379> HGET studentx name # 获取studentx中name字段的value
"gyc"
127.0.0.1:6379> HMGET studentx name age tel # 获取studentx中name、age、tel字段的value
1) "gyc"
2) "20"
3) "15623667886"
127.0.0.1:6379> HGETALL studentx # 获取studentx中所有的field及其value
 1) "name"
 2) "gyc"
 3) "age"
 4) "20"
 5) "sex"
 6) "1"
 7) "tel"
 8) "15623667886"
 9) "email"
10) "[email protected]"


--------------------HKEYS--HLEN--HVALS--------------
127.0.0.1:6379> HKEYS studentx # 查看studentx中所有的field
1) "name"
2) "age"
3) "sex"
4) "tel"
5) "email"
127.0.0.1:6379> HLEN studentx # 查看studentx中的字段数量
(integer) 5
127.0.0.1:6379> HVALS studentx # 查看studentx中所有的value
1) "gyc"
2) "20"
3) "1"
4) "15623667886"
5) "[email protected]"

-------------------------HDEL--------------------------
127.0.0.1:6379> HDEL studentx sex tel # 删除studentx 中的sex、tel字段
(integer) 2
127.0.0.1:6379> HKEYS studentx
1) "name"
2) "age"
3) "email"

-------------HINCRBY--HINCRBYFLOAT------------------------
127.0.0.1:6379> HINCRBY studentx age 1 # studentx的age字段数值+1
(integer) 21
127.0.0.1:6379> HINCRBY studentx name 1 # 非整数字型字段不可用
(error) ERR hash value is not an integer
127.0.0.1:6379> HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6
"90.8"

Zset(有序集合)

在Set集合的基础上,增加了一个值Score,用于排序。如果Score相同,就按照value进行字典排序。

添加元素

zadd key score value... # 添加value进入key集合,其排序值为Score,可添加多个值

输出

# 以正序输出元素,根据索引控制数据
zrange key min max [BYSCORE|BYLEX] [WITHSCORE] [limit offset count]
  • BYSCORE:根据score值排序
  • BYLEX:根据value值排序,如果是字符串就根据字典序排序
# 根据Score排序并以正序输出元素,根据数据范围控制
zRangeByScore key min max [WITHSCORES] [limit offset count] 
  • min、max:输出数据的范围,在该范围的项才能参与排序,一般使用 -inf +inf表示所有项均参与排序
  • WITHSCORES:同时显示value与score
  • limit 、offset、count:类似于sql语句
# 排序并以逆序输出元素
zRevRange key start stop [WITHSCORES]

输出[start,stop]索引的元素,如果stop为-1,等价于输出至最后一个元素

移除元素

zrem key value # 移除zset中的value元素

查询

zcard key # 查看set中元素总数
zcount key min max # 统计score 位于[min,max]的元素个数
zrank key member # 查询member元素位于sortedset的索引位置
zscore key member # 查询member元素的score

集合操作

# 差集
ZDIFF key1 key2 # 返回key1拥有但是key2没有的元素
# 并集
ZINTER key1 key2 
# 交集
ZUNION key1 key2 

# 差集并存储
ZDIFFSTORE 目标集合 numbers key1 key2 key3...
# 并集并存储
ZUNIONSTORE 目标集合 numbers key1 key2 key3... 

ZDIFF命令能够根据key1元素比较key2、key3输入集合求出差集并存储与目标集合中,输入集合数量由numbers给定

-------------------ZADD--ZCARD--ZCOUNT--------------
127.0.0.1:6379> ZADD myzset 1 m1 2 m2 3 m3 # 向有序集合myzset中添加成员m1 score=1 以及成员m2 score=2..
(integer) 2
127.0.0.1:6379> ZCARD myzset # 获取有序集合的成员数
(integer) 2
127.0.0.1:6379> ZCOUNT myzset 0 1 # 获取score在 [0,1]区间的成员数量
(integer) 1
127.0.0.1:6379> ZCOUNT myzset 0 2
(integer) 2
----------------ZINCRBY--ZSCORE--------------------------
127.0.0.1:6379> ZINCRBY myzset 5 m2 # 将成员m2的score +5
"7"
127.0.0.1:6379> ZSCORE myzset m1 # 获取成员m1的score
"1"
127.0.0.1:6379> ZSCORE myzset m2
"7"

--------------ZRANK--ZRANGE-----------------------------------
127.0.0.1:6379> ZRANK myzset m1 # 获取成员m1的索引,索引按照score排序,score相同索引值按字典顺序顺序增加
(integer) 0
127.0.0.1:6379> ZRANK myzset m2
(integer) 2
127.0.0.1:6379> ZRANGE myzset 0 1 # 获取索引在 0~1的成员
1) "m1"
2) "m3"
127.0.0.1:6379> ZRANGE myzset 0 -1 # 获取全部成员
1) "m1"
) "m3"
3) "m2"

#testset=>{abc,add,amaze,apple,back,java,redis} score均为0
------------------ZRANGEBYLEX---------------------------------
# 区间用法 
127.0.0.1:6379> zadd testset 0 abc 0 add 0 amaze 0 apple 0 back 0 java 0 redis
(integer) 7
127.0.0.1:6379> ZRANGEBYLEX testset - + # 返回所有成员 - +代表所有的区间
1) "abc"
2) "add"
3) "amaze"
4) "apple"
5) "back"
6) "java"
7) "redis"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 0 3 # 分页 按索引显示查询结果的 0,1,2条记录
1) "abc"
2) "add"
3) "amaze"
127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 3 3 # 显示 3,4,5条记录
1) "apple"
2) "back"
3) "java"
127.0.0.1:6379> ZRANGEBYLEX testset (- [apple # 显示 (-,apple] 区间内的成员 就是展示apple及前面的数据
1) "abc"
2) "add"
3) "amaze"
4) "apple"
127.0.0.1:6379> ZRANGEBYLEX testset [apple [java # 显示 [apple,java]字典区间的成员
1) "apple"
2) "back"
3) "java"
# 其实就是左开右开 但是-代表所有所以( [都是一样的
127.0.0.1:6379> ZRANGEBYLEX testset (- (apple
1) "abc"
2) "add"
3) "amaze"

------------------ZREM--ZREMRANGEBYLEX--ZREMRANGBYRANK--ZREMRANGEBYSCORE--------------------------------
127.0.0.1:6379> ZREM testset abc # 移除成员abc
(integer) 1
127.0.0.1:6379> ZREMRANGEBYLEX testset [apple [java # 移除字典区间[apple,java]中的所有成员
(integer) 3
127.0.0.1:6379> ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成员
(integer) 2
127.0.0.1:6379> ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成员
(integer) 2

# testset=> {abc,add,apple,amaze,back,java,redis} score均为0
# myzset=> {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)}
----------------ZREVRANGE--ZREVRANGEBYSCORE--ZREVRANGEBYLEX-----------
# 先倒排序 再筛选
# ZREVRANGE 就是zset reverse range 反转根据索引展示
127.0.0.1:6379> zadd myset 1 m1 2 m2 3 m3 4 m4 7 m7 9 m9
127.0.0.1:6379> ZRANGE myset 0 -1
1) "m1"
2) "m2"
3) "m3"
4) "m4"
5) "m7"
6) "m9"

127.0.0.1:6379> ZREVRANGE myset 0 3 # 按score递减排序,然后按索引,返回结果的 0~3
1) "m9"
2) "m7"
3) "m4"
4) "m3"

127.0.0.1:6379> ZREVRANGE myset 2 4 # 返回排序结果的 索引的2~4
1) "m4"
2) "m3"
3) "m2"

# 按score递减顺序 返回集合中分数在[2,6]之间的成员
127.0.0.1:6379> ZREVRANGEBYSCORE myset 6 2
1) "m4"
2) "m3"
3) "m2"
127.0.0.1:6379> ZREVRANGEBYSCORE myset 2 6 
(empty list or set) # 这样写是报错的 因为不存在
127.0.0.1:6379> ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典区间的成员
1) "java"
2) "back"
3) "apple"
4) "amaze"

-----------------------------补充---------------------------
# 补充这个是自动排序的根据首字母 如果score最小就最前面 否则在后面
127.0.0.1:6379> ZRANGE testset 0 -1
1) "java"
2) "redis"
# 这边发现添加的两个都是score为0的根据首字母排序了
127.0.0.1:6379> ZADD testset 0 hehe 0 xixi
(integer) 2
127.0.0.1:6379> ZRANGE testset 0 -1
1) "hehe"
2) "java"
3) "redis"
4) "xixi"
# 然后设置了score为1的 aaa实在最后
127.0.0.1:6379> ZADD testset 1 aaa
(integer) 1
127.0.0.1:6379> ZRANGE testset 0 -1
1) "hehe"
2) "java"
3) "redis"
4) "xixi"
5) "aaa"
-------------------------ZREVRANK------------------------------
# ZREVRANK 就是先倒排 再根据值找索引 位置
127.0.0.1:6379> ZREVRANK myset m7 # 按score递减顺序,返回成员m7索引
(integer) 1
127.0.0.1:6379> ZREVRANK myset m2
(integer) 4

# mathscore=>{(xm,90),(xh,95),(xg,87)} 小明、小红、小刚的数学成绩
# enscore=>{(xm,70),(xh,93),(xg,90)} 小明、小红、小刚的英语成绩
-------------------ZINTERSTORE--ZUNIONSTORE---------------------------------
127.0.0.1:6379> ZRANGE mathscore 0 -1
1) "xg"
2) "xm"
3) "xh"
127.0.0.1:6379> ZRANGE enscore 0 -1
1) "xm"
2) "xg"
3) "xh"
127.0.0.1:6379> ZRANGE enscore 0 -1 withscores
1) "xm"
2) "70"
3) "xg"
4) "90"
5) "xh"
6) "93"

# 将mathscore enscore进行合并 结果存放到sumscore
127.0.0.1:6379> ZINTERSTORE sumscore 2 mathscore enscore
(integer) 3
# 合并后的score是之前集合中所有score的和
127.0.0.1:6379> ZRANGE sumscore 0 -1 withscores
1) "xm"
2) "160"
3) "xg"
4) "177"
5) "xh"
6) "188"

# 就是取 两个数据的最小的值展示 带score排序的
127.0.0.1:6379> ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取两个集合的成员score最小值作为结果的
(integer) 3
127.0.0.1:6379> ZRANGE lowestscore 0 -1 withscores
1) "xm"
2) "70"
3) "xg"
4) "87"
5) "xh"
6) "93"

三种特殊数据类型

geospatial 地理位置

添加地理位置 (geoadd)

#      key键	 经度			纬度			 名称(城市、人名等等)
geoadd key latitude(纬度) longitude(经度) name ...

规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!

  • 有效的经度从-180度到180度。

  • 有效的纬度从-85.05112878度到85.05112878度。

当坐标位置超出上述指定范围时,该命令将会返回一个错误。

获取地理位置(geopos)

geopos key member [member...] # 获取集合中一个/多个成员坐标

返回的一定是一个坐标!

返回两个位置直线距离(geodist)

geodist key member1 member2  [unit]

该指令默认是以米为单位,通过修改unit改变输出单位,可输入值为:

  • 米 m
  • 千米 km
  • 英米 mi
  • 英尺 mt

给定的经纬度为中心,找出某一半径内的元素(georadius )

georadius key longitude latitude radius [unit] [withcoord] [count] [withdist] [count n]
  • unit : 单位,使用规则等同geodist
  • withcoord :显示经度与纬度
  • withdist:显示距离,单位与半径单位相同
  • count n :只显示前n个数据,按照距离递增排序

给定坐标为中心,找出某一半径内的元素(georadiusbymember)

georadiusbyMember key member radius [unit] [withcoord] [count] [withdist] [count n]

GEO底层的实现原理是Zset,可以使用Zset的操作处理GEO数据

127.0.0.1:6379[13]> ZRANGE china:city 0 -1 # 查看所有元素
1) "chongqin"
2) "shenzheng"
3) "hangzhou"
4) "jiangsu"
5) "shanghai"
6) "tianjing"
7) "beijing"
127.0.0.1:6379[13]> ZRANGEBYLEX china:city - + # 排序输出
1) "chongqin"
2) "shenzheng"
3) "hangzhou"
4) "jiangsu"
5) "shanghai"
6) "tianjing"
7) "beijing"
127.0.0.1:6379[13]> ZREM china:city jiangsu # 移除元素
(integer) 1
127.0.0.1:6379[13]> ZRANGE china:city 0 -1	
1) "chongqin"
2) "shenzheng"
3) "hangzhou"
4) "shanghai"
5) "tianjing"
6) "beijing"

举例说明

geoadd

127.0.0.1:6379[13]> GEOADD china:city 116.41667 39.91667 beijing
(integer) 1
127.0.0.1:6379[13]> GEOADD china:city 121.43333 34.50000 shanghai
(integer) 1
127.0.0.1:6379[13]> GEOADD china:city 117.20000 39.13333 tianjing
(integer) 1
127.0.0.1:6379[13]> GEOADD china:city 118.78333 32.05000 jiangsu
127.0.0.1:6379[13]> ZRANGE china:city 0 -1
1) "jiangsu"
2) "shanghai"
3) "tianjing"
4) "beijing"

geopos

127.0.0.1:6379[13]> GEOPOS china:city jiangsu # 获取jiangsu的经纬度位置
1) 1) "118.78332942724227905"
   2) "32.04999907785209956"
   
# 可写多个
127.0.0.1:6379[13]> GEOPOS china:city jiangsu shanghai
1) 1) "118.78332942724227905"
   2) "32.04999907785209956"
2) 1) "121.4333304762840271"
   2) "34.49999971716130887"

geodist

# 张家界市到西安市的直线距离
127.0.0.1:6379> GEODIST china:city zhangjiajie xian
"589959.2719"

georadius

# 再加上几个城市
127.0.0.1:6379[13]> geoadd china:city 114.06667 22.61667 shenzheng
(integer) 1
127.0.0.1:6379[13]> geoadd china:city 120.20000 30.26667 hangzhou
(integer) 1
127.0.0.1:6379[13]> geoadd china:city 106.45000 29.56667 chongqin
(integer) 1
127.0.0.1:6379[13]> GEORADIUS china:city 110 30 1000 km
1) "chongqin"
2) "shenzheng"
3) "hangzhou"
4) "jiangsu"
127.0.0.1:6379[13]> GEORADIUS china:city 110 30 500 km
1) "chongqin"
# withdist 显示到中间距离的位置  withcoord 显示他人的定位信息(坐标)
127.0.0.1:6379[13]> GEORADIUS china:city 110 30 1000 km withcoord withdist
1) 1) "chongqin"
   2) "346.0548"
   3) 1) "106.4500012993812561"
      2) "29.56666939001875249"
2) 1) "shenzheng"
   2) "915.6424"
   3) 1) "114.06667023897171021"
      2) "22.61666928352524764"
3) 1) "hangzhou"
   2) "981.3098"
   3) 1) "120.20000249147415161"
      2) "30.2666706589875858"
4) 1) "jiangsu"
   2) "867.3741"
   3) 1) "118.78332942724227905"
      2) "32.04999907785209956"
# 带上count 1 就是在筛选之上 选择一个展示 2就是前两个...
127.0.0.1:6379[13]> GEORADIUS china:city 110 30 1000 km withcoord withdist count 1
1) 1) "chongqin"
   2) "346.0548"
   3) 1) "106.4500012993812561"
      2) "29.56666939001875249"

hyperloglog

适用场景: 网站UV量。传统用set统计,但若存在大量用户id,则太消耗内容且麻烦,若只为计数且允许有错误率(0.81%),则可行,否则还是用set统计

基数:集合中不重复元素个数。如{1, 3, 5, 5 ,7}则为{1,3,5,7},基数为4

【狂神说】 Redis 笔记分享_第1张图片

命令 描述
PFADD key element1 [elememt2…] 添加指定元素到 HyperLogLog 中
PFCOUNT key [key] 返回给定 HyperLogLog 的基数估算值。
PFMERGE destkey sourcekey [sourcekey…] 将多个 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
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2					#2个集合取并集
OK
127.0.0.1:6379> PFCOUNT mykey3
(integer) 15
127.0.0.1:6379>

Bitmaps

Bitmaps通常存储两个状态的数据,例如:登录或未登录、打开或未打卡…

bitmaps的具体实现类是String类型

常见指令

命令 描述
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--getbit--------------
127.0.0.1:6379> setbit sign 0 1 # 设置sign的第0位为 1 
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 设置sign的第2位为 1  不设置默认 是0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> type sign
string
127.0.0.1:6379> getbit sign 2 # 获取第2位的数值
(integer) 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 4 # 未设置默认是0
(integer) 0
-----------bitcount----------------------------
# 统计这周的打卡记录,就可以看到是否有全勤
127.0.0.1:6379> BITCOUNT sign # 统计sign中为1的位数
(integer) 4
127.0.0.1:6379[13]> get sign
"\xb4"

redis基础

事务

在SQL中事务通常有四大特性:原子性(A)、一致性©、隔离性(I)、持久性(D)

Redis单条命令保存原子性,但是Redis事务不保障原子性!

Redis事务没有隔离级别,也就是没有隔离性!

Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
一次性
顺序性
排他性
1 Redis事务没有隔离级别的概念
2 Redis单条命令是保证原子性的,但是事务不保证原子性!

Redis事务操作过程

  • 开启事务(multi)
  • 命令入队
  • 执行事务(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 k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事务执行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
   2) "k2"
   3) "k1"

放弃事务(discard)

# 这是取消事务 手动取消的
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> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC 
(error) ERR EXEC without MULTI # 当前未开启事务
127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行
(nil)

Redis事务异常

Redis事务存在两种异常:运行时异常、编译时异常

编译时异常

编译时异常出错,所有的命令都不会执行!

# 这是编译时的错误 语法错误 所有命令不生效
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> error k1 # 这是一条模拟语法错误命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队 
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors. # 执行报错
127.0.0.1:6379> get k1 
(nil) # 其他命令并没有被执行

运行时异常

运行时异常出错,错误的命令不会执行,其余命令均可以正常执行!Redis事务不保证原子性

# 运行时报错 正常的命令是执行完了 所以不保证原子性
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> INCR k1 # 这条命令逻辑错误(对字符串进行增量)
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 运行时报错
4) "v2" # 其他命令正常执行

# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。

Redis事务虽然说是事务,但是其更像是一种批处理。

乐观锁

乐观锁的普通实现方式是定义一个version字段,当某一个线程修改某个参数时,让version的字段自动改变。请求到达时首先会查询当前version,当修改数据时再一次查询version,如果version前后不一致说明有其他线程参与了修改,修改失败。

Redis中乐观锁可以使用关键字watch监听version字段,每一次修改都会比对前后的version实现乐观锁。

watch key # 监听key的version字段
unwatch key # 解除key键的监听

举例说明

正常执行成功

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 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 out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

模拟多线程修改值,使用watch实现乐观锁操作!

# 第一个客户端 先别执行exec 因为此时watch后的值还是原来的值
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
127.0.0.1:6379> watch money # 监视 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
# 当第二个客户端执行完后 我再执行exec 就报错了
127.0.0.1:6379> exec #执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败!
(nil)

#第二个客户端
127.0.0.1:6379> set money 80
OK

无论是运行成功还是修改失败或者调用discard退出事务,该线程会自动放弃锁。

判断为失败的情况

# 获取最新的值,再次监视,select version
127.0.0.1:6379[13]> watch money
OK
127.0.0.1:6379[13]> MULTI
OK
127.0.0.1:6379[13]> DECRBY money 20
QUEUED
127.0.0.1:6379[13]> INCRby out 20
QUEUED
# 比对监视的值是否发生了变化,如果没有,执行成功,如果变量的版本变化了,执行失败
127.0.0.1:6379[13]> exec
1) (integer) 60
2) (integer) 20

Jedis

Jedis 是 Redis 官方推荐的 java连接开发工具! 使用Java 操作Redis 中间件!

导入对应的依赖


<dependencies>
    
    <dependency>
        <groupId>redis.clientsgroupId>
        <artifactId>jedisartifactId>
        <version>3.2.0version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.62version>
    dependency>
dependencies>

编码测试:

  • 连接数据库
  • 操作命令
  • 断开连接!
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
        // 1、 new Jedis 对象即可
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要!
        System.out.println(jedis.ping());
    }
}

常用的API

  • String
  • List
  • Set
  • Hash
  • Zset

所有的api命令,就是我们对应的上面学习的指令,一个都没有变化!
这里没有展示

使用事务

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("39.99.xxx.xx", 6379);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "kuangshen");
        // 开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();
        // jedis.watch(user1)
        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整合Redis

备注:从SpringBoot2.x之后,原先使用的Jedis被lettuce替代

Jedis:采用直连,模拟多个线程操作会出现安全问题。为避免此问题,需要使用Jedis Pool连接池!类似于BIO模式

lettuce:采用netty网络框架,对象可以在多个线程中被共享,完美避免线程安全问题,减少线程数据,类似于NIO模式

首先先查看RedisAutoConfiguration中的源码

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
// 绑定的配置类 RedisProperties.class
@EnableConfigurationProperties(RedisProperties.class)
// 配置两个类
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
  @Bean
  // 表示用户能够自定义RedisTemplate
  @ConditionalOnMissingBean(name = "redisTemplate")
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
        throws UnknownHostException {
     // 默认的RedisTemplate直接使用此类内部默认设置操作数据,但是Redis对象需要序列化
     // 泛型都是Object,后面使用的话,大都是RedisTemplate
     RedisTemplate<Object, Object> template = new RedisTemplate<>();
     template.setConnectionFactory(redisConnectionFactory);
     return template;
  }

  @Bean
  @ConditionalOnMissingBean
  // String类型在redis使用最多,StringRedisTemplate处理String数据
  public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
        throws UnknownHostException {
     StringRedisTemplate template = new StringRedisTemplate();
     template.setConnectionFactory(redisConnectionFactory);
     return template;
  }
}

上面的@Import注解导入了两个配置类,有Lettuce和Jedis,可以点开这两个类查看

在这里插入图片描述
在这里插入图片描述

对比一下可以发现,Jedis配置类中有两个类是默认不存在的,不存在就无法使用

1、导入依赖

<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2、属性配置

# 配置Redis
spring.redis.host=127.0.0.1
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中的Redis获取结果得到的却是乱码

127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x02k1"

这与RedisTemplate默认序列化有关,Java的默认序列化规则Redis无法识别,因此输入的数据无法被Redis反序列化输出的就是乱码。

先展示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
// 这是RedisTemplate的一个模板
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
  throws UnknownHostException {
  // 为了开发方便,可以直接使用
  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);
    // String序列化
  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进阶

redis.conf解析

Redis的启动必须要用到配置文件!

Units 单位

容量单位不区分大小写,同时注意:k!=kb

【狂神说】 Redis 笔记分享_第2张图片

includes 包含

可以使用 include 包含多个配置文件

image-20220123203722750

network 网络

网络,表示Redis启动时开放的端口默认与本机绑定

bind 127.0.0.1

Redis指定监听端口,默认为6379

port 6379

表示服务器闲置多长时间(秒)后被关闭,如果这个这个数值为0,表示这个功能不起作用

timeout 300

是否开启保护模式,Redis默认开启,如果没有设置bind的IP地址和Redis密码,那么服务就会默认只能在本机运行

protected-mode yes

General 通用

是否以守护进程的方式运行,即后台运行,一般默认为no,需要手动改为yes否则无法后台运行

daemonize yes

如果以守护进程的方式运行,就需要指定一个pid文件,在Redis启动时创建,退出时删除

pidfile /var/run/redis_6379.pid # pid文件的保存位置

配置日志等级,日志等级的可选项如下:

  • debug:打印的信息较多,在工作中主要用于开发和测试
  • verbose:打印的信息仅次于debug,但是格式较为工整
  • notice:Redis默认配置,在生产环境中使用
  • warning:只打印一些重要信息,比如警告和错误
loglevel notice

打印的日志文件名称,如果为空,表示标准输出,在配置守护进程的模式下会将输出信息保存到/dev/null

logfile ""

数据库支持数量,16个

databases 16

SNAPSHOTTING 快照(RDB配置)

中文翻译为快照,如果在规定的时间内,数据发生了几次更新,那么就会将数据同步备份到一个文件中

Redis的持久化有两种方式,一种是RDB,一种是AOF。SNAPSHOTTING主要针对的是Redis持久化中的RDB

Redis是一个内存数据库,如果不采用持久化对数据进行保存,那么就会出现断电即失的尴尬场面

# 在900秒内,至少有一个key被修改(添加),就会进行持久化操作
save 900 1
# 在300秒内,至少有10个key被修改,就会进行持久化操作
save 300 10
# 在60秒内,至少有1万个key被修改,就会进行持久化操作
save 60 10000

top-writes-on-bgsave-error yes # 如果Redis在进行持久化的时候出现错误,是否停止写入,默认为是

rdbcompression yes # 是否在进行数据备份时压缩持久化文件,默认为是,这个操作会耗费CPU资源,可以设置为no

rdbchecksum yes # 在保存持久化文件的同时,对文件内容进行数据校验

dir ./ # 持久化文件保存的目录,默认保存在当前目录下

Security 安全

【狂神说】 Redis 笔记分享_第3张图片

箭头处按照:requirepass 密码的格式设置登录密码,使用指令也同样可以实现,不过需要保存指令保存配置,指令格式如下:

127.0.0.1:6379> CONFIG GET requirepass   # 查看密码设置
1) "requirepass"
2) ""
127.0.0.1:6379> CONFIG SET requirepass "123456"  # 设置密码为123456
OK
127.0.0.1:6379> set name "cjx"
OK
127.0.0.1:6379> get name "cjx"  # 没有访问权限
Invalid argument(s)
127.0.0.1:6379> AUTH "123456"  # 登录
OK
127.0.0.1:6379> get name
"cjx22"

Client 客户端

maxclients 10000  # 最大客户端数量
maxmemory <bytes>  # 最大内存限制
maxmemory-policy noeviction # 内存达到限制值的处理策略

redis 中的默认的过期策略是 volatile-lru 。

设置方式

config set maxmemory-policy volatile-lru 

maxmemory-policy 策略

# volatile-lru -> Evict(驱逐) using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.

**1、volatile-lru:**只对设置了过期时间的key进行LRU(默认值)

2、allkeys-lru : 删除lru算法的key

**3、volatile-random:**随机删除即将过期key

**4、allkeys-random:**随机删除

5、volatile-ttl : 删除最近即将过期的

6、noeviction : 永不过期,返回错误

AOF配置 在这里插入图片描述

在这里插入图片描述

Redis持久化

面试和工作,持久化都是重点!
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能!

RDB(Redis DataBase)

什么是RDB

在这里插入图片描述

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件**替换**上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失我们默认的就是RDB,一般情况下不需要修改这个配置!

有时候在生产环境我们会将这个文件进行备份!

rdb保存的文件是dump.rdb 都是在我们的配置文件中快照中进行配置的!

触发机制

  • 1、save规则满足的情况下,会自动触发rdb规则
  • 2、执行 flushall 命令,也会触发我们的rdb规则!
  • 3、退出redis,也会产生 rdb 文件!

备份就自动生成一个 dump.rdb

【狂神说】 Redis 笔记分享_第4张图片

如果恢复rdb文件!

1、只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!

2、查看需要存在的位置

127.0.0.1:6379> config get dir
1) "dir"
2) "./" # 如果在这个目录下存在 dump.rdb文件,启动就会自动恢复其中的数据

几乎就他自己默认的配置就够用了,但是我们还是需要去学习!

优点:

  • 1、适合大规模的数据恢复!
  • 2、对数据的完整性要求不高!

缺点:

  • 1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了!
  • 2、fork进程的时候,会占用一定的内容空间!!

AOF(Append Only File)


将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!

什么是AOF?

在这里插入图片描述

以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

Aof保存的是 appendonly.aof 文件

什么是AOF

快照功能(RDB)并不是非常耐久(durable): 如果 Redis因为某些原因而造成故障停机,那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。

如果要使用AOF,需要修改配置文件
在这里插入图片描述

appendonly no yes则表示启用AOF

默认是不开启的,我们需要手动配置,然后重启redis,就可以生效了!

如果这个aof文件有错误,这时候redis是启动不起来的,我需要修改这个aof文件

redis给我们提供了一个工具redis-check-aof修复,修复代码为:redis-check-aof --fix appendonly.aof,这样的修复可能会造成某些数据的丢失。

重写规则说明

aof 默认就是文件的无限追加,文件会越来越大

在这里插入图片描述

如果 aof 文件大于 64m,太大了! fork一个新的进程来将我们的文件进行重写!

AOF 文件重写的实现

  • AOF重写并不需要对原有AOF文件进行任何的读取,写入,分析等操作,这个功能是通过读取服务器当前的数据库状态来实现的
 # 假设服务器对键list执行了以下命令s;
127.0.0.1:6379> RPUSH list "A" "B"
(integer) 2
127.0.0.1:6379> RPUSH list "C"
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E"
(integer) 5
127.0.0.1:6379> LPOP list
"A"
127.0.0.1:6379> LPOP list
"B"
127.0.0.1:6379> RPUSH list "F" "G"
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "C"
2) "D"
3) "E"
4) "F"
5) "G"
  • 当前列表键list在数据库中的值就为["C", "D", "E", "F", "G"]。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "C" "D" "E" "F" "G"代替前面的6条命令。

优点:

  • 1、每一次修改都同步,文件的完整会更加好!
  • 2、每秒同步一次,可能会丢失一秒的数据
  • 3、从不同步,效率最高的!

缺点:

  • 1、相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
  • 2、Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化

RDB和AOF选择

有点 RDB AOF
启动优先级
体积
恢复速度
数据安全性 丢数据 根据策略决定

如何选择使用哪种持久化方式?

如果仅仅是使用Redis当做缓存,那么甚至可以不用持久化!但是一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。

如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!
Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图:
第一个:消息发送者, 第二个:频道 第三个:消息订阅者!
在这里插入图片描述

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

在这里插入图片描述

当有新消息通过 PUBLISH 命令发送给频道 channel1 时,这个消息就会被发送给订阅它的三个客户端:

在这里插入图片描述

命令

命令 描述
PSUBSCRIBE pattern [pattern…] 订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE pattern [pattern…] 退订一个或多个符合给定模式的频道。
PUBSUB subcommand [argument[argument]] 查看订阅与发布系统状态。
PUBLISH channel message 向指定频道发布消息
SUBSCRIBE channel [channel…] 订阅给定的一个或多个频道。
UNSUBSCRIBE channel [channel…] 退订一个或多个频道
------------订阅端----------------------
127.0.0.1:6379> SUBSCRIBE sakura # 订阅sakura频道
Reading messages... (press Ctrl-C to quit) # 等待接收消息
1) "subscribe" # 订阅成功的消息
2) "sakura"
3) (integer) 1
1) "message" # 接收到来自sakura频道的消息 "hello world"
2) "sakura"
3) "hello world"
1) "message" # 接收到来自sakura频道的消息 "hello i am sakura"
2) "sakura"
3) "hello i am sakura"

--------------消息发布端-------------------
127.0.0.1:6379> PUBLISH sakura "hello world" # 发布消息到sakura频道
(integer) 1
127.0.0.1:6379> PUBLISH sakura "hello i am sakura" # 发布消息
(integer) 1

-----------------查看活跃的频道------------
127.0.0.1:6379> PUBSUB channels
1) "sakura"

原理

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典,这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

在这里插入图片描述

客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。

缺点

  • 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
  • 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。

应用

  • 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
  • 多人在线聊天室。

这边消息队列的功能相比MQ之类的就差很多了,所以稍微复杂的场景,我们就会使用消息中间件MQ处理。

主从复制与集群搭建

基础概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower)数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)

默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。通常情况下主机用于写数据,从机用于读取数据,因为真实情况下读取的请求数量远远高于写入的请求数量

集群作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
  • 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
  • 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在读多写少的场景下,通过多个从节点分担负载,提高并发量。
  • 高可用基石:主从复制还是哨兵和集群能够实施的基础。

为什么使用集群

  • 单台服务器难以负载大量的请求
  • 单台服务器故障率高,系统崩坏概率大
  • 单台服务器内存容量有限。

环境配置

查看当前库的信息: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

既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息:

  • 端口号:port
  • pid文件名:pidfile
  • 日志文件名:logfile
  • rdb文件名:dbfilename

启动单机多服务集群:

ps -ef | grep redis 查看当前启动的服务进程,可以看到集群搭建完毕:

【狂神说】 Redis 笔记分享_第5张图片

一主二从配置

默认情况下,每台Redis服务器都是主节点,因此我们一般情况下只用配置从机就好了!

认老大!一主(79)二从(80,81)

使用SLAVEOF host port命令就可以为从机配置主机了。

在这里插入图片描述

在这里插入图片描述

然后主机上也能看到从机的状态:

在这里插入图片描述

我们这里是使用命令搭建,是暂时的,真实开发中应该在从机的配置文件中进行配置,这样的话是永久的

在这里插入图片描述

使用规则⭐️

  1. 从机只能读,不能写,主机可读可写但是多用于写。主机写入的数据在所有从机中均能够读取!
##################################从机操作#####################################
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"
  1. 当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状

  2. 当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前作为从机时的数据的,若此时重新配置为从机,又可以获取到主机的所有数据。这里就要提到一个复制原理。

  3. 第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:

    1. 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机
    2. 使用哨兵模式(自动选举)

如果主机断开了连接,我们可以使用SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么就能重新连接!

复制原理

Slave启动成功连接到 master后会发送一个sync同步命令

Master接收到该命令后,启动后台的存盘进程,同时手机所有接收到的用于修改数据集命令,在后台进程执行完毕后,master将传送整个数据文件到Slave中,并完一次完全同步

全量复制:Slave服务在接收到数据库文件后,将其存盘并加载到内存中,常出现在与主机成功连接时。

增量复制:Master继续将所有收集到的修改命令依次传送给Slave,完成同步,常出现在主机执行修改命令后。

只要是重写连接master,全量复制就会自动执行!

哨兵模式

概念

在主从模式中,主机出现宕机的情况会导致集群无法执行写操作,过去需要手动更改从机的配置使其成为新的主机,这样不仅麻烦且容易出错,通常为了自动监控主机运行情况与主机宕机时实现主从切换,会配置哨兵进程监视主机,成为哨兵模式。

在这里插入图片描述

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了**多哨兵模式**。

在这里插入图片描述

如果某一个哨兵监测到主机宕机,此时该哨兵不会立即执行主从切换动作,此时仅仅是哨兵1主观认为主机无法使用,这个现象成为主观下线。如果多个哨兵均监测到主机无法使用时,哨兵之间会进行一次投票,投票会根据一定的算法决定出让哪一个从机成为新的主机,并进行failover(故障转移)操作,并且每个哨兵会自动修改其监视服务器为新主机,此过程成为客观下线

实现哨兵模式

每一个哨兵其实是一个redis服务进程,启动时也需要给定其配置文件,哨兵模式的配置文件是sentinel.conf

1、哨兵模式配置文件

# sentinel monitor 被监控的名称(任意) host port 1
sentinel monitor myredis 127.0.0.1 6379 1

后面的这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!

2、启动哨兵

redis-sentinel xxx/sentinel.conf # 成功启动哨兵模式

reids-sentinel同样是一个可执行服务

【狂神说】 Redis 笔记分享_第6张图片

可以看到连接成功!

在这里插入图片描述

此时哨兵监视着我们的主机6379,当我们断开主机后:

在这里插入图片描述

哨兵模式优缺点

优点:

  1. 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
  2. 主从可以切换,故障可以转移,系统的可用性更好
  3. 哨兵模式是主从模式的升级,手动到自动,更加健壮

缺点:

  1. Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
  2. 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项

哨兵模式的全部配置

完整的哨兵模式配置文件 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    
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass  
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
 
 
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds  
sentinel down-after-milliseconds mymaster 30000
 
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
# 这个数字越小,完成failover所需的时间就越长,
# 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
# 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs  
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  
sentinel failover-timeout mymaster 180000
 
# SCRIPTS EXECUTION
 
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
 
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script  
  sentinel notification-script mymaster /var/redis/notify.sh
 
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#       
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。 
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script  
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

缓存穿透与雪崩

服务的高可用问题

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题(事务在运行时不能保证原子性),从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

在这里插入图片描述

缓存穿透

概念

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。洪水攻击。数据库也查不到就没有缓存,就会一直与数据库访问。

解决方案

1.布隆过滤器

对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。

在这里插入图片描述

2.缓存空对象

一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。

在这里插入图片描述

1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响

缓存击穿(量太大 缓存过期)

概述

相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。

比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。

解决方案

1.设置热点数据永不过期

这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。

2.加互斥锁(分布式锁)

在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

缓存雪崩

缓存概念

大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

在这里插入图片描述

解决方案

redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群

限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

你可能感兴趣的:(redis,nosql,数据库)