Redis笔记

此笔记只为了自学&复习,同时也为能够帮助同样努力奋斗的小伙伴们。

该笔记是根据狂神大佬讲解的【狂神说Java】Redis最新超详细版教程通俗易懂写的,如果觉得侵权请联系我。同时很感谢狂神大佬,受益颇多。

一、NoSQL概述

1.NoSQL

not only sql(不仅仅是sql)

非关系型数据库

2.NoSQL特点

(1)方便扩展(数据之间没有关系,很好扩展)

(2)大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高)

(3)数据类型是多样型的(不需要事先设计数据库,随取随用)

(4)传统的 RDBMS 和 NoSQL

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

结构化组织

SQL

数据和关系都存在单独的表中 row col

操作,数据定义语言

严格的一致性

基础的事务

...

NoSQL:

不仅仅是数据

没有固定的查询语言

键值对存储,列存储,文档存储,图形数据库(社交关系)

最终一致性

CAP定理和BASE

高性能,高可用,高扩展

...

3.大数据时代的3V和3高

3V:主要用来描述问题(海量Velume、多样Variety、实时Velocity

3高:主要是对程序的要求(高并发、高可扩、高性能)

真正在公司中实践:NoSQL(非关系型数据库)+RDBMS(关系型数据库)一起使用才是最强的。

二、Redis入门

1.Redis是什么?

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

与memcached一样,为了保证效率,数据都是缓存在内存中。

区别是Redis会周期性的把更新的数据写入到磁盘或者修改把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

2.Redis能干什么?

(1)内存存储、持久化(内存是断电即失的,所以需要持久化(RDB、AOF))

(2)高效率、用于高速缓冲

(3)发布订阅系统

(4)地图信息分析

(5)计时器、计数器(eg:浏览量)

(6)......

3.Redis的特性

(1)多样的数据类型

(2)持久化

(3)集群

(4)事务

(5)......

4.修改Redis配置文件

修改配置文件的时候可以选择性的修改以下四处:

(1)daemonize 设置为 yes 表明要在后台运行

(2)把 bind 127.0.0.1这一行注释掉,就没有请求访问 ip限制了

(3)把 protected-mode 设置成 no 即可开启远程访问

(4)去掉 requirepass foobared前面的注释,并修改为所需要的密码:requirepass myPassword (其中myPassword就是要设置的密码,例如:requirepass 654321

5.Redis初使用

# 查看redis进程判断redis是否开启
ps -ef|grep redis

# 如果没开启,需要先开启redis-server
redis-server /etc/redis/6379.conf

# 开启服务端后,开启redis客户端
redis-cli

# 测试
# 输入:
ping
# 回复:
PONG

# 退出
exit

6.性能测试

redis-benchmark:Redis官方提供的性能测试工具

image-20211012091533621

# 进入redis-benchmark所在的文件目录
cd usr/local/bin

# 进行性能测试,其中数字分别表示为100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

image-20211012145128939

7.基础知识

image-20211012145348479

redis默认有16个数据库,默认使用的第0个数据库,使用select n切换到指定的第n+1个数据库(0≤n≤15),使用dbsize查看当前数据库的大小,与key数量相关。

image-20211012153730790

  • config get databases:查看数据库数量
  • select 8:切换数据库8
  • dbsize:查看当前数据库大小,根据数据库中的key的个数
  • keys *:查看当前数据库中所有的key
  • flushdb:清空当前数据库中的键值对
  • flushall:清空所有的数据库的键值对

Redis是单线程的,它基于内存操作。因此,Redis的性能瓶颈不是CPU,而是机器内存和网络带宽。

为什么redis单线程还这么快?

  • 误区一:高性能的服务器一定是多线程的(x)解释:redis单线程基于内存照样很快

  • 误区二:多线程一定比单线程效率高(x) 解释:多线程CPU上下文会进行切换

核心:Redis是将所有的数据放在内存中,所以说使用单线程操作效率就是最高的,多线程(CPU上下文切换会耗时),对于内存系统来说,如果没有上下文切换笑脸就是最高的,多次读写都是在一个CPU上,在内存存储数据的情况下,单线程就是最佳的方案。

三、Redis的五种基本数据类型

Redis开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序结合、位图,hypreloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供的高可用,通过Redis Cluster提供自动分区。

Redis-Key

在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。

下面学习的命令:

  • exists key:判断键是否存在
  • del key:删除键值对
  • move key db:将键值对移动到指定数据库
  • expire key seconds或者pexpire key milliseconds:设置键值对的过期时间,其中expire以秒为单位,pexpire以毫秒为单位
  • ttl key:查看key的过期剩余时间,-2表示key已过期,-1表示key未设置过期时间
  • type key:查看value的数据类型
  • rename key newkey:修改key的名称为newkey,如果newkey已存在,那么值会覆盖
  • renamenx key newkey:仅当newkey不存在时,将key改名为newkey

1.String(字符串)

  • set key value:设置值
  • get key:获取值
  • key *:获取所有的key
  • append key newvalue:在原有value上拼接newvalue后,和Java中的apped效果一样
  • strlen key:获取key的长度
  • incr key:value+1,如果不存在会创建,默认值为0
  • decr key:value-1,如果不存在会创建,默认值为0
  • incrby key 10:value+10,如果不存在会创建,默认值为0
  • decrby key 10:value-10,如果不存在会创建,默认值为0
  • getrange key 0 3:截取字符串[0,3],当getrange key 0 -1,获取全部的字符串和get key是一样的
  • setrange key n newvalue:替换指定n位置处的字符串为newvalue
  • setex:(set with expire)设置过期时间
  • setnx:(set if not exist)不存在再设置
  • mset k1 v1 k2 v2 k3 v3:批量创建
  • mget k1 k2 k3:批量获取
  • msetnx k1 v1 k2 v2 k3 v3:当不存在,再创建,msetnx是原子性操作,要么同时成功,要么同时失败
  • set key:n json:设置一个key:n的对象,值为json字符串来保存一个对象,例:set user:1 {name:zhangsan,age:3}
  • mset user:1:name zhangsan user:1:age 2:巧妙批量设置键值对
  • getset key value:先get后set,如果key已存在,那么返回原来的值,并赋新值value;如果key不存在,那么返回nil,并赋值value

2.List(列表)

总结:

  • 在redis里,可以把list玩成栈、队列、阻塞队列

  • list结构所有的命令都是l开头,只有pushpop会有r开头

  • 实际上是一个链表,before node after,left,right都可以插入值

  • 如果key存在,新增内容

  • 如果移出了所有值,空链表,也代表不存在

  • 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一点

127.0.0.1:6379> lpush list one # 创建key为list,value为one的list数据结构
(integer) 1
127.0.0.1:6379> lpush list two # list已存在,放入value值为two
(integer) 2
127.0.0.1:6379> lpush list three # list已存在,放入value值为three
(integer) 3
127.0.0.1:6379> del list # 删除key名为list的list结构数据
(integer) 1
127.0.0.1:6379> lpush list one
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 查看key为list的value值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lpush list 4 5 # 可以批量放入
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "5"
2) "4"
3) "three"
4) "two"
5) "one"
127.0.0.1:6379> rpush list zero # lpush是左侧放入,rpush是右侧放入
(integer) 6
127.0.0.1:6379> lrange list 0 -1 
1) "5"
2) "4"
3) "three"
4) "two"
5) "one"
6) "zero"
127.0.0.1:6379> lpop list # 删除最左边的一个value
"5"
127.0.0.1:6379> lrange list 0 -1
1) "4"
2) "three"
3) "two"
4) "one"
5) "zero"
127.0.0.1:6379> rpop list # 删除最右边的一个value
"zero"
127.0.0.1:6379> lrange list 0 -1
1) "4"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lindex list 1 # 输出下标处的值
"three"
127.0.0.1:6379> lindex list 0
"4"
127.0.0.1:6379> llen list # 输出长度
(integer) 4
127.0.0.1:6379> lpush list  one
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "4"
3) "three"
4) "two"
5) "one"
127.0.0.1:6379> lrem list 2 one # 删除list中的2个one,可以是只删除一个one,会从左到右依次选择
(integer) 2
127.0.0.1:6379> lrange list 0 -1 
1) "4"
2) "three"
3) "two"
127.0.0.1:6379> ltrim list 0 1  # 只取[0,1]之间的数据,包括0和1,其他删除
OK
127.0.0.1:6379> lrange list 0 -1
1) "4"
2) "three"
127.0.0.1:6379> rpoplpush list haha # 移动右边第一个数据到名为haha的list中,如果haha不存在则创建
"three"
127.0.0.1:6379> lrange list 0 -1 
1) "4"
127.0.0.1:6379> keys * # 查看数据库中所有的key
1) "haha"
2) "views"
3) "name"
4) "list"
127.0.0.1:6379> exists list # 判断list是否存在
(integer) 1
127.0.0.1:6379> lset list 0 item # 替换下标0处的值为item
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
127.0.0.1:6379> lset list 0 hello
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello"
127.0.0.1:6379> rpush list world # 向右边存入value,值为world
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
127.0.0.1:6379> linsert list before "world" "other" # 在world单词前插入other
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> linsert list after "world" "other" # 在world单词后插入other
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "other"
3) "world"
4) "other"

3.Set(集合)

set中的值无序不能重复

微博,A用户将所有关注的人放在一个set集合中(set值不重复的特性)!将它的粉丝也放在一个集合中!

127.0.0.1:6379> sadd myset "hello" # 创建一个set数据值为hello
(integer) 1
127.0.0.1:6379> sadd myset "zhangsan" "lisi" # 批量创建一个set数据值为zhangsan、lisi
(integer) 2
127.0.0.1:6379> smembers myset # 显示myset里中的所有数据
1) "lisi"
2) "hello"
3) "zhangsan"
127.0.0.1:6379> sismember myset hello # 加个is判断,查看是否存在值为hello的数据,存在返回1
(integer) 1
127.0.0.1:6379> sismember myset world # 加个is判断,查看是否存在值为hello的数据,不存在返回0
(integer) 0
127.0.0.1:6379> scard myset # 显示myset有几条数据
(integer) 3
127.0.0.1:6379> sadd myset "hello"
(integer) 0
127.0.0.1:6379> srem myset "hello" # 删除myset中的hello
(integer) 1
127.0.0.1:6379> scard myset 
(integer) 2
127.0.0.1:6379> smembers myset 
1) "lisi"
2) "zhangsan"
127.0.0.1:6379> srandmember myset  # 随机获取一个数据
"zhangsan"
127.0.0.1:6379> srandmember myset
"lisi"
127.0.0.1:6379> srandmember myset
"lisi"
127.0.0.1:6379> srandmember myset
"zhangsan"
127.0.0.1:6379> srem myset "zhangsan" # 删除数据zhangsan
(integer) 1
127.0.0.1:6379> smembers myset # 列出myset中的所有数据
1) "lisi"
127.0.0.1:6379> sadd myset "zhangsan" "wangwu"
(integer) 2
127.0.0.1:6379> smembers myset
1) "lisi"
2) "wangwu"
3) "zhangsan"
127.0.0.1:6379> spop myset # 随机删除一个数据
"zhangsan"
127.0.0.1:6379> smembers myset
1) "lisi"
2) "wangwu"
127.0.0.1:6379> sadd myset2 "set2" 
(integer) 1
127.0.0.1:6379> smove myset myset2 "lisi" # 移动myset中的数据lisi到myset2
(integer) 1
127.0.0.1:6379> smembers myset 
1) "wangwu"
127.0.0.1:6379> smembers myset2
1) "lisi"
2) "set2"
127.0.0.1:6379> sadd myset lisi 
(integer) 1
127.0.0.1:6379> smembers myset
1) "lisi"
2) "wangwu"
127.0.0.1:6379> sdiff myset myset2 # 显示myset中有而myset2中没有的数据,差集
1) "wangwu"
127.0.0.1:6379> sinter myset myset2 # 显示myset和myset2中都有的数据,交集
1) "lisi"
127.0.0.1:6379> sunion myset myset2 # 显示myset和myset2所有的数据,并集
1) "lisi"
2) "wangwu"
3) "set2"
127.0.0.1:6379> sdiff myset2 myset # 显示myset2中有而myset中没有的数据,差集
1) "set2"

4.Hash(哈希)

Map集合,key-map!值是一个map集合

本质和String类型没有太大的区别,还是一个简单的key-value

Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!Hash更适合于对象的存储,String更加适合字符串存储!

127.0.0.1:6379> hset myhash field1 zhangsan # 创建myhash 键是field1 值是zhangsan
(integer) 1
127.0.0.1:6379> hget myhash field1 # 获取myhash中键为field1的值
"zhangsan"
127.0.0.1:6379> hset myhash field1 lisi field2 wangwu # 批量创建myhash中的键值对(hmset也可以,但redis官方推荐使用hset)
(integer) 1
127.0.0.1:6379> hget myhash field1
"lisi"
127.0.0.1:6379> hget myhash field2
"wangwu"
127.0.0.1:6379> hget myhash field1 field2 # 批量获取只能使用hmset
(error) ERR wrong number of arguments for 'hget' command
127.0.0.1:6379> hmget myhash field1 field2 # 批量获取键对应的值
1) "lisi"
2) "wangwu"
127.0.0.1:6379> hgetall myhash # 获取myhash中的所有数据
1) "field1"
2) "lisi"
3) "field2"
4) "wangwu"
127.0.0.1:6379> hdel myhash field1 # 删除myhash中的键值对field1
(integer) 1
127.0.0.1:6379> hgetall myhash # 获取myhash中的所有数据
1) "field2"
2) "wangwu"
127.0.0.1:6379> hlen myhash # 获取myhash中键值对的数量
(integer) 1
127.0.0.1:6379> hset myhash field1 hello field2 world
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> hlen myhash 
(integer) 2
127.0.0.1:6379> hexists myhash field1 # 判断myhash中是否存在键为field1的数据
(integer) 1
127.0.0.1:6379> hexists myhash field3
(integer) 0
127.0.0.1:6379> hkeys myhash # 只显示myhash中的键
1) "field2"
2) "field1"
127.0.0.1:6379> hvals myhash # 只显示myhash中的值
1) "world"
2) "hello"
127.0.0.1:6379> hset myhash field3 5 
(integer) 1
127.0.0.1:6379> hincrby myhash field3 1 # 值+1
(integer) 6
127.0.0.1:6379> hincrby myhash field3 -1  # 值-1
(integer) 5
127.0.0.1:6379> hsetnx myhash field4 hello # 当myhash中不存在键(field4)时,再创建;存在则不创建
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 world
(integer) 0
127.0.0.1:6379> hset user:1 name zhangsan # 将user:1设置为hash名,键为name,值为zhangsan
(integer) 1
127.0.0.1:6379> hget user:1 name # 获取map结构为user:1中键为name的值
"zhangsan"
127.0.0.1:6379> keys * # 显示数据库中所有的键
1) "name"
2) "myhash"
3) "myset2"
4) "list"
5) "myset"
6) "views"
7) "haha"
8) "user:1"
127.0.0.1:6379> hkeys user:1 # 只显示map结构为user:1的key
1) "name"
127.0.0.1:6379> hvals user:1 # 只显示map结构为user:1的value
1) "zhangsan"

5.Zset(sorted set:有序集合)

在set的基础上,增加了一个`z

案例思路:set排序,班级成绩表,工资表排序

普通消息,(1)重要消息 (2)带权重进行判断

排行榜应用实现,取Top N测试

127.0.0.1:6379> zadd myzset 1 one  # 创建一个zset
(integer) 1 
127.0.0.1:6379> zadd myzset 2 two 3 three #批量创建zset
(integer) 2
127.0.0.1:6379> zrange myzset 0 -1 # 显示zset中所有的值
1) "one" 
2) "two"
3) "three"
127.0.0.1:6379> zadd salary 3000 xiaohong 
(integer) 1
127.0.0.1:6379> zadd salary 200 zhangsan 2399 lisi
(integer) 2
127.0.0.1:6379> zrangebyscore salary -inf +inf # 升序显示
1) "zhangsan"
2) "lisi"
3) "xiaohong"
127.0.0.1:6379> zrevrange salary 0 -1 # 降序显示
1) "lisi"
2) "zhangsan"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores # 升序显示并显示数值
1) "zhangsan"
2) "200"
3) "lisi"
4) "2399"
5) "xiaohong"
6) "3000"
127.0.0.1:6379> zrem salary xiaohong # 删除xiaohong
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "zhangsan"
2) "lisi"
127.0.0.1:6379> zcard salary # 显示数据数量
(integer) 2
127.0.0.1:6379> keys *
1) "salary"
2) "myset"
3) "myhash"
4) "myzset"
5) "user:1"
127.0.0.1:6379> del myset
(integer) 1
127.0.0.1:6379> del myzset
(integer) 1
127.0.0.1:6379> keys *
1) "salary"
2) "myhash"
3) "user:1"
127.0.0.1:6379> zadd myset 1 hello 
(integer) 1
127.0.0.1:6379> zadd myset 2 world 4 lisi
(integer) 2
127.0.0.1:6379> zcount myset 1 2 # 获取myset中1到2范围内(包括1和2)的数据的个数
(integer) 2
127.0.0.1:6379> zcount myset 1 3
(integer) 2
127.0.0.1:6379> zcount myset 1 4
(integer) 3

四、Redis的三种特殊数据类型

1.geospatial 地理位置

定位,附近的人,打车距离计算......

Redis的Geo在redis3.2版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!可以查询一些测试数据:http://www.jsons.cn/lngcodeinfo/0706D99C19A781A3/

geospatial只有6个命令,使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用

image-20211014132308062

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

附近的人(获得附近所有人的地址,开启手机定位功能)通过半径查询

127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing # 添加 键名 经度 纬度 名称
(integer) 1
127.0.0.1:6379> geoadd china:city 114.29 30.58 wuhan 
(integer) 1
127.0.0.1:6379> geoadd china:city 117.19 39.12 tianjin
(integer) 1
127.0.0.1:6379> geopos china:city wuhan # 显示武汉的经纬度
1) 1) "114.29000169038772583"
   2) "30.58000021509926825"
127.0.0.1:6379> geopos china:city tianjin beijing # 批量显示经纬度
1) 1) "117.19000071287155151" 
   2) "39.12000048819218279"
2) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> geodist china:city beijing wuhan  # 显示两个地点间的距离
"1054104.6973"
127.0.0.1:6379> geodist china:city beijing tianjin km  # 以km为单位显示两个地点间的距离
"110.1009"
127.0.0.1:6379> geodist china:city tianjin wuhan km
"985.9024"
127.0.0.1:6379> georadius china:city 110 30 1000 km # 显示在经纬度为110,30周围1000km的城市
1) "wuhan"
127.0.0.1:6379> georadius china:city 110 50 10000 km
1) "wuhan"
2) "tianjin"
3) "beijing"
127.0.0.1:6379> georadius china:city 110 50 5000 km
1) "wuhan"
2) "tianjin"
3) "beijing"
127.0.0.1:6379> georadius china:city 110 50 2000 km
1) "tianjin"
2) "beijing"
127.0.0.1:6379> georadius china:city 110 50 2000 km withdist # 显示经纬度为110,50周围2000km的城市和距离
1) 1) "tianjin"
   2) "1336.0749"
2) 1) "beijing"
   2) "1230.0579"
127.0.0.1:6379> georadius china:city 110 50 2000 km withcoord # 显示经纬度为110,50周围2000km的城市和坐标
1) 1) "tianjin"
   2) 1) "117.19000071287155151"
      2) "39.12000048819218279"
2) 1) "beijing"
   2) 1) "116.39999896287918091"
      2) "39.90000009167092543"
127.0.0.1:6379> georadius china:city 110 50 2000 km withdist withcoord # 显示经纬度为110,50周围2000km的城市、距离和坐标
1) 1) "tianjin"
   2) "1336.0749"
   3) 1) "117.19000071287155151"
      2) "39.12000048819218279"
2) 1) "beijing"
   2) "1230.0579"
   3) 1) "116.39999896287918091"
      2) "39.90000009167092543"
127.0.0.1:6379> georadius china:city 110 50 2000 km withdist withcoord count 1 # 显示1条经纬度为110,50周围2000km的城市、距离和坐标
1) 1) "beijing"
   2) "1230.0579"
   3) 1) "116.39999896287918091"
      2) "39.90000009167092543"
127.0.0.1:6379> georadius china:city 110 50 2000 km withdist withcoord count 2 # 显示2条经纬度为110,50周围2000km的城市、距离和坐标
1) 1) "beijing"
   2) "1230.0579"
   3) 1) "116.39999896287918091"
      2) "39.90000009167092543"
2) 1) "tianjin"
   2) "1336.0749"
   3) 1) "117.19000071287155151"
      2) "39.12000048819218279"
127.0.0.1:6379> georadiusbymember china:city tianjin 1000 km # 查看距离天津1000km的城市
1) "tianjin"
2) "beijing"
3) "wuhan"
127.0.0.1:6379> geohash china:city tianjin wuhan # 将二维的经纬度转换为一维的字符串,如果两个字符串越相似,那么距离越近
1) "wwgqd9x4sb0"
2) "wt3mbwzmqj0"
127.0.0.1:6379> zrange china:city 0 -1 # 查看所有的名称
1) "wuhan"
2) "tianjin"
3) "beijing"
127.0.0.1:6379> zrem china:city wuhan # 删除名称为wuhan的数据
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1 # 再次查询所有的名称
1) "tianjin"
2) "beijing"

2.Hyperloglog

什么是基数?不重复的元素

例如: A(1,3,5,7,8,7)B(1,3,5,7,8) 基数为5,可以接受误差!

Redis Hyperloglog是基数统计的算法!

优点:占用的内存是固定的,2^64不同的的元素的技术,只需要耗费12KB内存,如果要从内存角度来比较的话Hyperloglog首选!

网页的UV(一个人访问一个网站多次,但是还是算作一个人!)

传统的方式,set保存用户的id,然后就可以set中的元素数量作为标准判断!

这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id

0.81% 的错误率!统计UV任务,可以忽略不计的!

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 # 查看基数数量(不重复的元素)
(integer) 10
127.0.0.1:6379> pfadd mykey2 i j z x  #创建mykey2
(integer) 1
127.0.0.1:6379> pfcount mykey2  # 查看基数数量
(integer) 4
127.0.0.1:6379> type mykey 
string
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 # 将mykey和mykey2合并为mykey3,并集
OK
127.0.0.1:6379> pfcount mykey3 # 查看mykey3(是mykey和mykey2的并集)的基数数量
(integer) 12

如果允许容错,那么一定可以使用Hyperloglog!

如果不允许容错,那么就使用set或者自己的数据类型即可!

3.Bitmaps

位存储

使用场景:

统计疫情感染人数:0 1 0 1

统计用户的活跃信息,活跃,不活跃

登录、未登录

上班打卡、未打卡

两个状态的都可以使用Bitmaps!Bitmaps位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态!

365天 = 365bit;1字节 = 8bit,大约也就46个字节左右(省内存)

127.0.0.1:6379> setbit sign 0 1 # 第一个0代表星期一,第二个1是代表已打卡,创建
(integer) 0
127.0.0.1:6379> setbit sign 1 1 # 第一个1代表星期二,第二个1是代表已打卡
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 第一个2代表星期三,第二个1是代表已打卡
(integer) 0
127.0.0.1:6379> setbit sign 3 1 # 第一个3代表星期四,第二个1是代表已打卡
(integer) 0
127.0.0.1:6379> setbit sign 4 1 # 第一个4代表星期五,第二个1是代表已打卡
(integer) 0
127.0.0.1:6379> setbit sign 5 1 # 第一个5代表星期六,第二个1是代表已打卡
(integer) 0
127.0.0.1:6379> setbit sign 6 0 # 第一个6代表星期天,第二个0是代表未打卡
(integer) 0
127.0.0.1:6379> getbit sign 3 # 获取数字3代表的是星期四那天的打卡状态
(integer) 1
127.0.0.1:6379> getbit sign 6 # 获取数字6代表的是星期天那天的打卡状态
(integer) 0
127.0.0.1:6379> bitcount sign # 获取状态为1(已打卡)的天数,一般可用于全勤统计
(integer) 6

五、事务

Redis事务本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!一次性、顺序性、排他性!执行一些列的命令!

------ 队列 set set set 执行 ------

Redis事务没有隔离级别的概念!

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

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> set k3 v3
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"

放弃事务!

127.0.0.1:6379> clear127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k4 v4 # 命令入队
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> set k1 hh
QUEUED
127.0.0.1:6379> discard # 取消事务
OK
127.0.0.1:6379> get k4 # k4未录入,取消事务成功
(nil)
127.0.0.1:6379> get k1 # 事务队列中的命令都不会被执行
"v1"

编译型异常(代码有问题!命令有错!),事务中所有的命令都不会被执行!

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> getset k2 # 故意写个错误的命令(正确的写法是getset key value)
(error) ERR wrong number of arguments for 'getset' command 
127.0.0.1:6379> exec # 执行事务
(error) EXECABORT Transaction discarded because of previous errors. # 事务异常报错信息

运行时异常(1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令可以正常执行,错误命令会抛出异常信息

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1 # 命令入队
QUEUED
127.0.0.1:6379> incr k1 # k1的值是v1,不是一个数字,incr(自增+1)没法执行,但由于这不是编译型异常,命令正确,不会报错
QUEUED # 命令成功入队
127.0.0.1:6379> exec # 执行事务
1) OK
2) (error) ERR value is not an integer or out of range # 事务报错信息
127.0.0.1:6379> get k1 # 其他事务照常执行
"v1"

监控(Watch)面试常问

悲观锁

  • 很悲观,什么时候都会出问题,无论做什么都会加锁

乐观锁

  • 很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 获取version
  • 更新的时候比较version

Redis监视测试

正常执行成功

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可以当做redis的乐观锁操作

线程1:
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 # 开启监视
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 # 命令入队,这个时候线程2执行了
QUEUED
127.0.0.1:6379> exec # 由线程2回来,开始执行事务
(nil)  # 执行失败

线程2:
127.0.0.1:6379> get money # 由于线程1的事务还没开始执行,所以money数值没变
"100"
127.0.0.1:6379> get out
"0"
127.0.0.1:6379> incrby money 20 # 执行money+20,再回到线程1去开始执行事务
(integer) 120

如果修改失败,获取最新的值就好

127.0.0.1:6379> unwatch # 如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> watch money  # 获取最新的值,再次监视,类似于MySQL数据中乐观锁的select version
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 30
QUEUED
127.0.0.1:6379> incrby out 30
QUEUED
127.0.0.1:6379> exec # 比对监视的值是否发生了变化,如果没有变化,那么可以执行成功,如果变化了就执行失败
1) (integer) 90
2) (integer) 30

注意:每次提交执行exec后都会自动释放锁,不管是否成功

六、Jedis

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

<dependencies>    
	<!--导入jedis的包-->    
	<dependency>        
		<groupId>redis.clients</groupId>        
		<artifactId>jedis</artifactId>        
		<version>3.3.0</version>    
	</dependency>
	
	<!--导入fastjson的包-->    
	<dependency>        
		<groupId>com.alibaba</groupId>        
		<artifactId>fastjson</artifactId>        
		<version>1.2.75</version>    
	</dependency></dependencies>
//测试
public class TestPing {    
	public static void main(String[] args) {        
		//创建jedis对象        
		Jedis jedis = new Jedis("127.0.0.1",6379);        
		//测试,和redis命令一样,会返回pong        
		System.out.println(jedis.ping());    
	}
}
//事务
public class TestTX {    
	public static void main(String[] args) {        
		Jedis jedis = new Jedis("127.0.0.1",6379);        
		Transaction multi = jedis.multi();        
		JSONObject jsonObject = new JSONObject();        
		jsonObject.put("hello", "world");        
		jsonObject.put("name","zhangsan");        
		String jsonString = jsonObject.toJSONString();        
		try {            
			multi.set("user3",jsonString);            
			multi.set("user4",jsonString);            
			//执行事务            
			System.out.println(1/0);            
			multi.exec();        
		} catch (Exception e) {            
			//放弃事务            
			multi.discard();        
		} finally {            
			System.out.println(jedis.get("user3"));            
			System.out.println(jedis.get("user4"));            
			jedis.close();        
		}    
	}
}

七、SpringBoot整合

SpringBoot操作数据:spring-data jpa jdbc mongodb redis

SpringData也是和SpringBoot齐名的项目!

说明:在SpringBoot2.X之后,原来使用的jedis被替换为了lettuce?

jedis:采用直连,多个线程操作是不安全的,如果想要避免不安全,使用jedis pool连接池!更像BIO模式

lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据,更像NIO模式

SpringBoot中自动配置的原理:整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并在spring.factories中也一定能找到这个类的全限定名,如图:

image-20211019154110096

同时,一定存在一个RedisProperties类

image-20211019154812989

RedisAutoConfiguration:Spring Boot中自带的Redis的配置类的源码

@Configuration(proxyBeanMethods=false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {	
	@Bean	
	@ConditionalOnMissingBean(name = "redisTemplate") 
	//如果找不到已存在的redisTemplate,那么再使用这个,所以这个方法可以自定义一个	
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)	
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {        
		//默认的RedisTemplate没有过多的设置,redis对象需要序列化       	
		//两个泛型都是object,object类型,我们使用时需要强制转换<String,Object>		
		RedisTemplate<Object, Object> template = new RedisTemplate<>();		
		template.setConnectionFactory(redisConnectionFactory);		
		return template;	
	}	
	@Bean	
	@ConditionalOnMissingBean	
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)	
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {        
		//由于在开发中string类型用的多,所以单独从redisTemplate中提取出来。		
		StringRedisTemplate template = new StringRedisTemplate();		
		template.setConnectionFactory(redisConnectionFactory);		
		return template;	
	}
}

新建的springboot项目写好测试后报错:TestEngine with ID 'junit-jupiter' failed to discover tests

解决方法:在pom.xml中添加如下依赖:

<dependency> 
<groupId>org.junit.jupiter</groupId> 
<artifactId>junit-jupiter-api</artifactId> 
<version>5.7.0</version> 
<scope>test</scope>
> ```
@SpringBootTestclass 
Redis02SpringbootApplicationTests {    
	@Autowired    
	private RedisTemplate redisTemplate;    
	@Test    
	void contextLoads() {        
		redisTemplate.opsForGeo();        
		redisTemplate.opsForHyperLogLog();        
		redisTemplate.opsForValue().set("age",12);        
		System.out.println(redisTemplate.opsForValue().get("age"));    
	}
}

自定义RedisConfig

@Configurationpublic class RedisConfig {
	@Bean    
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {        
		RedisTemplate<String, Object> template = new RedisTemplate<>();        
		template.setConnectionFactory(redisConnectionFactory);        
		return template;    
	}
}

在使用自定义RedisConfig时遇到的问题如下

  • 当存储的为Json数据格式时,在Redis中Json数据会乱码,但是在程序中可以正常输出。

  • 当存储的为Java对象时,会报如下错误。

因此,需要将Java对象进行序列化,可以解决以上遇到的第二个问题

image-20211021180840310

如图,官方提供的序列化方式为jdk,我们在自定义的RedisConfig中指定序列化方法(Jackson序列化),可以解决乱码问题(以上遇到的问题一)

image-20211021184023294

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

在网上找了一个Redis工具类用于实际开发

官方GitHub项目,点击跳转

如果GitHub访问不了,可以跳转到复制版,点击跳转

八、Redis.conf详解

配置文件 unit单位 对大小写不敏感

image-20211021203222936

包含,类似于js的include标签,可以引入别的redis配置文件

image-20211021203438029

网络

bind 127.0.0.1 # 绑定的ip,如果注释掉,就没有请求访问ip限制了
protected-mode yes # 保护模式,设置成 no 即可开启远程访问

通用 GENERAL

daemonize yes # 设置为yes表明要在后台运行

pidfile /var/run/redis_6379.pid # 如果以后台的方式运行,那么需要指定一个pid文件

# 日志
# Specify the server verbosity level.
# This can be one of:
# 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)
loglevel notice

logfile “” # 日志的文件位置名

databases 16 # 默认的16个数据库

always-show-logo yes # 是否总是显示LOGO

快照

持久化,在规定的时间内,执行了多少次操作,则会持久化到文件.rdb .aof

redis是内存数据库,如果没有持久化,那么数据断电即失!

save 900 1  # 如果900s内,至少有一个key进行了修改,立即进行持久化操作
save 300 10 # 如果300s内,至少有10个key进行了修改,立即进行持久化操作
save 60 10000 # 如果60s内,至少有10000个key进行了修改,立即进行持久化操作
# 之后学习持久化,会自己定义这个测试!
stop-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作
rdbcompression yes # 是否压缩rdb文件,需要消耗一些cpu资源
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验
dir ./  # rdb文件保存的目录

REPLICATION复制,我们后面讲解主从复制的时候再进行讲解

SECURITY 安全

# 设置密码,redis默认没有密码
requirepass foobared # 去掉注释,并修改为所需要的密码:`requirepass myPassword` (其中myPassword就是要设置的密码,例如:
requirepass 654321

# 当然也可以通过命令查看和设置密码
config get requirepass # 查看密码
config set requirepass “654321” # 设置密码

auth 654321 # 使用密码进行登录

限制 CLIENTS

maxclients 10000  # 设置能连接上redis的最大客户端连接数量
maxmemory <bytes> # redis配置最大的内存容量
maxmemory-policy noeviction # 内存到达上限之后的处理策略(移出一些过期的key,报错,……)

主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:

针对设置了过期时间的key做处理:

  • volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。【删除即将过期的key】
  • volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。【随机删除即将过期的key】
  • volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。【使用LRU算法删除即将过期的key】
  • volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。【使用LFU算法删除即将过期的key】

针对所有的key做处理:

  • allkeys-random:从所有键值对中随机选择并删除数据。【所有的数据集中随机删除数据,非常不靠谱的方式】
  • allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。【所有的数据集中筛选部分使用LRU算法删除key】
  • allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。【所有的数据集中筛选部分使用LFU算法删除key】

不处理:

  • noeviction:(默认使用该策略)不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。

LRU和LFU的区别:

LRU 算法(Least Recently Used,最近最少使用)

淘汰很久没被访问过的数据,以最近一次访问时间作为参考。

LFU 算法(Least Frequently Used,最不经常使用)

淘汰最近一段时间被访问次数最少的数据,以次数作为参考。

APPEND ONLY MODE(aof配置)

appendonly no # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof" # 持久化的文件的名字# 
appendfsync always  # 每次修改都会sync,消耗性能
appendfsync everysec  # 每秒执行一次sync,可能会丢失这1s的数据# 
appendfsync no  	  # 不执行sync,这个时候操作系统自己同步数据,速度最快

九、Redis持久化

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

1.RDB(Redis Databases)

(1)什么是RDB?

在指定时间间隔后,将内存中的数据集快照写入到数据库;在恢复的时候,直接读取快照文件进行数据恢复

默认情况下,Redis将数据库快照保存在名字为dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。

(2)工作原理

在进行RDB的时候,redis的主线程是不会做io操作的,主线程会fork一个子线程来完成该操作;

  • Redis调用forks,同时拥有父进程和子进程
  • 子进程将数据集写入到一个临时RDB文件中
  • 当子进程完成对新RDB文件的写入时,Redis用新RDB文件替换原来的RDB文件,并删除旧的RDB文件

这种工作方式使得Redis可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求)

(3)触发机制

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

save

使用save命令,会立刻对当前内存中的数据进行持久化,但是会阻塞,也就是不接受其他操作了

由于save命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save命令执行速度会非常慢,阻塞所有客户端的请求

flushall

flushall命令也会触发持久化

触发持久化规则

满足配置条件中的触发条件

可以通过配置文件对Redis进行设置,让它在“N秒内数据集至少有M个改动”这一条件被满足时,自动进行数据集保存操作

# 持久化规则,持久化到文件 .rdb .aof
# 如果在900s内,至少有1个key进行了修改,就进行持久化
save 900 1
# 如果在300s内,10个key进行了修改,就进行持久化
save 300 10
# 如果在10000s内,60个key进行了修改,就进行持久化
save 60 10000

bgsave

bgsave是异步进行,进行持久化的时候,redis还可以将继续响应客户端请求

bgsave和save对比

命令 save bgsave
IO类型 同步 异步
阻塞? 是(阻塞发生在fock(),通常非常快)
复杂度 O(n) O(n)
优点 不会消耗额外的内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要fock子进程,消耗内存

(4)优缺点

优点

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

缺点

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

2.AOF(Append Only File)

(1)什么是AOF?

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

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

快照功能(RDB)并不是非常耐久(durable):如果Redis因为某些原因而造成故障停机,那么服务器将丢失最近写入以及未保存到快照中的那些数据。

从1.1版本开始,Redis增加了一种完全耐久的持久化方式:AOF持久化。

(2)开启AOF

如果要使用AOF持久化,需要修改配置文件:

appendonly no # 默认使用rdb方式持久化,将`no`改为`yes`则表示开启aof,重启redis生效
appendfilename "appendonly.aof" # 默认文件名

如果这个aof文件有错误,这时候redis是启动不起来的,我需要修复这个aof文件,redis给我们提供了一个工具:redis-check-aof --fix

image-20211025085544078

重写规则说明

aof默认就是文件的无限追加,文件会越来越大!如果aof文件大于64m,太大了!fork一个新的进程来将我们的文件进行重写!

image-20211025090719526

(3)优缺点

优点

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

缺点

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

3.RDB和AOP选择

(1)RDB和AOF对比

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

(2)如何选择使用哪种持久化方式

一般来说,如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能

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

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

十、Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

Redis客户端可以订阅任意数量的频道

订阅/发布消息图:

image-20211024172010684

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

pubsub1

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

pubsub2

Redis 发布订阅命令

这些命令被广泛用于构建即时通信应用,比如网络聊天室(chaatroom)和实时广播、实时提醒等

image-20211024172934791

测试

订阅端:

127.0.0.1:6379> SUBSCRIBE xiaoliangshuo # 订阅一个频道 xiaoliangshuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "xiaoliangshuo"
3) (integer) 1
# 等待读取推送的信息
1) "message" # 消息
2) "xiaoliangshuo" # 频道的名称
3) "hello" # 频道的消息
1) "message" 
2) "xiaoliangshuo"
3) "world"

发送端:

127.0.0.1:6379> publish xiaoliangshuo "hello"
(integer) 1
127.0.0.1:6379> publish xiaoliangshuo "world"
(integer) 1

原理

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

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

缺点

  • 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。

  • 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。

应用

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

稍微复杂的场景,我们就会使用消息中间件MQ处理。

十一、Redis主从复制

image-20211025151529561

主从复制,读写分离!80%的情况都是在进行读操作。减缓服务器的压力,架构中经常使用,最少都需要一主二从!

什么是主从复制

主从复制,就是指一台redis服务器上的数据复制到其他的redis服务器上。前者称为主节点(master/leader),后者称为从节点(slave/follower)。数据的复制是单向的,只能由主节点到从节点。主节点写,从节点读。

默认情况下,每台redis服务器都是主节点,且一个主节点可以有多个从节点或没有从节点,但一个从节点只能有一个主节点。

主从复制的作用:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
  • 故障恢复:主节点出现问题后,可以由从节点提供服务,实现快速的故障恢复。实际上是一种服务冗余
  • 负载均衡:主从复制中,配合读写分离,可以由主节点提供写服务,从节点提供读服务,分担服务器负载,可以大大提高redis服务器的并发量
  • 高可用(集群)基石:主从复制还是哨兵和集群能够实施的基础

一般来说,将Redis运用于工程项目,只使用一台redis是万万不能的,原因如下:

  • 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
  • 从容量上,单个redis服务器容量有限,一般来说,单台redis的最大使用内存不应该超过20G

环境配置

只配置从库,不配置主库(因为redis默认就是主库)

1

配置配置文件,先拷贝三份

2

修改配置文件(三个配置文件都要改(79、80、81)

1:修改端口

2:开启守护线程模式

3:修改pidfile

4:修改logfile

5:修改dbfilename

修改完毕之后,启动三个redis服务器,可以通过进程信息进行查看

3

slaveof host port # 配置从机

4

我们这里使用的是命令,是暂时的。真实的主从配置是在配置文件中配置的(如下图),这样的话才是永久的。

image-20211025191549836

细节

主机可以写,从机不能写只能读!主机中的所有信息和数据都会自动被从机保存。

主机:

5

从机:

6

没有配置哨兵的话,即使主机宕机,从机所属的主机依然不会改变。这个时候如果主机恢复了,从机依然可以获取主机写入的信息

如果是使用命令行配置的主从,这个时候如果重启了,就会变回主机。只要变回从机,就会立马从主机中获取值

复制原理

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

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

全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中(slave获取master的所有数据)

增量复制:master继续将新的所有收集到的修改命令依次传给slave,完成同步(master新增的修改命令传给slave)

只要是重新连接master,一次完全同步(全量复制)将被自动执行!我们的数据一定可以在从机中看到!

层层链路

上一个M连接下一个S!

7

这个时候也可以完成主从复制!(但是这种方法工作中不会使用)

如果没有老大了,这个时候能不能选择一个老大出来呢?手动&自动

命令设置(手动)

slaveof no one # 如果主机断开了连接,可以使用这个命令让自己变成主机!其他节点就可以手动连接到最新的这个主节点!如果这个时候原来的老大修复了,也无法当之前的主节点了

8

哨兵模式(自动)

概述

主从切换技术的方法是:当主服务器宕机后,需要手动把一台服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供Sentinel(哨兵)架构来解决这个问题。

谋朝篡位的自动版,能够监控主机是否故障,如果故障了根据投票数自动将从库转化为主库

哨兵模式是一种特殊的模式,首先redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。原理是:哨兵通过发送命令,等待redis服务器响应,从而监控多个redis示例。

单机哨兵

9

这里的哨兵有两个作用:

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

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

多哨兵模式

10

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover(重新选举),仅仅是哨兵1主观认为主服务器不可用,称为主观下线。当后面的哨兵也检测到主服务器不可用,并且打到一定数量是,那么哨兵就会进行一次投票,投票的结果由一个哨兵发起,进行failover【故障转移】操作。切换成功后,通过发布订阅模式,让各个哨兵把自己监控的从服务器切换成主服务器,这个过程称为客观下线

开启哨兵模式

步骤:

  • 创建一个哨兵配置文件(sentinel.conf)

  • 使用vim编辑sentinel.conf

    11

  • 配置文件

    sentinel monitor 被监控的主机名称 host port 1  # 1代表主机挂了,要进行重新选举
    

    12

  • 启动哨兵

    redis-sentinel kconfig/sentinel.conf # 启动哨兵模式
    

13

主机宕机后,会重新选择主机。这时候,即使主机回来,也只能当新主机的从机

优缺点

优点:

  • 哨兵集群,基于主从复制模式,所有的主从配置的优点,它都有
  • 主从可以切换、故障可以转移,可用性高
  • 哨兵模式是主从模式的升级,手动到自动,更加健壮

缺点:

  • redis不好在线扩容,集群数量一旦打到上限,扩容就十分麻烦
  • 实现哨兵模式的配置其实很麻烦,里面有很多选择

哨兵模式的全部配置

# Example   sentinel.conf
# 哨兵sentinel实例运行的端口   默认是26379,如果有哨兵集群,我们还需要配置每个哨兵端口
port 26379

#哨兵sentinel的工作目录
dir /tmp

# 哨兵 sentine1 监控的redis主节点的 ip port   
# master-name  ,可以自己命名的主节点名字 只能由字母A-Z、数字0-9、这三个字符"  .   -  _ "组成。
# quorum配置多少个sentine1哨兵统- -认为master主节点失联那么这时客观上认为主节点失联了
# sentine1 monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster   127.0.0.1   6379   2

#当在Redis实例中开启了requirepass foobared 授权密码这样所有连接kedis实例的客户端都要提供密码
#设置哨兵sentinel连接主从的密码注意必须为主从设置- - 样的验证密码
# sentine1 auth-pass <master-name> <password>
sentine1 auth-pass mymaster MySUPER--secret-0123passwOrd

#指定多少毫秒之后主节点没有应答哨兵sentine1 此时哨兵主观上认为主节点下线默认30秒
# sentinel down-after-mi 11i seconds <master-name> <mi 11iseconds>
sentine1 down-after-mi 11iseconds mymaster 30000

#这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成fai lover所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为replication而 不可用。可以通过将这个值设为1来保证每次只有一个slave处于不能处理命令请求的状态。
# sentine1 paralle1-syncs <master-name> <numslaves>
sentine1 paralle1-syncs mymaster 1

#故障转移的超时时间failover-timeout 可以用在以下这些方面:
#1.同一个sentine1对同一 个master两次fai lover之间的间隔时间。
#2.当一个slave从一 个错误的master那里同步数据开始计算时间。直到s1ave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有s1aves指向新的master所需的最大时间。不过,即使过了这个超时,slaves 依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
#默认三分钟
# sentine1 failover-timeout <master-name> <milliseconds>
sentine1 fai lover-ti meout mymaster 180000

# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被-一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentine1有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等 方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一 个是事件的类型,一个是事件的描述。如果sentine1. conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentine1无法正常启动成功。
#通知脚本
# she11编程
# sentine1 notification-script <master-name> <script-path>
sentine1 notificati on-script mymaster /var/redis/notify. sh

#客户端重新配置主节点参数脚本
#当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
#以下参数将会在调用脚本时传给脚本: 
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#目前<state>总是“failover",
# <role>是“Teader"或者"observer"中的-一个。
#参数from-ip, from-port, to-ip,to-port是用来和旧的master和新的master(即旧的s lave)通信的
#这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentine1 client-reconfig-script <master-name> <script-path>
sentine1 client-reconfig-script mymaster /var/redis/reconfig.sh #一般都是由运维来配置!

十二、Redis缓存穿透和雪崩

面试高频,工作常用(服务的高可用问题)

缓存穿透(查不到)

image-20211025211840434

概念

用户查询一个数据,发现redis数据库没有,也就是缓存没有命中,于是向持久层数据库查询,发现也没有,于是此次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求数据库,这会给持久层数据库造成很大压力,也就相当于出现了缓存穿透。

解决方案

(1)布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。

15

(2)缓存空对象

当存储层不命中后,即便返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源

16

这种方法会存在两个问题:

  1. 存储空值会消耗大量的空间
  2. 即使对控制设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间的窗口不一致,这对需要保存一直性的业务会有影响

缓存击穿(量太大、缓存过期导致的)

概念

缓存击穿是值一个key非常热点(如微博热搜),在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿透缓存,直接请求数据库。这就像在一个屏障上凿开了一个洞。

解决方案

(1)设置热点数据永不过期

从缓存层面来说,没有设置过期时间,就不会出现热点key过期后产生的问题

(2)加互斥锁

分布式锁:使用分布式锁,保证每个key同时只有一个线程去查询后端服务(后端数据库),其他key没有获得分布式锁的权限,因此只需等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大

image-20211025213226629

缓存雪崩(缓存集中过期、Redis宕机)

概念

在某个时间段,缓存集中过期失效(redis宕机也会导致)。

其实缓存集中过期不是非常致命,而是缓存服务器某个节点宕机或者断网才是致命的,很有可能瞬间就把数据库压垮

双十一:停掉一些服务(例如:退款),保证主要的服务可用!

image-20211025213712506

解决方案

(1)redis高可用

多设几台redis,即使一台挂掉之后其他的也可以继续工作。其实就是搭建集群(异地多活)

(2)限流降级

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

(3)数据预热

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

posted @ 2021-10-25 22:01  intTom  阅读(40)  评论(0编辑  收藏  举报