redis

概述

什么是Redis

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

Redis能干什么

​ 1、会话缓存(最常用)

​ 2、消息队列(支付)

​ 3、活动排行榜或计数

​ 4、发布,订阅消息(消息通知)

​ 5、商品列表,评论列表

​ …

Redis的特点

  • Redis以内存作为数据存储介质,读写数据的效率极高。

  • Redis跟memcache不同的是,储存在Redis中的数据是持久化的,断电或重启,数据也不会丢失。

  • Redis的存储分为内存存储、磁盘存储和log文件。

  • Redis可以从磁盘重新将数据加载到内存中,也可以通过配置文件对其进行配置,因此,redis才能实现持久化。Redis持久有两种方式:快照(RDB)仅附加文件(AOF)

  • Redis支持主从模式,可以配置集群,更利于支撑大型的项目。

Windows安装

官网下载

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-paAOZBUz-1636007528413)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018190752406.png)]

解压

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-drlsQfjE-1636007645830)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018190752406.png)]

开启服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iQTSmrif-1636007528416)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018191050451.png)]

首先开启服务端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bp3SOQX8-1636007528417)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018190916367.png)]

然后再开启客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eicIPRoR-1636007528418)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018191217828.png)]

简单使用

# 测试连接
ping
# 存值
set key value
# 取值
get key

Linux安装

通常在linux上使用redis

下载安装包解压

$ wget https://download.redis.io/releases/redis-6.2.6.tar.gz
$ tar xzf redis-6.2.6.tar.gz

安装

$ yum install gcc-c++
$ cd redis-6.2.6
$ make
$ make install

进入/usr/local/bin目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y82VInBk-1636007528420)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018195237516.png)]

可以看见已经安装完成了

复制配置文件

在当前/usr/local/bin目录下创建一个文件夹myconfig

之后我们使用拷贝的配置文件即可,如果出错了就去重新拷贝一份

# 创建文件夹
$ mkdir myconfig
# 从解压目录下拷贝过来
$ cp /home/redis-6.2.6/redis.conf myconfig/

修改配置文件

$ vim redis.conf

修改daemonize的值从no改为为yes

使得redis服务能够后台运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8az2sDyJ-1636007528420)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018200139321.png)]

启动服务

  1. 开启服务端
$ redis-server myconfig/redis.conf
  1. 开启客户端
$ redis-cli -p 6379
  1. 测试
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set name wcy
OK
127.0.0.1:6379> get name
"wcy"
127.0.0.1:6379> keys *
1) "name"

查看和结束redis服务进程

  1. 查看
$ ps -ef|grep redis
  1. 结束
127.0.0.1:6379> shutdown
not connected> exit

结束后服务端和客户端都会关闭

Redis性能测试

语法

redis 性能测试的基本命令如下:

$ redis-benchmark [option] [option value]

注意:该命令是在 redis 的目录下执行的,而不是 redis 客户端的内部指令。

参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DRSMvI5H-1636007528421)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018204129236.png)]

测试

模拟100000万个请求,和100个并发

$ redis-benchmark -h localhost -p 6379 -c 100 -n 100000

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QZoyVK2V-1636007528422)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018204905415.png)]

基础知识

数据库数量

redis默认有16个数据库,默认使用的是第0个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AikrziNN-1636007528423)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018205425493.png)]

切换数据库

切换到第三个数据库

$ select 3

每个数据库中的数据不共享

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AQzLKmrN-1636007528424)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211018205814819.png)]

基础命令

# 查看数据库大小
dbsize

# 查看所有的键
keys *

# 清空当前数据库
flushdb

# 清空所有数据库
flushall

Redis默认端口:6379

Redis是单线程的还如此快?

  • Redis是使用C语言来编写的
  • Redis是使用内存来操作的,将所有数据全部放在内存中,所以用单线程是最快的,CPU不是Redis的瓶颈,内存大小和网络带宽才是
  • 多线程不是一定比单线程快,它会有CPU上下文的切换

Redis-Key

查看所有key

keys *

判断key是否存在

exists key

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sqWh86Y-1636007528424)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211019090714022.png)]

存在返回1,不存在返回0

移除指定的key

move key 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zITXPa2i-1636007528425)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211019091350984.png)]

设置key的过期时间和查看剩余时间

# 设置过期时间 单位s
expire key seconds

# 查看key的剩余时间
ttl key

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KiarNUqe-1636007528426)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211019091051368.png)]

查看key的类型

type key

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WjLzjDdN-1636007528426)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211019091557198.png)]

可以在这里查看Redis的命令

菜鸟教程

官方文档

五大数据类型

String

基本命令

# 设置值
127.0.0.1:6379> set name hello
OK

# 获取值
127.0.0.1:6379> get name
"hello"

# 获取所有键
127.0.0.1:6379> keys *
1) "name"

# 判断键是否存在
127.0.0.1:6379> exists name
(integer) 1

# 往后面追加值 返回总长度
127.0.0.1:6379> append name ",world"
(integer) 11
127.0.0.1:6379> get name
"hello,world"

# 获取键对应值的长度
127.0.0.1:6379> strlen name
(integer) 11

# 当key不存在使用append时,相当于创建一个key
127.0.0.1:6379> append age 18
(integer) 2
127.0.0.1:6379> get age
"18"

自增

必须是可以转为数字类型的字符串

127.0.0.1:6379> set num 0
OK
# 自增1 num++
127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> get num
"1"
# 自减1 num--
127.0.0.1:6379> decr num
(integer) 0
127.0.0.1:6379> get num
"0"
# 自增10  num += 10
127.0.0.1:6379> incrby num 10
(integer) 10
127.0.0.1:6379> get num
"10"
# 自减10  num -= 10
127.0.0.1:6379> decrby num 10
(integer) 0

一般可以用于文章的浏览量

字符串操作

  • 切片操作
127.0.0.1:6379> set name hello,world
OK
127.0.0.1:6379> get name
"hello,world"
# 获取索引为0-4 [01234]
127.0.0.1:6379> getrange name 0 4
"hello"
# 获取所有,-1代表最后
127.0.0.1:6379> getrange name 0 -1
"hello,world"
  • 替换操作
 127.0.0.1:6379> get name
"hello,world"
# 从索引6的位置开始替换
127.0.0.1:6379> setrange name 6 wangcy
(integer) 12
127.0.0.1:6379> get name
"hello,wangcy"
127.0.0.1:6379> 

过期时间

# setex(set with expire)
# 设置name为hello,world过期时间为10s
127.0.0.1:6379> setex name 10 hello,world
OK
127.0.0.1:6379> get name
"hello,world"
# 查看剩余时间
127.0.0.1:6379> ttl name
(integer) 0
127.0.0.1:6379> ttl name
(integer) -2

不存在时设置

# setnx(set if no exist)
# 当age不存在时 设置为18
127.0.0.1:6379> setnx age 18
(integer) 1
127.0.0.1:6379> get age
"18"
# 此时age已经存在,故设置失败
127.0.0.1:6379> setnx age 20
(integer) 0
127.0.0.1:6379> get age
"18"

批量设置和获取值

# 批量设置
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"

# 批量获取
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"

# 批量不存在时设置
# 其中有一个存在的话,全部都会设置失败
# 是一个原子性的操作,只会全部成功或全部失败  
127.0.0.1:6379> msetnx k1 v11 k4 v4
(integer) 0
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k4
(nil)

存储对象

实际中存储键的名称为user:{id}:{filed}的形式

# 以json字符串的形式存储
127.0.0.1:6379> set user:1 {name:wcy,age:18}
OK
127.0.0.1:6379> get user:1
"{name:wcy,age:18}"

# 以键的方式存储
127.0.0.1:6379> set user:1:name wcy
OK
127.0.0.1:6379> set user:1:age 18
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "wcy"
2) "18"

先取后存

# 先区后存,不存在就返回nil
127.0.0.1:6379> getset name wcy
(nil)
# 存在就取出原来的值,载设置新的值
127.0.0.1:6379> getset name lqy
"wcy"
127.0.0.1:6379> get name
"lqy"

String的应用场景

  • 计数器

  • 统计多单位的数量

  • 粉丝数量

  • 对象的缓存存储

List

在redis中list可以当做栈,队列,阻塞队列

redis中list命令都是以l开头的

添加元素

# 创建一个nums列表 向左端(头部)添加一个元素
127.0.0.1:6379> lpush nums 1
(integer) 1
# 向右端(尾部)添加一个数据
127.0.0.1:6379> rpush nums 2
(integer) 2
# 向左端添加
127.0.0.1:6379> lpush nums 0
(integer) 3
# 向右端添加
127.0.0.1:6379> rpush nums 3
(integer) 4
# 查看所有数据 
127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"

# 一次添加多个元素
127.0.0.1:6379> lrange nums 0 -1
1) "3"
127.0.0.1:6379> lpush nums 2 1 0
(integer) 4
127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"

移除元素

  • 移除头部
127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"

# 移除头部的一个元素
127.0.0.1:6379> lpop nums
"0"
127.0.0.1:6379> lrange nums 0 -1
1) "1"
2) "2"
3) "3"

# 移除头部的两个元素
127.0.0.1:6379> lpop nums 2
1) "1"
2) "2"

127.0.0.1:6379> lrange nums 0 -1
1) "3"

  • 移除尾部
127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"

# 移除尾部的一个元素
127.0.0.1:6379> rpop nums
"3"
127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"

# 移除尾部的两个元素
127.0.0.1:6379> rpop nums 2
1) "2"
2) "1"
127.0.0.1:6379> lrange nums 0 -1
1) "0"
  • 移除指定元素

lrem key count element

127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "3"
6) "3"
7) "3"

# 移除掉列表中一个值为0的元素
127.0.0.1:6379> lrem nums 1 0
(integer) 1
127.0.0.1:6379> lrange nums 0 -1
1) "1"
2) "2"
3) "3"
4) "3"
5) "3"
6) "3"

# 移除掉列表中一个值为1的元素
127.0.0.1:6379> lrem nums 1 1
(integer) 1
127.0.0.1:6379> lrange nums 0 -1
1) "2"
2) "3"
3) "3"
4) "3"
5) "3"

# 移除掉列表中2个值为3的元素
127.0.0.1:6379> lrem nums 2 3
(integer) 2
127.0.0.1:6379> lrange nums 0 -1
1) "2"
2) "3"
3) "3"

索引取值

127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"

# 获取索引为1的值
127.0.0.1:6379> lindex nums 1
"1"
# 获取索引为3的值
127.0.0.1:6379> lindex nums 3
"3"
# 获取列表中最后一个值
127.0.0.1:6379> lindex nums -1
"3"

列表长度

127.0.0.1:6379> lrange nums 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
# 获取列表长度
127.0.0.1:6379> llen nums
(integer) 4

列表截取

列表截取会改变list的值

127.0.0.1:6379> lrange nums 0 -1
1) "2"
2) "3"
3) "3"

# 截取第0个到第1个为新的列表值
127.0.0.1:6379> ltrim nums 0 1
OK
127.0.0.1:6379> lrange nums 0 -1
1) "2"
2) "3"

移除列表最后一个元素并移动到新的列表中

rpoplpush source destination

127.0.0.1:6379> lrange nums 0 -1
1) "2"
2) "3"

# 移除nums最后一个元素并移动到新的列表newnums中
127.0.0.1:6379> rpoplpush nums newnums
"3"

127.0.0.1:6379> lrange nums 0 -1
1) "2"
127.0.0.1:6379> lrange newnums 0 -1
1) "3"

替换指定索引元素的值

127.0.0.1:6379> exists mylist
(integer) 0

# 列表不存在时 无法替换
127.0.0.1:6379> lset mylist 0 wcy
(error) ERR no such key
127.0.0.1:6379> lpush mylist hello
(integer) 1
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"

# 将索引为0的元素值替换为wcy
127.0.0.1:6379> lset mylist 0 wcy
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "wcy"

# 当索引不存在时,也会替换失败
127.0.0.1:6379> lset mylist 1 hello
(error) ERR index out of range

元素插入

linsert key before|after pivot element

在指定列表中的元素pivot前面或者后面插入element

127.0.0.1:6379> lrange mylist 0 -1
1) "wp"
2) "lqy"
3) "wcy"

# 在元素wcy的后面插入hhh
127.0.0.1:6379> linsert mylist after wcy hhh
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "wp"
2) "lqy"
3) "wcy"
4) "hhh"
# 在元素wcy的前面插入www
127.0.0.1:6379> linsert mylist before wcy www
(integer) 5
127.0.0.1:6379> lrange mylist 0 -1
1) "wp"
2) "lqy"
3) "www"
4) "wcy"
5) "hhh"
127.0.0.1:6379> 

总结

redis的list底层是一个链表

  • 在头部或者尾部插入元素的效率是最高的
  • 在中间插入效率会比较低

使用list可以做消息队列

  • 左边插入lpush
  • 右边取出rpop

也可以是栈

  • 左边插入
  • 左边取出

Set

set(集合)中的元素是不能够重复的

set是无序的

添加元素

# 向sadd中添加元素
127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset hello1 hello2
(integer) 2

# 查看set中的所有元素
127.0.0.1:6379> smembers myset
1) "hello1"
2) "hello2"
3) "hello"

# 元素存在时,添加不会成功
127.0.0.1:6379> sadd myset hello
(integer) 0

移除元素

  • 移除指定元素
127.0.0.1:6379> smembers myset
1) "hello1"
2) "hello2"
3) "hello"
# 移除hello元素
127.0.0.1:6379> srem myset hello
(integer) 1
127.0.0.1:6379> smembers myset
1) "hello1"
2) "hello2"
127.0.0.1:6379> 
  • 随机移除元素

set是无序的,所以是随机移除

127.0.0.1:6379> smembers nums
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 随机移除一个
127.0.0.1:6379> spop nums 
"2"
# 移除两个
127.0.0.1:6379> spop nums 2
1) "1"
2) "4"
127.0.0.1:6379> smembers nums
1) "3"
2) "5"

成员判断


# 判断某个元素是否在set中
127.0.0.1:6379> sismember mysert hello
(integer) 0
127.0.0.1:6379> sismember myset hello
(integer) 1
127.0.0.1:6379> sismember myset hello4
(integer) 0

元素总数

# 获取set中的元素总数
127.0.0.1:6379> scard myset
(integer) 3

随机抽取

127.0.0.1:6379> smembers nums
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 随机抽取一个元素
127.0.0.1:6379> SRANDMEMBER nums
"4"
127.0.0.1:6379> SRANDMEMBER nums
"2"
# 两个
127.0.0.1:6379> SRANDMEMBER nums 2
1) "1"
2) "5"
# 三个
127.0.0.1:6379> SRANDMEMBER nums 3
1) "4"
2) "3"
3) "5"

将指定的元素移动到另一个set中

127.0.0.1:6379> smembers nums
1) "3"
2) "5"
# 将nums中的3移动到newnums中
127.0.0.1:6379> smove nums newnums 3
(integer) 1
127.0.0.1:6379> smembers nums
1) "5"
127.0.0.1:6379> smembers newnums
1) "3"

集合运算

127.0.0.1:6379> sadd set1 a b c d
(integer) 4
127.0.0.1:6379> sadd set2 c d e f
(integer) 4
# 差集
127.0.0.1:6379> SDIFF set1 set2
1) "b"
2) "a"
# 交集
127.0.0.1:6379> SINTER set1 set2
1) "d"
2) "c"
# 并集
127.0.0.1:6379> SUNION set1 set2
1) "a"
2) "d"
3) "b"
4) "c"
5) "f"
6) "e"

在微博中,共同关注的就可以使用交集来实现

Hash

map集合,key-map,key中存的map是一系列key-value

存取操作

# 在myhash中添加k-v
127.0.0.1:6379> hset myhash k1 v1
(integer) 1
# 获取myhash中k1值
127.0.0.1:6379> hget myhash k1
"v1"
# 批量添加k-v
127.0.0.1:6379> hmset myhash k1 v1 k2 v2 k3 v3
OK
# 批量获取
127.0.0.1:6379> hmget myhash k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
# 获取全部 结果以k-v的顺序展示
127.0.0.1:6379> hgetall myhash
1) "k1"
2) "v1"
3) "k2"
4) "v2"
5) "k3"
6) "v3"

删除指定的key

# 删除myhash中k1
127.0.0.1:6379> hdel myhash k1
(integer) 1
# 删除k2 k3
127.0.0.1:6379> hdel myhash k2 k3
(integer) 2
127.0.0.1:6379> hgetall myhash
(empty array)

获取全部k-v的数量

127.0.0.1:6379> hmset myhash k1 v1 k2 v2 k3 v3
OK
# 获取kv数量
127.0.0.1:6379> hlen myhash
(integer) 3

判断key是否存在

# k1存在
127.0.0.1:6379> HEXISTS myhash k1
(integer) 1
# k5不存在
127.0.0.1:6379> HEXISTS myhash k5
(integer) 0

获取hash中的所有key或者所有值

127.0.0.1:6379> HKEYS myhash
1) "k1"
2) "k2"
3) "k3"

127.0.0.1:6379> HVALS myhash
1) "v1"
2) "v2"
3) "v3"

自增自减

127.0.0.1:6379> hset myhash k4 1
(integer) 1
# 加2
127.0.0.1:6379> hincrby myhash k4 2
(integer) 3
# 减1
127.0.0.1:6379> hincrby myhash k4 -1
(integer) 2

当不存在时设置值

# 当k6不存在时设置值
127.0.0.1:6379> HSETNX myhash k6 v6
(integer) 1
# 存在时设置不成功
127.0.0.1:6379> HSETNX myhash k6 v7
(integer) 0
127.0.0.1:6379> HGET myhash k6
"v6"

Hash存储对象

127.0.0.1:6379> hmset user:1 name wcy pwd 123 age 18
OK
127.0.0.1:6379> hgetall user:1
1) "name"
2) "wcy"
3) "pwd"
4) "123"
5) "age"
6) "18"

使用hash来存储对象非常方便

Zset

Zset(有序集合,sorted sets)

  • 在set的基础上增加了排序功能

  • 增加了一个score值来表示元素的优先级

排序

# 添加元素 10是score
127.0.0.1:6379> zadd myzset 10 wcy
(integer) 1
127.0.0.1:6379> zadd myzset 20 lqy
(integer) 1
127.0.0.1:6379> zadd myzset 30 www
(integer) 1
127.0.0.1:6379> zadd myzset 40 qqq
(integer) 1

# 按照score排序 从负无穷到正无穷排序,从小到大
127.0.0.1:6379> ZRANGEBYSCORE myzset -inf +inf
1) "wcy"
2) "lqy"
3) "www"
4) "qqq"
# 从 0 - 100
127.0.0.1:6379> ZRANGEBYSCORE myzset 0 100
1) "wcy"
2) "lqy"
3) "www"
4) "qqq"

# 按照score排序 从负无穷到正无穷排序 并显示score
127.0.0.1:6379> ZRANGEBYSCORE myzset -inf +inf withscores
1) "wcy"
2) "10"
3) "lqy"
4) "20"
5) "www"
6) "30"
7) "qqq"
8) "40"

# 从大到小排序
127.0.0.1:6379> ZREVRANGE myzset 0 -1
1) "qqq"
2) "www"
3) "lqy"

127.0.0.1:6379> ZREVRANGE myzset 0 -1 withscores
1) "qqq"
2) "40"
3) "www"
4) "30"
5) "lqy"
6) "20"

移除指定元素

# 显示所有元素
127.0.0.1:6379> ZRANGE myzset 0 -1
1) "wcy"
2) "lqy"
3) "www"
4) "qqq"

# 移除wcy
127.0.0.1:6379> ZREM myzset wcy
(integer) 1
127.0.0.1:6379> ZRANGE myzset 0 -1
1) "lqy"
2) "www"
3) "qqq"

获取集合中的元素总数

127.0.0.1:6379> ZRANGE myzset 0 -1
1) "lqy"
2) "www"
3) "qqq"
# 获取元素总数
127.0.0.1:6379> ZCARD myzset
(integer) 3

计算在score区间中的元素个数

127.0.0.1:6379> zadd zset 1 wcy 2 lqy 3 qqq 4 www
(integer) 4
# 1-4之间
127.0.0.1:6379> zcount zset 1 4
(integer) 4
# 1-3之间
127.0.0.1:6379> zcount zset 1 3
(integer) 3

完整的命令可以查官方文档

三种特殊的数据类型

geospatial

Geo地理位置

  • 可以推算出地理位置的信息的信息
  • 两地之间的距离
  • 方圆几里内的人

可以用于朋友的定位,附近的人,打车距离的计算

Geo的命令很少

geoadd

语法:geoadd key [NX|XX] [CH] longitude latitude member [longitude latitude member]

将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。这些数据将会存储到sorted set这样的目的是为了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令对数据进行半径查询等操作。

该命令以采用标准格式的参数x,y,所以经度必须在纬度之前。这些坐标的限制是可以被编入索引的,区域面积可以很接近极点但是不能索引。具体的限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:

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

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

# 添加城市经纬度信息
127.0.0.1:6379> geoadd locations 116.23 40.22 beijing
(integer) 1
127.0.0.1:6379> geoadd locations 106.62 26.67 guiyang
(integer) 1
127.0.0.1:6379> geoadd locations 104.10 30.65 chengdu
(integer) 1
127.0.0.1:6379> geoadd locations 121.48 31.40 shanghai
(integer) 1
127.0.0.1:6379> geoadd locations 118.89 31.32 nanjin
(integer) 1
127.0.0.1:6379> geoadd locations 120.63 31.30 suzhou
(integer) 1

geopos

语法:geopos key member [member ...]

获取的结果是一个坐标值

# 获取指定的城市的经纬度
127.0.0.1:6379> GEOPOS locations beijing
1) 1) "116.23000055551528931"
   2) "40.2200010338739844"
127.0.0.1:6379> GEOPOS locations suzhou
1) 1) "120.62999814748764038"
   2) "31.30000043401529553"
127.0.0.1:6379> GEOPOS locations guiyang
1) 1) "106.61999970674514771"
   2) "26.67000018801083172"

geodist

语法:geodist key member1 member2 [m|km|ft|mi]

返回两个位置之间的直线距离

单位:

  • m表示米
  • km表示千米
  • mi表示英里
  • ft表示英尺
# 北京贵阳之间的距离
127.0.0.1:6379> GEODIST locations beijing guiyang km
"1748.6332"
127.0.0.1:6379> GEODIST locations beijing chengdu km
"1527.3545"

georadius

语法:georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]

查询某一个经纬度方圆某个距离的记录

# 查询录入的位置中在130,30方圆1000km以内的城市
127.0.0.1:6379> GEORADIUS locations 130 30 1000 km
1) "suzhou"
2) "shanghai"
# 加上坐标
127.0.0.1:6379> GEORADIUS locations 130 30 1000 km withcoord
1) 1) "suzhou"
   2) 1) "120.62999814748764038"
      2) "31.30000043401529553"
2) 1) "shanghai"
   2) 1) "121.48000091314315796"
      2) "31.40000025319353938"
# 再加上距离
127.0.0.1:6379> GEORADIUS locations 130 30 1000 km withcoord withdist
1) 1) "suzhou"
   2) "907.8827"
   3) 1) "120.62999814748764038"
      2) "31.30000043401529553"
2) 1) "shanghai"
   2) "829.3492"
   3) 1) "121.48000091314315796"
      2) "31.40000025319353938"

# 限制记录条数  这里值取1条
127.0.0.1:6379> GEORADIUS locations 130 30 1000 km withcoord withdist count 1
1) 1) "shanghai"
   2) "829.3492"
   3) 1) "121.48000091314315796"
      2) "31.40000025319353938"

georadiusbymember

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]

找出指定元素方圆某个距离的所有元素

# 找出北京1500km内的城市
127.0.0.1:6379> GEORADIUSBYMEMBER locations beijing 1500 km
1) "nanjin"
2) "suzhou"
3) "shanghai"
4) "beijing"
# 其他参数
127.0.0.1:6379> GEORADIUSBYMEMBER locations beijing 1500 km withcoord withdist count 3
1) 1) "beijing"
   2) "0.0000"
   3) 1) "116.23000055551528931"
      2) "40.2200010338739844"
2) 1) "nanjin"
   2) "1018.4575"
   3) 1) "118.89000087976455688"
      2) "31.3199993839624824"
3) 1) "suzhou"
   2) "1068.2744"
   3) 1) "120.62999814748764038"
      2) "31.30000043401529553"

geohash

geohash key member [member]

返回11个字符的geohash字符串

127.0.0.1:6379> GEOHASH locations beijing
1) "wx4sucu47r0"

geo的底层是zset

我们可以使用zset的命令来操作geo

127.0.0.1:6379> ZRANGE locations 0 -1
1) "guiyang"
2) "chengdu"
3) "nanjin"
4) "suzhou"
5) "shanghai"
6) "beijing"


127.0.0.1:6379> ZREM locations shanghai nanjin
(integer) 2

127.0.0.1:6379> ZRANGE locations 0 -1
1) "guiyang"
2) "chengdu"
3) "suzhou"
4) "beijing"

Hyperloglog

Hyperloglog :基数统计

什么是基数?

一个集合中去掉重复元素后,剩余元素的个数

A = {1,2,3,4,5,5,6,6,7} 基数为 7

B = {a,b,c,d,e,f,f,f} 基数为6

简介

例:统计一个网站的访问量,一个人访问多次也只是一个人

传统的方式是使用set来保存用户的id,在统计元素的个数,但是这样会保存大量的用户id,会比较麻烦,我们只需计数,所以使用Hyperloglog

Hyperloglog的优点:

占用的内存是固定的,内存花费小

Hyperloglog有0.81%的容错率,在统计数量的时候可以忽略

基本使用

# 添加元素
127.0.0.1:6379> PFADD key1 1 2 3 4 5 5 a b c
(integer) 1
# 统计基数
127.0.0.1:6379> PFCOUNT key1
(integer) 8
# 添加元素
127.0.0.1:6379> PFADD key2  a a a b b b c c c
(integer) 1
# 统计基数
127.0.0.1:6379> PFCOUNT key2
(integer) 3
# 合并两个集合
127.0.0.1:6379> PFMERGE key3 key1 key2
OK
# 统计合并后的基数
127.0.0.1:6379> PFCOUNT key3
(integer) 8

使用很简单

Bitmap

位存储

Bitmap位图,是一种数据结构,都是操作二进制位来进行记录,只有0和1两个状态

统计状态的都可以使用bitmap来记录

例如:用户是否活跃,是否登陆,是否打卡,这种两个状态的

基本使用

例如记录一个周的打卡情况

# 录入打卡信息 1打卡 0未打卡
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 1
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1
(integer) 0
127.0.0.1:6379> SETBIT sign 5 0
(integer) 0
127.0.0.1:6379> SETBIT sign 6 1
(integer) 0

# 获取某一天的打卡信息
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 2
(integer) 0

# 统计一周的打卡情况 打卡的天数
127.0.0.1:6379> BITCOUNT sign
(integer) 5

事务

关系型数据库中的事务的特性

  • Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Redis中的事务特性

redis的事务本质

redis的事务本质是一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行。一次性,顺序性,排他性的执行一系列命令

所有的命令在事务中并没有被直接执行,只有发起执行命令exec的时候才会执行

redis的事务没有隔离级别的概念

Redis的单条命令是保证原子性的,但是Redis的事务是不保证原子性的

redis执行事务的流程

  • 开启事务multi
  • 命令入队
  • 执行事务exec

正常执行一次事务

# 开启事务
127.0.0.1:6379> multi
OK

# 一系列命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED

# 执行事务
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v1"
4) OK

放弃事务

使用discard来取消事务

一旦放弃事务,队列中的所有命令都不会被执行

# 开启事务
127.0.0.1:6379> multi
OK
# 命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
# 取消事务
127.0.0.1:6379(TX)> discard
OK
# 获取不到k1的值 可以发现队列中的命令并没有被执行
127.0.0.1:6379> get k1
(nil)

事务中出现编译时异常

当事务中出现编译性异常,就是在命令检查阶段就出现了错误,比如命令写出错了,这样事务在执行时,所有的语句都不会执行

# 开启事务
127.0.0.1:6379> multi
OK
# 命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
# 错误命令
127.0.0.1:6379(TX)> sett k2 v2
(error) ERR unknown command `sett`, with args beginning with: `k2`, `v2`, 
127.0.0.1:6379(TX)> set k3 v3
QUEUED
# 事务执行出错
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
# 命令都没有执行
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> get k3
(nil)

事务中出现运行时异常

当事务中出现运行时异常,比如一些逻辑错误,除0等,这样事务在执行时,会执行其它正确的命令,只有异常的命令不被执行

127.0.0.1:6379> set k1 v1
OK

# 开启事务
127.0.0.1:6379> multi
OK
#命令入队
127.0.0.1:6379(TX)> set k2 v2
QUEUED
# k1是字符串 不能自增 这里会出错
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
# 执行事务,只有错误的语句执行失败
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"

所以记住:Redis的事务没有原子性!

Redis实现乐观锁

乐观锁和悲观锁

悲观锁:很悲观,认为什么时候都会出温柔,无论做什么都会加锁

乐观锁:很乐观,认为什么时候都没有问题,无论做什么都不会上锁,只会在更改数据的时候判断在此期间该数据有没有被修改过

  • 获取version
  • 更新的时候比较version是否一致

乐观锁的实现

使用wacth来实现

模拟AB之间的转账

  • 正常情况下
127.0.0.1:6379> set A 1000
OK
127.0.0.1:6379> set B 1000
OK
# 监视A和B 如果修改时A和B的值和此时不一样会执行失败 相当于version
127.0.0.1:6379> watch A B
OK
# 开启事务
127.0.0.1:6379> multi
OK
# A转B100
127.0.0.1:6379(TX)> decrby A 100
QUEUED
# b收到100
127.0.0.1:6379(TX)> incrby B 100
QUEUED

# 执行成功
127.0.0.1:6379(TX)> exec
1) (integer) 900
2) (integer) 1100
  • 在执行事务时被另一个线程抢先修改
127.0.0.1:6379> set A 1000
OK
127.0.0.1:6379> set B 1000
OK
# 监视A和B 如果修改时A和B的值和此时不一样会执行失败 相当于version
127.0.0.1:6379> watch A B
OK
127.0.0.1:6379> multi
OK
# A转给B 100
127.0.0.1:6379(TX)> decrby A 100
QUEUED
127.0.0.1:6379(TX)> incrby B 100
QUEUED

# 在执行事务前 线程2修改了A的余额 导致事务失败
127.0.0.1:6379(TX)> exec
(nil)

# 线程2
# 另一个线程 修改A的余额
127.0.0.1:6379> set A 2000
OK

事务失败时重新检视即可

# 取消检视
127.0.0.1:6379> UNWATCH
OK
# 重新检视
127.0.0.1:6379> WATCH A B
OK

# 重新执行事务
...

Jedis

什么是Jedis

  • 使用Java来操作Redis的中间件

  • Redis推荐的Java连接开发工具

基本使用

导入依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>
<!--方便测试-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
</dependency>

记得开启RedisServer

jedis中的所有方法对应redis中的命令,掌握了命令使用jedis很容易

@Test
public void testList(){
    // 连接redis
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    // 测试连接 成功返回pong
    System.out.println(jedis.ping());
    jedis.rpush("list", "1", "2", "3", "4");
    List<String> list = jedis.lrange("list", 0, -1);
    System.out.println(list);
    jedis.close();
}

@Test
public void testList(){
    // 连接redis
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.rpush("list", "1", "2", "3", "4");
    List<String> list = jedis.lrange("list", 0, -1);
    System.out.println(list);
    jedis.close();
}

@Test
public void testSet(){
    // 连接redis
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.sadd("set", "a", "b", "c", "a");
    Set<String> set = jedis.smembers("set");
    System.out.println(set);
    jedis.close();
}

@Test
public void testHash(){
    // 连接redis
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    Map<String, String> map = new HashMap<>();
    map.put("name", "wcy");
    map.put("age", "18");
    jedis.hset("hash", map);
    List<String> hmget = jedis.hmget("hash", "name", "age");
    System.out.println(hmget);
    jedis.close();
}

Jedis操作事务

依然模拟AB转账

  • 正常情况
@Test
public void testTransaction(){
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.flushDB();
    jedis.mset("A", "1000", "B", "1000");
    jedis.watch("A", "B");
    // 事务对象
    Transaction transaction = jedis.multi();
    try {
        // 用事务对象执行
        transaction.decrBy("A", 100);
        transaction.incrBy("B", 100);
        // 提交事务
        transaction.exec();
    }catch (Exception e){
        // 放弃事务
        transaction.discard();
        e.printStackTrace();
    }
    System.out.println(jedis.mget("A", "B"));
    jedis.close();
}
[900, 1100]
  • 发生错误
@Test
public void testTransaction(){
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.flushDB();
    jedis.mset("A", "1000", "B", "1000");
    jedis.watch("A", "B");
    // 事务对象
    Transaction transaction = jedis.multi();
    try {
        // 用事务对象执行
        transaction.decrBy("A", 100);
        int i = 1 / 0;
        transaction.incrBy("B", 100);
        // 提交事务
        transaction.exec();
    }catch (Exception e){
        // 放弃事务
        transaction.discard();
        e.printStackTrace();
    }
    System.out.println(jedis.mget("A", "B"));
    jedis.close();
}
[1000, 1000]

Spring整合Redis

简介

在springboot2.x以后,原来使用的jedis被替换为了lettuce

整合Redis

导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置文件

spring.redis.host=localhost
spring.redis.database=0
spring.redis.port=6379

使用Redis需要用到RedisTemplate

在RedisAutoConfiguration中默认有两个RedisTemplate

我们可以自定义RedisTemplate,这样会使用我们自己定义的

@Bean
@ConditionalOnMissingBean(
    name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

测试

@Autowired
RedisTemplate<Object, Object> redisTemplate;
  • 连接对象
@Test
void testConn(){
    // 获取连接对象
    RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    // 测试连接
    System.out.println(connection.ping());
    connection.flushDb();
    connection.close();

}

RedisTemplate中提供了系列的ops方法来操作Redis中对应的数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oSbtMciw-1636007528428)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211021123903924.png)]

例如:

// String
@Test
void testString() {
    redisTemplate.opsForValue().set("name", "wcy");
    System.out.println(redisTemplate.opsForValue().get("name"));
}

// List
@Test
void testList(){
    redisTemplate.opsForList().leftPushAll("list", 1,2,3,4,5);
    System.out.println(redisTemplate.opsForList().range("list", 0, -1));
}

//Hash
@Test
void testHash(){
    HashMap<String, String> map = new HashMap<>();
    map.put("name", "wcy");
    map.put("age", "18");
    redisTemplate.opsForHash().putAll("user:1", map);
    System.out.println(redisTemplate.opsForHash().multiGet("user:1", Arrays.asList("name", "age")));
}

存储对象

直接存储

这样必须让对象实现序列化接口,否则报错

@Data
@Accessors(chain = true)
public class User implements Serializable {
    private String name;
    private Integer age;
}
@Test
void testObject1() {
    redisTemplate.opsForValue().set("user:1", new User().setName("wcy").setAge(18));
    System.out.println(redisTemplate.opsForValue().get("user:1"));
}

如果不实现序列化

可以将对象转换为json字符串存储

@Test
void testObject() throws JsonProcessingException {
    String user = new ObjectMapper().writeValueAsString(new User().setName("wcy").setAge(18));
    redisTemplate.opsForValue().set("user:1", user);
    System.out.println(redisTemplate.opsForValue().get("user:1"));
}

这样可以成功,但是redis中存储的键会有转义字符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Md3SGGEu-1636007528429)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211021185130332.png)]

通过自定义RedisTemplate可以解决

自定义RedisTemplate

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // jackson的序列化方式
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NON_PRIVATE);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        // String序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 设置Redis的序列化
        // key
        template.setKeySerializer(stringRedisSerializer);
        // hashKey
        template.setHashKeySerializer(stringRedisSerializer);
        // value
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hashValue
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

这样我们可以直接存储对象,而且键也不会乱码

在实际使用使用中我们会分装一个RedisUtils来在SpringBoot中使用redis,不然每次都要写很长的语句来实现

例如:

@Component
public class RedisUtils {
    
    @Autowired
    RedisTemplate<String, Object> redisTemplate;
    
    public Long lpush(String key, Object... values){
        return redisTemplate.opsForList().leftPushAll(key, values);
    }
    
}

Redis.conf

在启动redis-server是我们是通过redis.conf来启动的

$ redis-server myconfig/redis.conf

配置文件中究竟有些什么呢?

单位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kRKIKRZI-1636007528429)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211021194306775.png)]

  • 在redis.conf中单位不区分大小写

包含配置文件(Include)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-af5acIXy-1636007528430)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211021201144224.png)]

可以在这里导入其他的配置文件

include xx.conf

网络(Network)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X855ZR8B-1636007528431)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211021201332359.png)]

常用的redis关于网络的配置

# 绑定ip
bind 127.0.0.1

# 保护模式
protected-mode yes

# 端口
port 6379

常规配置(General)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FA2UomEU-1636007528432)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211021201749406.png)]

daemonize yes  # 是否后台运行 一般我们都会开启 

# pid文件位置  后台运行需要的pid文件
pidfile /var/run/redis_6379.pid

# 日志级别
loglevel notice
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)

# 日志文件
logfile ""

# 数据库数量
databases 16

# 是否总是显示logo
always-show-logo no

快照(SNAPSHOTTING)

redis数据的持久化,没隔一段时间完成指定次数的操作就会持久化到.rdb.aof

redis是内存数据库,如果不持久化,数据就会丢失

save <seconds> <changes>

save 3600 1
	* After 3600 seconds (an hour) if at least 1 key changed
save 300 100
	* After 300 seconds (5 minutes) if at least 100 keys changed
save 60 10000
	* After 60 seconds if at least 10000 keys changed

# 持久化出错时是否还要继续工作
stop-writes-on-bgsave-error yes

# 是否压缩rdb文件,这回耗费cpu资源
rdbcompression yes

# 保存rdb文件是,进行错误的检查校验
rdbchecksum yes

# 保存文件的位置
dir ./

主从复制(REPLICATION)

主从复制

replicaof <masterip> <masterport>  # 主机ip port

masterauth <master-password>    #如果主机有密码 配置主机密码

安全(SECURITY)

redis默认没有密码,可以在这里设置

# 初始密码为空
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""

# 设置密码
127.0.0.1:6379> config set requirepass 123456
OK

# 这时需要认证
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> ping
PONG

客户端(Clients)

# 最大客户端连接数量
maxclients 10000

内存管理(MEMORY MANAGEMENT)

# 最大内存容量
maxmemory <bytes>

# 内存达到上限之后的处理策略
maxmemory-policy noeviction
# 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.

AOF配置(APPEND ONLY MODE )

# 默认不使用aof持久化,一般rdb足够
appendonly no

# aof持久化文件名
appendfilename "appendonly.aof"


# 每次修改都会同步
appendfsync always
# 每秒同步一次 可能会丢失1s的数据
appendfsync everysec
# 不执行同步 操作系统自己同步数据
appendfsync no

Redis持久化

RDB(Redis DataBase)

img

RDB其实就是把数据以快照的形式保存在磁盘上。

什么是快照呢,可以理解成把当前时刻的数据拍成一张照片保存下来。

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb

在我安装了redis之后,所有的配置都是在redis.conf文件中,里面保存了RDB和AOF两种持久化机制的各种配置

既然RDB机制是通过把某个时刻的所有数据生成一个快照来保存,那么就应该有一种触发机制,是实现这个过程。

对于RDB来说,提供了三种机制:save、bgsave和自动化

1、save触发方式

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iXVObh3a-1636007528433)(https://pics1.baidu.com/feed/e7cd7b899e510fb3aa8c05042b22c093d0430ca7.jpeg?token=7ed4cf784a82d04e60b8dc72cf7e3c24&s=EDBAA5565D1859C85444707E02005071)]

执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都是几万或者是几十万,这种方式显然不可取。

2、bgsave触发方式

执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttAFiGHG-1636007528434)(https://pics5.baidu.com/feed/023b5bb5c9ea15cefb035bc8431132f53b87b21e.jpeg?token=a72f072d65d2de548d71bb459cd0bf4f&s=05AAFE168FF04C8A10FD2DEE0300E032)]

具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。

3、自动触发

自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:

**①save:**这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。

默认如下配置:

save 900 1
#表示900 秒内如果至少有 1 个 key 的值变化,则保存

save 300 10
#表示300 秒内如果至少有 10 个 key 的值变化,则保存

save 60 10000
#表示60 秒内如果至少有 10000 个 key 的值变化,则保存

如果不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。

**②stop-writes-on-bgsave-error:**默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了

**③rdbcompression ;**默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。

**④rdbchecksum:**默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

**⑤dbfilename :**设置快照的文件名,默认是 dump.rdb

**⑥dir:**设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。

我们可以修改这些配置来实现我们想要的效果。因为第三种方式是配置的,所以我们对前两种进行一个对比:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3fE5hq2-1636007528439)(https://pics5.baidu.com/feed/1c950a7b02087bf43b4490d50ac25f2a11dfcf7e.jpeg?token=22f387ba78130c6115420059481b2393&s=EF48A15796784D8816E1D9EB03007024)]

执行flushall命令,也会触发rdb规则

退出redis也会产生dump.rdb文件

4、RDB 的优势和劣势

优势

(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。

(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

劣势

RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。

当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

AOF(Append Only File)

img

全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。AOF的默认文件为appendonly.aof

1、持久化原理

他的原理看下面这张图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aCy3bhkY-1636007528441)(https://pics3.baidu.com/feed/32fa828ba61ea8d3c2502e396b1b3848251f58b0.jpeg?token=394597ccd73bd15778c518b5c5be6998&s=2D62E7169D305F8A847546E20200B036)]

每当有一个写命令过来时,就直接保存在我们的AOF文件中。

2、文件重写原理

AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6jiuG6uP-1636007528441)(https://pics7.baidu.com/feed/09fa513d269759ee28454d2c4cea4b106c22dfd3.jpeg?token=86eda46b8bcd54a7a0e7d8a37d87bee8&s=EDB2A4579D317B824660D4DF0200E036)]

重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

3、AOF也有三种触发机制

(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好

(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失

(3)不同no:从不同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9dg9CGft-1636007528442)(https://pics5.baidu.com/feed/b17eca8065380cd7df69859ba056a5325982816c.jpeg?token=a060f459d81c409c3d6c7208d2118888&s=AF4AA5574ED85CC841D04BE60300A036)]

AOF的开启

# 默认为no不开启
appendonly yes

当aof文件受到损坏时,redis会无法正常启动,可以通过redis-chect-aof来修复

redis-check-aof --fix appendonly.aof

4、优点

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。

(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。

(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

5、缺点

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

当AOF和RDB同时开启时会优先加载AOF来恢复原始的数据,因为AOF保存的数据通常情况下要比RDB更加的完整

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAPsKG9i-1636007528442)(https://pics5.baidu.com/feed/8326cffc1e178a82c532308ef2117b8ba977e8ae.jpeg?token=fea28817e45f0e091b5be3854d856fbb&s=BD48B55F1C784C095E61DCEB0300D036)]

Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式

  • 发布者发送消息
  • 订阅者接收消息

Redis可以订阅任意数量的频道(channel)

img

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

img

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

img

Redis 发布订阅命令

序号命令及描述
1PSUBSCRIBE pattern [pattern …] 订阅一个或多个符合给定模式的频道。
2PUBSUB subcommand [argument [argument …]]] 查看订阅与发布系统状态。
3PUBLISH channel message 将信息发送到指定的频道。
4PUNSUBSCRIBE [pattern [pattern …]] 退订所有给定模式的频道。
5SUBSCRIBE channel [channel …] 订阅给定的一个或多个频道的信息。
6UNSUBSCRIBE [channel [channel …]] 指退订给定的频道。
# 订阅wcy频道
127.0.0.1:6379> PSUBSCRIBE wcy
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "wcy"
3) (integer) 1


# wcy频道发送消息 重开一个端发送
127.0.0.1:6379> PUBLISH wcy hello
(integer) 1

# 订阅者接收
127.0.0.1:6379> PSUBSCRIBE wcy
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "wcy"
3) (integer) 1
1) "pmessage"
2) "wcy"
3) "wcy"
4) "hello"

使用场景

  • 订阅关注系统
  • 实时聊天系统
  • 实时消息系统

Redis主从复制

原理

什么是主从复制

主从复制指将一台redis的数据复制另外一台redis服务器上,前者称为主节点(master),后者称为从节点(slave)

主节点可以进行读写操作,从节点只能有读操作(并不一定,只是推荐这么做,后续会说明)

当主节点有数据写入,会将数据同步复制给从节点

一个主节点可以同时拥有多个从节点,而从节点只能拥有一个主节点。

从节点也可以有从节点,级联结构

使用主从复制,一般最少一主两从

所有的redis数据库默认都是主节点,需要通过配置来配置从节点

主从复制的作用

**数据冗余:**主从复制实现了数据的热备份

故障恢复:当主节点出现问题时,可以由从节点提供服务,快速恢复故障

负载均衡:主节点复制写数据,从节点负责读数据,实现读写分离分担服务器负载

img

如何配置

我们在同一个服务器上开启多个redis服务来模拟集群,来实现一主两从

首先复制多份配置文件

cp redis.conf redis6379.conf
cp redis.conf redis6380.conf
cp redis.conf redis6381.conf

修改配置

port 6379
pidfile /var/run/redis_6379.pid
logfile "6379.log"
dbfilename dump6379.rdb

port 6380
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dbfilename dump6380.rdb

port 6381
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump6381.rdb

开启服务

redis-server myconfig/redis6379.conf 
redis-server myconfig/redis6380.conf 
redis-server myconfig/redis6381.conf 

ps -ef|grep redis
root       590     1  0 11:18 ?        00:00:00 redis-server 127.0.0.1:6380
root       609     1  0 11:18 ?        00:00:00 redis-server 127.0.0.1:6381
root      5260     1  0 Oct22 ?        00:01:21 redis-server 127.0.0.1:6379

通过info replication查看情况

127.0.0.1:6379> info replication
# Replication
role:master	    # 默认的role都是master 即为主节点
connected_slaves:0
master_failover_state:no-failover
master_replid:aa17ff819ab3816adb10a19eb26cc89845542f08
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

配置主从节点

配置从节点,我们不需要配置那个节点为从节点 只要配置该节点的主节点是谁就ok

使用SLAVEOF host prot通过命令配置,这样配置在服务器关闭后会失效,如果需要永久配置需要在配置文件中配置

127.0.0.1:6380> SLAVEOF localhost 6379
OK
127.0.0.1:6381> SLAVEOF localhost 6379
OK

查看6379服务可以发现已经有两个从节点

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2  # 两个从节点
slave0:ip=127.0.0.1,port=6380,state=online,offset=98,lag=1  # 6380
slave1:ip=127.0.0.1,port=6381,state=online,offset=98,lag=1	# 6381
master_failover_state:no-failover
master_replid:b7685685da14dde97fe2c8740cae750aae3246ca
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:98
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:98


# 
127.0.0.1:6380> info replication
# Replication
role:slave			# 从节点
master_host:localhost
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_read_repl_offset:22513
slave_repl_offset:22513
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:b7685685da14dde97fe2c8740cae750aae3246ca
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:22513
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:22513

主节点可以读写,从节点只能读

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK

127.0.0.1:6380> get k1
"v1"
127.0.0.1:6381> get k2
"v2"

# 不可以写
127.0.0.1:6381> set k5 v5
(error) READONLY You can't write against a read only replica.

主机宕机时会发生什么

默认情况下,当我们的主机宕机时,从机依然是从机,不会改变

主机宕机后,从机依然能够获取之前的值

当主机重新恢复后,设置值,从机依然能够读取到

当我们使用命令行的方式来配置从机时,当服务重启后就会变为主机,但是一旦将他继续配置为从机后,他就能立马获取到主机中的信息

主从复制的原理

全量复制:

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

master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,

在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步

而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:

master继续将新的所有收集到的修改命令依次传给slave,完成同步。

注意:

(但是只要是重新连接master,回自动执行一次完全同步(全量复制))

如果主机宕机了,是否可以重新选取一个作为主机?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-togB6LEw-1636007528447)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211023163103532.png)]

每一个后面的节点都把前面的节点当做主机

如果前面的主机宕机了,再将后面的从机变为主机

完成这个操作只需要一个命令slaveof no one,将当前节点变为主节点

127.0.0.1:6380> SLAVEOF no one
OK

# 可以看到已经变成了主节点
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:4a02a544bfd0b20de074bd9e4b8d13577d4c531d
master_replid2:b7685685da14dde97fe2c8740cae750aae3246ca
master_repl_offset:24725
second_repl_offset:24726
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:24725

修改之后,如果原先的主机重新连接了,此时也不会有作用,除非重新配置为主机

哨兵模式

之前我们都是手动配置主从节点,但是在redis中可以通过哨兵模式来实现自动配置。

Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis 的高可用性解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。

sentinel是一个单独的进程

sentinel是redis高可用的解决方案,sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。

sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。

一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。其结构如下:

img

如何配置

还是主1从2的模式

首先要创建配置文件sentinel.conf

# 设置监控
sentinel monitor <masterName> <ip> <port> <quorum>
	<masterName>     名称
    <ip> 			ip
    <port>			端口
    <quorum>		有几个哨兵认为该主机宕机了就会重新选取主机

sentinel.conf

哨兵监视的只有主机

sentinel monitor myconfig 127.0.0.1 6379 1

开启哨兵

redis-sentinel myconfig/sentienl.conf 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpVA14DO-1636007528448)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211023190227193.png)]

这样就是启动成功了,并且也显示了两个从节点

测试哨兵模式

当我们的主节点挂掉之后,会怎么样呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2WFbAtSy-1636007528449)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20211023190538203.png)]
可以看到6381的服务成为了主节点,我们并没有手动配置,这就是哨兵模式自动给我吗选出来的

并且就算宕机的主节点重新上线之后,他也不会继续回到主节点的位置,而是变成当前主节点的从节点

一组sentinel能同时监控多个Master

通常我们会开启多个sentinel来监视,防止sentinel自身宕机

Redis缓存击穿、缓存穿透和缓存雪崩

缓存穿透

概述

缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义

img

影响

缓存穿透问题可能会使后端存储负载加大,由于很多后端持久层不具备高并发性,甚至可能造成后端存储宕机。通常可以在程序中统计总调用数、缓存层命中数、如果同一个Key的缓存命中率很低,可能就是出现了缓存穿透问题。

产生原因

  • 自身业务代码或者数据出现问题(例如:set 和 get 的key不一致)
  • 一些恶意攻击、爬虫等造成大量空命中(爬取线上商城商品数据,超大循环递增商品的ID)

解决方法

  • 缓存空对象

    缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)

    缓存空对象会有两个问题:

    • value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

    • 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象

  • 布隆过滤器拦截

    在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

    布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

缓存击穿

概述

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

系统中存在以下两个问题时需要引起注意:

  • 当前key是一个热点key(例如一个秒杀活动),并发量非常大。
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。

在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

img

解决方案

  • 分布式互斥锁

    只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可

  • key永不过期

    从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
    从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存

缓存雪崩

概述

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

img

解决方案

  • 可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。
  • 采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
  • 缓存的过期时间用随机值,尽量让不同的key的过期时间不同(例如:定时任务新建大批量key,设置的过期时间相同)
    er服务继续处理请求。

sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。

一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。其结构如下:

[外链图片转存中…(img-w2NDDvor-1636007528447)]

如何配置

还是主1从2的模式

首先要创建配置文件sentinel.conf

# 设置监控
sentinel monitor <masterName> <ip> <port> <quorum>
	<masterName>     名称
    <ip> 			ip
    <port>			端口
    <quorum>		有几个哨兵认为该主机宕机了就会重新选取主机

sentinel.conf

哨兵监视的只有主机

sentinel monitor myconfig 127.0.0.1 6379 1

开启哨兵

redis-sentinel myconfig/sentienl.conf 

[外链图片转存中…(img-GpVA14DO-1636007528448)]

这样就是启动成功了,并且也显示了两个从节点

测试哨兵模式

当我们的主节点挂掉之后,会怎么样呢?

[外链图片转存中…(img-2WFbAtSy-1636007528449)]
可以看到6381的服务成为了主节点,我们并没有手动配置,这就是哨兵模式自动给我吗选出来的

并且就算宕机的主节点重新上线之后,他也不会继续回到主节点的位置,而是变成当前主节点的从节点

一组sentinel能同时监控多个Master

通常我们会开启多个sentinel来监视,防止sentinel自身宕机

Redis缓存击穿、缓存穿透和缓存雪崩

缓存穿透

概述

缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义

[外链图片转存中…(img-i4U6AESB-1636007528450)]

影响

缓存穿透问题可能会使后端存储负载加大,由于很多后端持久层不具备高并发性,甚至可能造成后端存储宕机。通常可以在程序中统计总调用数、缓存层命中数、如果同一个Key的缓存命中率很低,可能就是出现了缓存穿透问题。

产生原因

  • 自身业务代码或者数据出现问题(例如:set 和 get 的key不一致)
  • 一些恶意攻击、爬虫等造成大量空命中(爬取线上商城商品数据,超大循环递增商品的ID)

解决方法

  • 缓存空对象

    缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)

    缓存空对象会有两个问题:

    • value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

    • 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象

  • 布隆过滤器拦截

    在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

    布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

缓存击穿

概述

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

系统中存在以下两个问题时需要引起注意:

  • 当前key是一个热点key(例如一个秒杀活动),并发量非常大。
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。

在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

[外链图片转存中…(img-apdqql14-1636007528450)]

解决方案

  • 分布式互斥锁

    只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可

  • key永不过期

    从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
    从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存

缓存雪崩

概述

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

[外链图片转存中…(img-CVdAAuXY-1636007528451)]

解决方案

  • 可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。
  • 采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
  • 缓存的过期时间用随机值,尽量让不同的key的过期时间不同(例如:定时任务新建大批量key,设置的过期时间相同)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值