Redis学习笔记

Redis学习

主要知识点

  • nosql介绍
  • nosql数据类型
  • nosql 四大分类
  • CAP
  • BASE
  • Redis入门
  • 五大基本数据类型
    • String
    • List
    • Set
    • Hash
    • Zset
  • 三种特殊的数据类型
    • geo
    • hyperloglog
    • bitmap
  • Redis配置详解
  • Redis持久化
    • RDB
    • AOF
  • Redis事务操作
  • Redis实现订阅发布
  • Redis实现主从复制
  • Redis哨兵模式(重点的重点)
  • 缓存穿透及解决方案(重点的重点)
  • 缓存击穿及解决方案(重点的重点)
  • 缓存雪崩及解决方案(重点的重点)
  • 基础API之Jedis详解
  • Springboot集成Redis
  • Redis实战分析

为什么要用NoSQL

由于现在是大数据时代,我们单靠MySQL和MySQL集群已经解决不了现在的海量数据存储了,不论对索引优化的多好,也很那解决现在百亿条数据的读写,所以NoSQL出现了。

纵观互联网历史

第一个时代:单机MySql的年代

Redis学习笔记_第1张图片

在当时那个年代,基本上都是静态的网站,访问量很少,所以一个单机mysql就可以了。DAL-数据库访问层

但是这样的网站有瓶颈:

  • 数据量太大,一个机器放不下

  • 数据的索引,单个数据库超过300万就必须建立索引,但是索引太大,单机也放不下 索引(B+ tree)

  • 访问量,一个服务器承受不了

第二个时代:有了缓存的存在Memcached(高速缓存插件) + MySQL + 垂直拆分 读写分离

一个网站基本都是在做读操作,每次都去操作数据库的时候就很浪费很麻烦,所以我们做一个缓存,将第一次查到的内容放到缓存里面,第二次查的时候直接从缓存里面拿,这样就快很多,减少了数据库服务器的压力。
Redis学习笔记_第2张图片

分库分表 + Mysql集群 + 水平拆分 集群主从复制

MySQL的存储引擎:

早些年是:MyISAM:表锁,即当我们访问一条记录的时候会把整个表锁住,当我们的表中有一百万条数据的时候十分影响效率,高并发下会出现严重的锁问题。

现在是InnoDB:行锁,每次查询只锁一行

慢慢的就是分库分表来解决写的压力

Redis学习笔记_第3张图片
BOSN就是JSON的二进制格式

这时节nosql就可以很好地解决上面的问题。处理海量数据

NoSQL不仅仅是sql!!!

Map就是典型的nosql的特点因为Map键值对的形势,可以存储任何形式的值。

NoSQL的特点

  • 方便解耦
  • 方便扩展 因为数据之间没有关系,
  • 大数据量的高性能 (redis 一秒可以写8万次,读取11万)
  • 数据类型是多样的 不需要事先设置表结构 因为是键值对的形式,随取随用

CAP定理 和 BASE理论 – 异地多活 搞明白这些就是初级架构师了!!

3V 和 3H

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

  • 海量 volume
  • 实时 variety
  • 多样 velocity

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

  • 高并发
  • 高性能
  • 高可扩 随时拆分,机器不够了可以随时增加机器来提高性能

现在是Nosql和rdbms一起使用
Redis学习笔记_第4张图片
Redis学习笔记_第5张图片

一个网站的信息存储架构

  • 商品的基本信息
    • 名称,价格,商家信息,这些都是存储在sql中的,也就是关系型数据库,阿里云用的是mysql,但是他这个MySQL和我们的MySQL并不一样,他们将MySQL底层重构了~~
  • 商品的评论,描述(文字较多)
    • 文档数据库中 mongoDB
  • 图片
    • 分布式存储系统 FastDFS
    • 淘宝自己的 TFS
    • Google的 GFS
    • Hadoop HDFS
    • 阿里云的 oss
  • 商品的关系字 即搜素引擎
    • 阿里云用的是自己研发的 ISerach: 是多隆大佬一高兴就研发出来的。
  • 商品热门的波段信息
    • 内存数据库
    • redis
  • 商品的交易 支付接口
    • 第三方接口 支付宝 微信等API

一个网页的背后很复杂。

Redis学习笔记_第6张图片

NoSQL的四大分类

Redis是单线程的

  • 键值对存储
    • 美团:redis + Tair
    • 新浪:redis
  • 文档存储
    • MongoDB是非关系型数据库中功能最丰富的,最像关系型数据库的一种nosql。
  • 列存储
    • HBase
    • 分布式文件系统
  • 图形关系数据库
    • 他不是用来存储图形的,它是用来存储拓扑关系的。
    • 社交推荐,广告推荐
      Redis学习笔记_第7张图片

追求幸福,探索未知

Redis入门

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。

Redis能干吗?

  • 内存存储,数据持久化
  • 效率高,可用作高速缓存
  • 简单的发布订阅系统–可实现简单的消息队列的功能
  • 地图信息分析
  • 计时器,计数器(浏览量)

redis默认端口号:6379
Redis学习笔记_第8张图片

Redis 性能测试

Redis 性能测试是通过同时执行多个命令实现的。

语法

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

redis-benchmark [option] [option value]

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

Redis学习笔记_第9张图片
Redis学习笔记_第10张图片
Redis学习笔记_第11张图片
每秒处理87642条请求,速度不畏不快!!!

Redis的基础知识

默认的16个数据库

在这里插入图片描述
Redis学习笔记_第12张图片

# 切换数据库
select 数字(0-15)
# 查询此数据库中所有的键的名称
keys * 
# 清除当前数据库 
flushdb 
# 清除全部的数据库的内容 16个数据库全部清除
flushall
# 判断键是否存在
exists 键名
# 迁移键 将本数据库的键迁移到其他的数据库 最后面的数字指的就是要迁到的数据库名
move 键名 1
# 设置过期时间 键名+时间(秒)
expire name 10
# 查看一个键还有多少秒过期
ttl 键名
# 查看类型
type 键名

Redis是单线程的

redis的瓶颈不是cpu而是服务器的内存和网络带宽

Redis是单线程的,为什么还这么快?

  • 高性能的服务器不一定是多线程的
  • 多线程不一定比单线程快 多线程有上下文切换,比较耗时
  • Redis是将所有的数据放到内存中的

五大数据类型

在这里插入图片描述

String (用的最多)

###########################################################################

# 给String追加字符串  如果键名不存在则新建一个 相当于set key
append 键名 "hello"
# 字符串长度
strlen 键名
# 自增1
incr 键名
# 自减1
decr 键名
# 增加任意数值
incrby 键名 数值
# 减去任意数值
decrby 键名 数值

###########################################################################

# 截取字符串的某个范围内的字符
getrange 键名 start end
getrange 键名 0 -1 # 查看全部的字符串
# 替换某个字符串
setrange 键名 开始替换的位置 替换的内容
setrange address 3 wangjiatun

###########################################################################

# 设置键值对,顺便设置过期时间
setex 键名 过期时间 内容
# 如果该值存在则返回0,不覆盖原来的内容 如果不存在则创建 分布式锁中会用到
setnx 键名 内容

###########################################################################

# 一次性创建多个键值对 
mset k1 v1 k2 v2 k3 v3
# 原子性的操作,要们都成功 要么都失败
msetnx ..

# 类似于对象的操作
mset user:1:name zhansgan user:1:age 21 # 存储
mget user:1:name user:1:age # 读取

# 如果不存在值 则返回nil  如果存在值,获取原来的值,设置新的值
getset 键名 内容

##########################################################################

List (可做消息队列)

在redis里面我们可以将list做成栈,队列等。

所有的list命令都是以l开头的

##########################################################################

lpush list one  # 将一个值放入list 放在头部
(integer) 1
127.0.0.1:6379[3]> lpush list two
(integer) 2
127.0.0.1:6379[3]> lpush list two
(integer) 3
127.0.0.1:6379[3]> lpush list two
(integer) 4
127.0.0.1:6379[3]> lrange list 0 -1 # 将list中的所有的值输出
1) "two"
2) "two"
3) "two"
4) "one"
127.0.0.1:6379[3]> lrange list 1 2 # 输出指定的范围的list
1) "two"
2) "two"
127.0.0.1:6379[3]> rpush list ahhh # 放在尾部
(integer) 5
127.0.0.1:6379[3]> lrange list 0 -1
1) "two"
2) "two"
3) "two"
4) "one"
5) "ahhh"
127.0.0.1:6379[3]> lrange list 0 -1 
1) "wangge"
2) "two"
3) "two"
4) "two"
5) "one"
6) "ahhh"
127.0.0.1:6379[3]> lpop list # 在左边移除 也就是第一个元素 头元素
"wangge"
127.0.0.1:6379[3]> lrange list 0 -1
1) "two"
2) "two"
3) "two"
4) "one"
5) "ahhh"
127.0.0.1:6379[3]> rpop list # 在右边移除 也就是最后一个元素 尾元素
"ahhh"
127.0.0.1:6379[3]> lrange list 0 -1
1) "two"
2) "two"
3) "two"
4) "one"
127.0.0.1:6379[3]> lindex list 0 # 取出list中的某一个位置的元素
"two"

# 获取list的长度
llen list
# 移除指定的值
lrem key 移除几个 移除的内容
lrem list 3 two

127.0.0.1:6379[3]> lrange list 0 -1
1) "hello4"
2) "hello3"
3) "hello2"
4) "hello1"
127.0.0.1:6379[3]> ltrim list 1 2 # 截取第二个到第三个 其余的删除
OK
127.0.0.1:6379[3]> lrange list 0 -1
1) "hello3"
2) "hello2"

##########################################################################

# rpoplpush 将这个集合的元素移到另一个集合中 移除列表的最后一个元素并将他添加到一个集合的第一个元素的位置
# 将指定下标的值替换为另外一个值 相当于更新操作  如果不存在列表就会报错,,存在则更新
lset list 0 "dads"

##########################################################################

# 在某个值的前面或者是后面插入一个值
linsert list before "world" "hhh"
linsert list after "world" "kkk" # 后面

##########################################################################

list实际上是一个链表,左边右边都可以插入

如果key不存在 创建新的链表

如果key存在 新增内容

在两边插入或者是改动值效率最高,如果链表很长,执行中间元素效率很低。

消息队列,Lpush,Rpop ,栈,

Set (无序不重复)

set的值是不可以重复的。

##########################################################################

127.0.0.1:6379[3]> sadd set "hello" # set集合中添加元素
(integer) 1 
127.0.0.1:6379[3]> sadd set "world" 
(integer) 1
127.0.0.1:6379[3]> sadd set "and"
(integer) 1
127.0.0.1:6379[3]> smembers set # 查看set的所有的内容
1) "and"
2) "world"
3) "hello"
127.0.0.1:6379[3]> sismember set add
(integer) 0
127.0.0.1:6379[3]> sismember set and # 查看该key是否在该set中存在 ismember判断 1存在 0不存在在
(integer) 1
127.0.0.1:6379[3]> scard set # 获取该集合的元素个数
(integer) 3

##########################################################################

127.0.0.1:6379[3]> srem set and # 移除指定的元素
(integer) 1
127.0.0.1:6379[3]> smembers set
1) "world"
2) "hello"
127.0.0.1:6379[3]> srandmember set # 随机输出一个元素
"world"
127.0.0.1:6379[3]> srandmember set # 随机输出一个元素
"hello"
127.0.0.1:6379[3]> srandmember set 2 # 随机输出指定个数的元素
1) "hello"
2) "world"

##########################################################################

127.0.0.1:6379[3]> smembers set 
1) "and"
2) "world"
3) "hello"
127.0.0.1:6379[3]> spop set # 随机删除一个元素
"world"
127.0.0.1:6379[3]> spop set
"hello"
127.0.0.1:6379[3]> smembers set
1) "and"

##########################################################################

# 将一个指定的值移动到另一个set集合中
127.0.0.1:6379[3]> smembers set2
1) "zhe"
127.0.0.1:6379[3]> smove set set2 kuang # 移动到另一个集合中
(integer) 1
127.0.0.1:6379[3]> smembers set2
1) "kuang"
2) "zhe"
127.0.0.1:6379[3]> smembers set
1) "and"
2) "world"
3) "hello"

##########################################################################
- 交集
- 并集
- 差集
127.0.0.1:6379[3]> sadd k1 a
(integer) 1
127.0.0.1:6379[3]> sadd k1 b
(integer) 1
127.0.0.1:6379[3]> sadd k1 c
(integer) 1
127.0.0.1:6379[3]> sadd k2 c
(integer) 1
127.0.0.1:6379[3]> sadd k2 d
(integer) 1
127.0.0.1:6379[3]> sadd k2 e
(integer) 1
127.0.0.1:6379[3]> sdiff k1 k2 # 取k1中和k2不同的元素
1) "b"
2) "a"
127.0.0.1:6379[3]> sdiff k2 k1
1) "d"
2) "e"
127.0.0.1:6379[3]> sinter k1 k2 # 取交集  微博中的共同关注,微信QQ的共同好友就是这样实现的
1) "c"
127.0.0.1:6379[3]> sinter k2 k1
1) "c"
127.0.0.1:6379[3]> sunion k1 k2 # 取并集
1) "c"
2) "d"
3) "b"
4) "a"
5) "e"

##########################################################################

QQ中的共同好友是怎么实现的呢?

就是用set,你所有的好友都放在一个set里面,因为QQ号是唯一的,刚好能发挥set的作用,然后将你的好友的所有的好友也放在一个set里面,然后对这两个set集合取交集,取得的交集就是你们两个的共同好友!!

六度分隔(Six Degrees of Separation)理论。简单地说:“你和任何一个陌生人之间所间隔的人不会超五个,也就是说,最多通过六个人你就能够认识任何一个陌生人。

Hash (哈希)

想象成Map集合,本质和string类型没有太大区别,只是多加了一层,key是哈希表,里面是字段和value。

##########################################################################

127.0.0.1:6379[3]> hset myhash k1 shi # 设置一个kv键值对放入哈希 类似于向Map集合添加一个键值对
(integer) 1
127.0.0.1:6379[3]> hget myhash k1 # 取出一个哈希表中一个键的值
"shi"
127.0.0.1:6379[3]> hmset myhash k1 wang k2 han # 同时设置多个键值对
OK
127.0.0.1:6379[3]> hmget myhash k1 k2 # 同时取出多个键的值
1) "wang"
2) "han"
127.0.0.1:6379[3]> hgetall myhash # 取出哈希表中所有的键值对
1) "k1"
2) "wang"
3) "k2"
4) "han"

##########################################################################

127.0.0.1:6379[3]> hdel myhash k1 # 删除hash指定的字段
(integer) 1
127.0.0.1:6379[3]> hlen myhash # 获取hash表的字段的数量,即哈希表长度
(integer) 3
127.0.0.1:6379[3]> hexists myhash k2 # 判断某个字段是否存在
(integer) 1

##########################################################################

127.0.0.1:6379[3]> hkeys myhash # 获取所有的字段名
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379[3]> hvals myhash # 获取所有的值
1) "han"
2) "kuang"
3) "jing"

##########################################################################

127.0.0.1:6379[3]> hset myhash count 0
(integer) 1
127.0.0.1:6379[3]> hget myhash count
"0"
127.0.0.1:6379[3]> hincrby myhash count 2 # 自增2
(integer) 2
127.0.0.1:6379[3]> hincrby myhash count 2
(integer) 4
127.0.0.1:6379[3]> hget myhash count
"4"
127.0.0.1:6379[3]> hincrby myhash count -3 # 自减3
(integer) 1
127.0.0.1:6379[3]> hget myhash count
"1"

##########################################################################

127.0.0.1:6379[3]> hsetnx myhash k2 ss # 如果存在不可以设置 存在不可以设置 分布式锁
(integer) 0
127.0.0.1:6379[3]> hsetnx myhash k5 ss
(integer) 1

##########################################################################

Hash更适合存储对象,String更加适合存储字符串

Zset (有序集合)

在set的基础上增加了一个值,set k1 v1 zset k1 score1 v1

##########################################################################

127.0.0.1:6379[3]> zadd set 1 one # 添加一个元素 指定位置 有序
(integer) 1
127.0.0.1:6379[3]> zadd set 2 two 3 three
(integer) 2
127.0.0.1:6379[3]> zrange set 0 -1 # 输出所有的元素 都是按插入的顺序排列好的
1) "one"
2) "two"
3) "three"

##########################################################################
127.0.0.1:6379[3]> zadd salary 2500 wang
(integer) 1
127.0.0.1:6379[3]> zadd salary 5000 liu
(integer) 1
127.0.0.1:6379[3]> zadd salary 12000 shi
(integer) 1
127.0.0.1:6379[3]> zadd salary 3000 zheng
(integer) 1
127.0.0.1:6379[3]> zadd salary 800 han
(integer) 1
127.0.0.1:6379[3]> zrange salary 0 -1
1) "han"
2) "wang"
3) "zheng"
4) "liu"
5) "shi"
127.0.0.1:6379[3]> zrangebyscore salary -inf +inf # 按score字段排序后输出
1) "han"
2) "wang"
3) "zheng"
4) "liu"
5) "shi"
127.0.0.1:6379[3]> zrevrange salary 0 -1 # 从大到小排序
1) "shi"
2) "liu"
3) "zheng"
4) "han"
127.0.0.1:6379[3]> zrevrange salary 0 -1 withscores
1) "shi"
2) "12000"
3) "liu"
4) "5000"
5) "zheng"
6) "3000"
7) "han"
8) "800"
127.0.0.1:6379[3]> zrangebyscore salary -inf +inf withscores # 带上字段的值输出
 1) "han"
 2) "800"
 3) "wang"
 4) "2500"
 5) "zheng"
 6) "3000"
 7) "liu"
 8) "5000"
 9) "shi"
10) "12000"
127.0.0.1:6379[3]> zrangebyscore salary -inf 3000 withscores # 从最小的到3000范围内的人
1) "han"
2) "800"
3) "wang"
4) "2500"
5) "zheng"
6) "3000"
127.0.0.1:6379[3]> zrem salary wang # 删除指定的字段
(integer) 1
127.0.0.1:6379[3]> zcard salary # 获取集合的长度
(integer) 4
127.0.0.1:6379[3]> zcount salary 1 2 # 获取指定区间内的成员数量

##########################################################################

set可以做排行榜,将一些信息放到redis里面然后每天或者是某个时间根据一些权重,放在set里面,定期的刷新一下,so easy~

三种特殊的数据类型 用这些数据结构可以优化程序效率

geospatial 地图位置

应用场景:

  • 附近的朋友
  • 朋友的定位
  • 打车距离计算

查看经纬度的网址: https://jingweidu.bmcx.com/

Redis的Geo可以推算地理位置的信息,两地之间的距离,方圆多少公里之内的人。
Redis学习笔记_第13张图片
他只有如上六个命令。

GEOADD 添加某个城市的经纬度

# 添加一些城市的经纬度坐标 (经度,维度,名称)  longitude : 精度  latitude : 维度
127.0.0.1:6379[3]> geoadd china:city 120.39 36.30 qingdao
(integer) 1
127.0.0.1:6379[3]> geoadd china:city 118.89 31.32 nanjing
(integer) 1
127.0.0.1:6379[3]> geoadd china:city 120.21 30.20 hangzhou 121.48 31.40 shanghai
(integer) 2
127.0.0.1:6379[3]> geoadd china:city 117.86 36.49 zibo
(integer) 1

GEODIST 获取指定城市的经纬度

# 获取指定的城市的经度和维度 取到的一定是一个坐标值
127.0.0.1:6379[3]> geopos china:city shanghai
1) 1) "121.48000091314315796"
   2) "31.40000025319353938"
127.0.0.1:6379[3]> geopos china:city nanjing hangzhou
1) 1) "118.89000087976455688"
   2) "31.3199993839624824"
2) 1) "120.21000176668167114"
   2) "30.19999988833350102"

GEOPOS 返回两个给定位置之间的距离

单位必须是以下之一,默认为米:

  • 为米。
  • 公里为公里。
  • 英里英里。
  • 英尺为英尺。
# 表示从你定义的城市当中取出来计算他们之间的距离 可以指定距离单位
127.0.0.1:6379[3]> geodist china:city nanjing qingdao km
"570.9649"
127.0.0.1:6379[3]> geodist china:city qingdao zibo km # 青岛到淄博的距离
"227.4906"

GEORADIUS 查找附近的人

以给定的经纬度为中心,找出某一半径内的所有的元素微信中附近的人就是这么做的!只不过他们的经纬度很精确!!但是有一点我们附近的朋友不可能全部查询出来,比如说附近有一万个,我们只需要200个就够了。

127.0.0.1:6379[3]> georadius china:city 110 30 1000 km # 查找经纬度为110 30 半径为 1000 km范围之内的所有的城市
1) "hangzhou"
2) "nanjing"
127.0.0.1:6379[3]> georadius china:city 110 30 1000 km withcoord # 返回值带城市的坐标
1) 1) "hangzhou"
   2) 1) "120.21000176668167114"
      2) "30.19999988833350102"
2) 1) "nanjing"
   2) 1) "118.89000087976455688"
      2) "31.3199993839624824"
127.0.0.1:6379[3]> georadius china:city 110 30 1000 km withcoord withdist # 带坐标和距离
1) 1) "hangzhou"
   2) "982.4071"
   3) 1) "120.21000176668167114"
      2) "30.19999988833350102"
2) 1) "nanjing"
   2) "862.8969"
   3) 1) "118.89000087976455688"
      2) "31.3199993839624824"
127.0.0.1:6379[3]> georadius china:city 110 30 1000 km withcoord withdist withhash
1) 1) "hangzhou"
   2) "982.4071"
   3) (integer) 4054122592018769
   4) 1) "120.21000176668167114"
      2) "30.19999988833350102"
2) 1) "nanjing"
   2) "862.8969"
   3) (integer) 4054278551831736
   4) 1) "118.89000087976455688"
      2) "31.3199993839624824"
127.0.0.1:6379[3]> georadius china:city 110 30 1000 km count 2 # 查询出2个来
1) "nanjing"
2) "hangzhou"

GEORADIUSBYMEMBER 查找某个坐标附近的坐标

127.0.0.1:6379[3]> GEORADIUSBYMEMBER china:city nanjing 100 km
1) "nanjing"

# 以北京这个已经存入的点来查找方圆800km之内的所有的城市
127.0.0.1:6379[3]> GEORADIUSBYMEMBER china:city nanjing 800 km 
1) "hangzhou"
2) "nanjing"
3) "shanghai"
4) "zibo"
5) "qingdao"

GEOHASH

将二维的经纬度转化为一维的字符串,字符串长得越像,说明两个坐标越接近!

127.0.0.1:6379[3]> geohash china:city nanjing hangzhou
1) "wtsd1mth1z0"
2) "wtm7z3wrb00"

GEO是基于Zset实现的,我们可以用Zset命令操作GEO

127.0.0.1:6379[3]> zrange china:city 0 -1
1) "hangzhou"
2) "nanjing"
3) "shanghai"
4) "zibo"
5) "qingdao"
127.0.0.1:6379[3]> zrem china:city hangzhou
(integer) 1
127.0.0.1:6379[3]> zrange china:city 0 -1
1) "nanjing"
2) "shanghai"
3) "zibo"
4) "qingdao"

hyperloglog 基数–不重复的元素

什么是基数?即不重复的元素,可以接受误差!

做基数统计,占用固定内存12kb,

127.0.0.1:6379[3]> pfadd key2 i j k o h y r e t y b v f  f # 创建第一组元素
(integer) 1
127.0.0.1:6379[3]> pfcount key2 # 统计元素
(integer) 12
127.0.0.1:6379[3]> pfcount key
(integer) 1
127.0.0.1:6379[3]> pfmerge key3 key2 key # 合并两组元素的基数数量
OK
127.0.0.1:6379[3]> pfcount key3
(integer) 13

bitmaps 位存储

统计用户信息,活跃 or 不活跃,登录 or 没登陆! 打卡,只要是两个状态的都可以用bitmaps,因为是位存储,只能是0或1两个状态。

位图,数据结构,操作二进制位来进行记录。
Redis学习笔记_第14张图片

127.0.0.1:6379[3]> getbit sign 4 # 查看是否打卡 
(integer) 1
127.0.0.1:6379[3]> getbit sign 3
(integer) 0
127.0.0.1:6379[3]> getbit sign 1
(integer) 1
# 统计操作
127.0.0.1:6379[3]> bitcount sign 0 -1 # 统计所有的内容有多少个值是1的,也就是统计几天打过卡
(integer) 4

Redis事务

事务初探

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

一次性,顺序性,排他性!

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

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

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

Redis的事务:

  • 开启事务 (multi)
  • 命令入队 ()
  • 执行事务 (Exec)

开启事务

127.0.0.1:6379[1]> multi 	# 开启事务
OK
127.0.0.1:6379[1]> set k1 v1  # 将操作放入队列
QUEUED
127.0.0.1:6379[1]> set k2 v2
QUEUED
127.0.0.1:6379[1]> get k1
QUEUED
127.0.0.1:6379[1]> set k3 v3
QUEUED
127.0.0.1:6379[1]> exec  # 执行事务 按顺序执行
1) OK
2) OK
3) "v1"
4) OK

放弃事务

127.0.0.1:6379[1]> multi
OK
127.0.0.1:6379[1]> set k1 v1
QUEUED
127.0.0.1:6379[1]> set k4 v4
QUEUED
127.0.0.1:6379[1]> discard  # 放弃事务  放弃事务之后所作的操作都没有实现
OK
127.0.0.1:6379[1]> get k4
(nil)

如果代码有问题则都不会被执行。–编译型一异常

127.0.0.1:6379[1]> multi
OK
127.0.0.1:6379[1]> set k1 v1
QUEUED
127.0.0.1:6379[1]> set k2 v2
QUEUED
127.0.0.1:6379[1]> getset k2  # 语法有错误,直接报错
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379[1]> set k3 v3
QUEUED
127.0.0.1:6379[1]> exec  # 执行之后报错,队列中的所有元素都没有存入
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379[1]> get k2
(nil)

为什么说Redis没有原子性呢?因为当发生运行时错误的时候只有错误的语句不会被执行,其他的语句正常执行,但是在关系型数据库中不允许发生这种操作。–运行时异常

127.0.0.1:6379[1]> set k1 "v1"
OK
127.0.0.1:6379[1]> multi
OK
127.0.0.1:6379[1]> incr k1 # 语法没有错误 只有在运行的时候才会报错
QUEUED
127.0.0.1:6379[1]> set k2 v2
QUEUED
127.0.0.1:6379[1]> set k3 v3
QUEUED
127.0.0.1:6379[1]> get k2
QUEUED
127.0.0.1:6379[1]> exec  # 执行之后除了错误的操作之外的操作都执行
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v2"
127.0.0.1:6379[1]> get k3
"v3"

Redis实现乐观锁 (面试常问)

监控,watch

乐观锁

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

悲观锁

  • 很悲观,认为什么时候都会出现问题,所以每次都会加锁。

Redis测试监控测试

正常执行,单线程执行:

127.0.0.1:6379[1]> set money 100
OK
127.0.0.1:6379[1]> set out 0
OK
127.0.0.1:6379[1]> watch money  # 监视money
OK
127.0.0.1:6379[1]> multi
OK
127.0.0.1:6379[1]> decrby money 20
QUEUED
127.0.0.1:6379[1]> incrby out 20
QUEUED
127.0.0.1:6379[1]> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379[1]>

多线程执行,当一个线程监视了某个值的时候并且在事务中,另外一个线程企图修改该值,最后提交事务的时候会失败:

线程一执行事务失败:

127.0.0.1:6379[1]> set money 100
OK
127.0.0.1:6379[1]> set out 0
OK
127.0.0.1:6379[1]> watch money  # 监视nmoney
OK
127.0.0.1:6379[1]> multi
OK
127.0.0.1:6379[1]> decrby money 20
QUEUED
127.0.0.1:6379[1]> incrby out 20
QUEUED
127.0.0.1:6379[1]> exec  # 执行事务之前,检测到有人修改了我们监视的值,所以事务执行失败,这就是乐观锁
(nil)

线程二修改该值:

127.0.0.1:6379[1]> get money
"100"
127.0.0.1:6379[1]> get out
"0"
127.0.0.1:6379[1]> set money 1000
OK
127.0.0.1:6379[1]> get money
"1000"

可以解决上面的问题,我们先解除监视,在重新监视就可以了。

127.0.0.1:6379[1]> unwatch  # 先解除监视
OK
127.0.0.1:6379[1]> watch money  # 重新监视
OK
127.0.0.1:6379[1]> multi  # 重新执行事务
OK
127.0.0.1:6379[1]> decrby money 20
QUEUED
127.0.0.1:6379[1]> incrby out 20
QUEUED
127.0.0.1:6379[1]> exec  #  这次在执行的时候比对监视的值有没有发生变化,没有修改,执行成功 乐观锁
1) (integer) 980
2) (integer) 20

如果修改失败则执行重新监控就可以了,这就是乐观锁的实现。

Jedis

Jedis是在Java中执行Redis的一个包,里面有很多的方法可以操作Redis,Jedis包中的方法名就是我们上面学的命令的名字,一摸一样。

导入依赖


<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>3.5.1version>
dependency>

<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.36version>
dependency>
public class TestPing {
     

     public static void main(String[] args ){
     
         // 连接Redis服务
         // 前提是必须已经启动了Redis的服务
         Jedis jedis = new Jedis("localhost");

         // 查看服务是否可以运行
         System.out.println(jedis.ping("Hello Redis"));
         // ......
         jedis.select(1);
         System.out.println(jedis.keys("*"));
         System.out.println(jedis.get("money"));
         System.out.println(jedis.exists("money"));

     }
}
//Java中操作Redis事务
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TestPing {
     

     public static void main(String[] args ){
     
         // 连接Redis服务
         // 前提是必须已经启动了Redis的服务
         Jedis jedis = new Jedis("localhost");

         JSONObject jsonObject = new JSONObject();
         jsonObject.put("k1","v1");
         jsonObject.put("k2","v2");
         jsonObject.put("k3","v3");

         String s = jsonObject.toJSONString();

         //开启事务
         Transaction multi = jedis.multi();
         try {
     
            multi.set("user1",s);
            multi.set("user2",s);
            multi.set("user3",s);
            // int i =1/0;//运行时错误
            multi.exec();
         }catch (Exception e){
     
             // 执行失败之后取消事务
             multi.discard();
             e.printStackTrace();
         }finally {
     
             // 执行成功之后输出 并关闭
             System.out.println(jedis.get("user1"));
             System.out.println(jedis.get("user2"));
             System.out.println(jedis.get("user3"));
             jedis.close();
         }

     }
}

SpringBoot整合Redis

// 关键依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

SpringBoot自2.0之后Redis的底层用的是lettuce,并不是用的Jedis,因为Jedis是用直连的方式,多个线程操作的话是不安全的,如果要避免不安全的话,就要用Jedis Pool连接池,更像BIO模式。而lettuce采用netty,实例可以在多个线程中进行分享,不存在线程不安全的的情况,可以减少线程数据,更像NIO模式。

package org.springframework.boot.autoconfigure.data.redis;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis support.
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({
      LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
     

    // 模板
	@Bean
    // 这条语句的意思是如果不存在的话这里就生效
	@ConditionalOnMissingBean(name = "redisTemplate") 
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class) 
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
     
        // Redis都是需要序列化的
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

    // 因为String比较常用,所以有一个String模板
	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
     
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

}
//配置默认序列化与反序列化工具类
2.afterPropertiesSet
//根据参数执行相关operation操作,例如,事务
3.execute
//执行pipelining流水线相关操作
4.executePipelined
//执行指定connection连接的相关操作
5.executeWithStickyConnection
//执行session内的execute方法
6.executeSession
//创建RedisConnection代理类
7.createRedisConnectionProxy
//connection连接的预处理
8.preProcessConnection
//结果的后处理,默认什么都不做
9.postProcessResult
//是否向RedisCallback暴露本地连接
10.isExposeConnection
//设置是否向RedisCallback暴露本地连接
11.setExposeConnection
//12到26都是设置和获取相关序列化工具类
12.isEnableDefaultSerializer
13.setEnableDefaultSerializer
14.getDefaultSerializer
15.setDefaultSerializer
16.setKeySerializer
17.getKeySerializer
18.setValueSerializer
19.getValueSerializer
20.getHashKeySerializer
21.setHashKeySerializer
22.getHashValueSerializer
23.setHashValueSerializer
24.getStringSerializer
25.setStringSerializer
26.setScriptExecutor
//27到34为私有方法,不对外提供使用
27.rawKey
28.rawString
29.rawValue
30.rawKeys
31.deserializeKey
32.deserializeMixedResults
33.deserializeSet
34.convertTupleValues
//执行事务
35.exec
36.execRaw
//删除操作
37.delete
//接触链接
38.unlink
//查看是否含有指定key
39.hasKey
40.countExistingKeys
//设置过期时间
41.expire
42.expireAt
//转换成字节流并向channel发送message
43.convertAndSend
//获取过期时间
44.getExpire
//根据传入的正则表达式返回所有的key
46.keys
//取消指定key的过期时间
47.persist
//移动指定的key和index到数据库中
48.move
//从键空间随机获取一个key
49.randomKey
//将指定key改成目标key
50.rename
//key不存在时,将指定key改成目标key
51.renameIfAbsent
//设置存储在指定key的类型
52.type
//检索存储在key的值的序列化版本
53.dump
//执行Redis的restore的命令
54.restore
//标记事务阻塞的开始
55.multi
//丢弃所有在multi之后发出的命令
56.discard
//观察指定key在事务处理开始即multi之后的修改情况
57.watch
//刷新先前观察的所有key
58.unwatch
//为key元素排序
59.sort
//关闭客户端连接
60.killClient
//请求连接客户端的相关信息和统计数据
61.getClientList
//更改复制配置到新的master
62.slaveOf
//将本机更改为master
63.slaveOfNoOne
//64到79都是获取相对应的操作
64.opsForCluster
65.opsForGeo
66.boundGeoOps
67.boundHashOps
68.opsForHash
69.opsForHyperLogLog
70.opsForList
71.boundListOps
72.boundSetOps
73.opsForSet
74.opsForStream
75.boundStreamOps
76.boundValueOps
77.opsForValue
78.boundZSetOps
79.opsForZSet
//设置是否支持事务
80.setEnableTransactionSupport
//设置bean的类加载器
81.setBeanClassLoader

配置文件

#ip地址
redis.hostName=127.0.0.1
#端口号
redis.port=6379
#如果有密码
redis.password=
#客户端超时时间单位是毫秒 默认是2000
redis.timeout=2000

#最大空闲数
redis.maxIdle=10
#连接池的最大数据库连接数。设为0表示无限制,如果是jedis 2.4以后用redis.maxTotal
redis.maxActive=10
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=10
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=1000
#连接的最小空闲时间 默认1800000毫秒(30分钟)
redis.minEvictableIdleTimeMillis=300000
#每次释放连接的最大数目,默认3
redis.numTestsPerEvictionRun=1024
#逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
redis.timeBetweenEvictionRunsMillis=30000
#是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
redis.testOnBorrow=false
#在空闲时检查有效性, 默认false
redis.testWhileIdle=false
spring.redis.port=6379
spring.redis.host=localhost

测试

package com.shi;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class SpringbootRedisApplicationTests {
     

	@Autowired
	RedisTemplate redisTemplate;

	@Test
	void contextLoads() {
     
		// 获取redis的连接对象
		//		RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
		//		connection.flushDb();
		//		connection.flushAll();
		redisTemplate.opsForValue().set("mykey","shixinashneg");
		System.out.println(redisTemplate.opsForValue().get("mykey"));

	}
}

序列化:序列化是将对象转换为容易传输的格式的过程。

Redis学习笔记_第15张图片
在springboot中对象没有序列化之后是不可以存储进Redis 的,所以我们必须先将对象进行序列化才可以存储进redis。

序列化之后正确,可以存入。
Redis学习笔记_第16张图片
Redis学习笔记_第17张图片
在企业开发中我们所有的pojo都会实现序列化

自己定义的Redis的Template–序列化固定模板

package com.shi.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author Shi Jun Yi
 * @version 1.0
 * @date 2021/5/7 19:53
 */
@Configuration
public class RedisConfig {
     

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
     
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 配置序列化
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        // hash的value序列化也采用jackson
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

当我们用自定义的序列化之后我们可以直接存入对象类型的数据,以为我们已经将string的k,v序列化好了,于是我们在命令行中可以直接看到
Redis学习笔记_第18张图片

RedisUtil工具类

RedisUtil 工具类,封装了 RedisTemplate 这个类,以提供更为便利的 对于 Redis 的访问。

package com.shi.utils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

/**
 * @author Shi Jun Yi
 * @version 1.0
 * @date 2021/5/7 20:39
 */
@Component
public final class RedisUtil {
     

    /**
     * 基于spring和redis的redisTemplate工具类
     * 针对所有的hash 都是以h开头的方法
     * 针对所有的Set 都是以s开头的方法                    不含通用方法
     * 针对所有的List 都是以l开头的方法
     */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
     
        this.redisTemplate = redisTemplate;
    }
    //=============================common============================

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
     
        try {
     
            if (time > 0) {
     
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
     
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
     
        try {
     
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
     
        if (key != null && key.length > 0) {
     
            if (key.length == 1) {
     
                redisTemplate.delete(key[0]);
            } else {
     
                redisTemplate.delete(String.valueOf(CollectionUtils.arrayToList(key)));
            }
        }
    }

    //============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
     
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
     
        try {
     
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
     
        try {
     
            if (time > 0) {
     
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
     
                set(key, value);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key 键
     * @param by  要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
     
        if (delta < 0) {
     
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key 键
     * @param by  要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
     
        if (delta < 0) {
     
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    //================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
     
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
     
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
     
        try {
     
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
     
        try {
     
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
     
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
     
        try {
     
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
     
        try {
     
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
     
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
     
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
     
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
     
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
     
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    //============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
     
        try {
     
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
     
        try {
     
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
     
        try {
     
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
     
        try {
     
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
     
                expire(key, time);
            }
            return count;
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
     
        try {
     
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
     
        try {
     
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
    //===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束  0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
     
        try {
     
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
     
        try {
     
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
     
        try {
     
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value) {
     
        try {
     
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
     
        try {
     
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
     
        try {
     
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
     
        try {
     
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
     
        try {
     
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
     
        try {
     
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
     
            e.printStackTrace();
            return 0;
        }
    }
}

Redis配置文件详解

设置Redis密码:
Redis学习笔记_第19张图片

参考网址: https://www.cnblogs.com/ysocean/p/9074787.html

开头说明

Redis学习笔记_第20张图片
这一部分主要是说不区分大小写

INCLUDES (包括,合并多个配置文件)

Redis学习笔记_第21张图片
我们知道Redis只有一个配置文件,如果多个人进行开发维护,那么就需要多个这样的配置文件,这时候多个配置文件就可以在此通过 include /path/to/local.conf 配置进来,而原本的 redis.conf 配置文件就作为一个总闸。

另外需要注意的时,如果将此配置写在redis.conf 文件的开头,那么后面的配置会覆盖引入文件的配置,如果想以引入文件的配置为主,那么需要将 include 配置写在 redis.conf 文件的末尾。

NETWORK (网络)

Redis学习笔记_第22张图片

  1. bind:绑定redis服务器网卡IP,默认为127.0.0.1,即本地回环地址。这样的话,访问redis服务只能通过本机的客户端连接,而无法通过远程连接。如果bind选项为空的话,那会接受所有来自于可用网络接口的连接。

  2. port:指定redis运行的端口,默认是6379。由于Redis是单线程模型,因此单机开多个Redis进程的时候会修改端口。

  3. timeout:设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接。默认值为0,表示不关闭。

  4. tcp-keepalive :单位是秒,表示将周期性的使用SO_KEEPALIVE检测客户端是否还处于健康状态,避免服务器一直阻塞,官方给出的建议值是300s,如果设置为0,则不会周期性的检测。

GENERAL (通用)

Redis学习笔记_第23张图片

  1. daemonize:设置为yes表示指定Redis以守护进程的方式启动(后台启动)。默认值为 no

  2. pidfile:配置PID文件路径,当redis作为守护进程运行的时候,它会把 pid 默认写到 /var/redis/run/redis_6379.pid 文件里面

  3. loglevel :定义日志级别。默认值为notice,有如下4种取值:

    • debug(记录大量日志信息,适用于开发、测试阶段)

    • verbose(较多日志信息)

    • notice(适量日志信息,使用于生产环境)

    • warning(仅有部分重要、关键信息才会被记录)

  4. logfile :配置log文件地址,默认打印在命令行终端的窗口上

  5. databases:设置数据库的数目。默认的数据库是DB 0 ,可以在每个连接上使用select < dbid > 命令选择一个不同的数据库,dbid是一个介于0到databases - 1 之间的数值。默认值是 16,也就是说默认Redis有16个数据库。
    Redis学习笔记_第24张图片

SNAPSHOTTING (快照 Redis持久化之RDB)

Redis学习笔记_第25张图片
Redis学习笔记_第26张图片

  1. save:这里是用来配置触发 Redis的持久化条件,也就是什么时候将内存中的数据保存到硬盘。默认如下配置:
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存

当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。可以直接一个空字符串来实现停用:save ""

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

  2. rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。

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

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

  5. dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。使用上面的 dbfilename 作为保

REPLICATION (主从复制)

  1. slave-serve-stale-data:默认值为yes。当一个 slave 与 master 失去联系,或者复制正在进行的时候,slave 可能会有两种表现:

    • 如果为 yes ,slave 仍然会应答客户端请求,但返回的数据可能是过时,或者数据可能是空的在第一次同步的时候

    • 如果为 no ,在你执行除了 info he salveof 之外的其他命令时,slave 都将返回一个 “SYNC with master in progress” 的错误

  2. slave-read-only:配置Redis的Slave实例是否接受写操作,即Slave是否为只读Redis。默认值为yes。

  3. repl-diskless-sync:主从数据复制是否使用无硬盘复制功能。默认值为no。

  4. repl-diskless-sync-delay:当启用无硬盘备份,服务器等待一段时间后才会通过套接字向从站传送RDB文件,这个等待时间是可配置的。 这一点很重要,因为一旦传送开始,就不可能再为一个新到达的从站服务。从站则要排队等待下一次RDB传送。因此服务器等待一段 时间以期更多的从站到达。延迟时间以秒为单位,默认为5秒。要关掉这一功能,只需将它设置为0秒,传送会立即启动。默认值为5。

  5. repl-disable-tcp-nodelay:同步之后是否禁用从站上的TCP_NODELAY 如果你选择yes,redis会使用较少量的TCP包和带宽向从站发送数据。但这会导致在从站增加一点数据的延时。 Linux内核默认配置情况下最多40毫秒的延时。如果选择no,从站的数据延时不会那么多,但备份需要的带宽相对较多。默认情况下我们将潜在因素优化,但在高负载情况下或者在主从站都跳的情况下,把它切换为yes是个好主意。默认值为no。

SECURITY (安全)

Redis学习笔记_第27张图片

LIMITS (客户)

Redis学习笔记_第28张图片
maxclients :设置客户端最大并发连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件。 描述符数-32(redis server自身会使用一些),如果设置 maxclients为0 。表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息

mongodb 5.0中把MEMORY MANAGEMENT中的内容放在了LIMITS中,

  1. maxmemory:设置Redis的最大内存,如果设置为0 。表示不作限制。通常是配合下面介绍的maxmemory-policy参数一起使用。

  2. maxmemory-policy :当内存使用达到maxmemory设置的最大值时,redis使用的内存清除策略。有以下几种可以选择:

1)volatile-lru 利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )

2)allkeys-lru 利用LRU算法移除任何key

3)volatile-random 移除设置过过期时间的随机key

4)allkeys-random 移除随机ke

5)volatile-ttl 移除即将过期的key(minor TTL)

6)noeviction noeviction 不移除任何key,只是返回一个写错误 ,默认选项

  1. maxmemory-samples :LRU 和 minimal TTL 算法都不是精准的算法,但是相对精确的算法(为了节省内存)。随意你可以选择样本大小进行检,redis默认选择3个样本进行检测,你可以通过maxmemory-samples进行设置样本数。

APPEND ONLY MODE (Redis持久化之AOF)

Redis学习笔记_第29张图片
​ ①、appendonly:默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式, 可以提供更好的持久化特性。Redis会把每次写入的数据在接收后都写入appendonly.aof文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。默认值为no。

②、appendfilename :aof文件名,默认是"appendonly.aof"

③、appendfsync:aof持久化策略的配置;no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快;always表示每次写入都执行fsync,以保证数据同步到磁盘;everysec表示每秒执行一次fsync,可能会导致丢失这1s数据

④、no-appendfsync-on-rewrite:在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。默认值为no。

⑤、auto-aof-rewrite-percentage:默认值为100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。

⑥、auto-aof-rewrite-min-size:64mb。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。

⑦、aof-load-truncated:aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象 redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为 yes。

LUA SCRIPTING (LUA脚本)

lua-time-limit:一个lua脚本执行的最大时间,单位为ms。默认值为5000.

REDIS CLUSTER (Redis集群)

cluster-enabled:集群开关,默认是不开启集群模式。

②、cluster-config-file:集群配置文件的名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。 这个文件并不需要手动配置,这个配置文件有Redis生成并更新,每个Redis集群节点需要一个单独的配置文件。请确保与实例运行的系统中配置文件名称不冲突。默认配置为nodes-6379.conf。

③、cluster-node-timeout :可以配置值为15000。节点互连超时的阀值,集群节点超时毫秒数

④、cluster-slave-validity-factor :可以配置值为10。在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了, 导致数据过于陈旧,这样的slave不应该被提升为master。该参数就是用来判断slave节点与master断线的时间是否过长。判断方法是:比较slave断开连接的时间和(node-timeout * slave-validity-factor) + repl-ping-slave-period 如果节点超时时间为三十秒, 并且slave-validity-factor为10,假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移

⑤、cluster-migration-barrier :可以配置值为1。master的slave数量大于该值,slave才能迁移到其他孤立master上,如这个参数若被设为2,那么只有当一个主节点拥有2 个可工作的从节点时,它的一个从节点会尝试迁移。

⑥、cluster-require-full-coverage:默认情况下,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。 设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。

SLOW LOG (慢日志)

LATENCY MONITOR (延迟监控)

EVENT NOTIFICATION (事件通知)

ADVANCED CONFIG (高级配置)

INCLUDES

在这里插入图片描述

Redis持久化 (面试重点)

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

Redis学习笔记_第30张图片
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

Redis是内存数据库,众所周知,内存是断电即失的,所以我们需要将内存中的数据存储到磁盘当中,那么一旦服务器退出或者是意外宕机也不会丢失数据。

在配置文件中有自定义的rdb持久化的规则,如下:

大概意思就是说当900秒内操作1次的时候就会保存到磁盘,

当300秒执行10次的时候也会保存到磁盘,

第三种是处理高并发的时候用的,意思是在一分钟内有一万个高并发的请求,也将这一万个操作保存到磁盘。

在这里插入图片描述

RDB (Redis DataBase)

Redis学习笔记_第31张图片

什么是RDB

在指定的时间间隔中,将内存中的数据存储到磁盘当中,也就是我们所说的快照功能,他恢复的时候是直接读到内存中的。

Redis会单独创建一个子进程用来进行持久化,在子进程中,会将数据临时写入到一个临时文件,等到所有的快照写入完成之后再替换上次持久化好的正式文件,整个过程主进程是不参与任何操作的,所以保证了极高的性能。如果进行大规模的数据恢复,且对于数据的完整性不是非常敏感,那么RDB方式要比AOF方式更加的高效,RDB的缺点是最后一次修改的数据有可能丢失,且子进程会占用过多的内存空间。我们默认的就是RDB方式,一般不会改动。

保存的是:dump.rdb 文件

触发机制

  1. save的规则满足的条件下,会自动触发rdb规则
  2. 执行flushall命令的时候,也会触发我们的rdb规则
  3. 退出redis,也会产生rdb文件

如何恢复rdb文件

将rdb文件放到我们的redis启动目录之下,redis启动的时候就会检查我们的rdb文件。

优点和缺点

优点

  1. 适合大规模的数据恢复
  2. 对数据的完整性要求不高,因为不是每秒存储,而是一个时间间隔内存储,所以完整性不如aof但性能绝对强于aof

缺点

  1. 需要一定的时间间隔进程操作,如果redis宕机了,那最后一次修改的数据就没有了
  2. fork进程的时候,会占用一定的内存空间
  3. . 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF (Append Only File)

追加文件,将所有的命令都记录下来,history,恢复的时候就把这个文件执行一遍,记录所有的写操作。
Redis学习笔记_第32张图片

保存的文件是: appendonly.aof

检验aof文件,用工具 redis-cheack-aof
Redis学习笔记_第33张图片

优点和缺点

Redis学习笔记_第34张图片

优点

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

缺点

  • 相比于数据文件来说,aof文件大小远远大于rdb,修复的速度远远小于rdb
  • aof运行的效率也比rdb慢,所以我们默认的是rdb,因为没有共享内存

如果是只用Redis做缓存的话不用持久化。

两者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

Redis发布订阅

Redis学习笔记_第35张图片

关键角色:

  • 消息发布者
  • 频道
  • 消息队列

命令

Redis学习笔记_第36张图片
Redis学习笔记_第37张图片

订购端:

127.0.0.1:6379> subscribe shi  # 订阅一个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "shi"
3) (integer) 1
# 等待发送的消息 每发送一个消息就会输出一个这个
1) "message"  # 消息
2) "shi"   # 发送的订阅端
3) "Hello shi" # 发送的内容
# 等待发送的消息
1) "message"
2) "shi"
3) "Hello World"

发送端:

127.0.0.1:6379> publish shi "Hello shi" # 发送给谁,发什么内容
(integer) 1
127.0.0.1:6379> publish shi "Hello World"
(integer) 1
127.0.0.1:6379>

适用场景

  • 实时消息系统,比如在一个网站中回会实时收到管理员发来的消息
  • 实时聊天
  • 订阅,关注系统,例如微信公众号订阅,微博,B站关注的人等,都是可以做的,只不过稍微复杂点的就要用消息队列来做–MQ.说一下订阅消息推送是怎么实现的,我们关注一个公众号之后,公众号的后台写好了文章之后发送给我们这个过程是,相当于有一个消息队列,里面放着所有的公众号,公众号是键,里面的值是我们这些广大的粉丝,这个值是一个链表,然后后台发送了一个消息,发送到了队列(队列中维护了一个字典),然后通过队列转发给我们这些粉丝。

Redis主从复制 (重点)

概念

主从复制是指将一台Redis服务器上的数据复制到其他的Redis服务器中,前者称为主节点,后者称为从节点。

数据的复制是单向的,只能有主节点到从节点,Master以写为主,Slave以读为主。

默认情况下Redis可以没有从节点,但是从节点只能有一个主节点。

主从复制的作用:

  • 数据冗余 :主从复制实现了热备份,是持久化之外的一种数据冗余方式
  • 故障恢复:当主节点出现问题的时候,可以用从节点提供服务,实现快速的故障恢复,
  • 负载均衡:主从复制的基础上配合读写分离,主节点写,从节点读,分担服务器负载,多个服务器分担读的操作可以提高Redis的并发量
  • 高可用:主从复制是哨兵模式和集群实施的基础,

Redis学习笔记_第38张图片
主从复制,读写分离,百分之八十的操作就是在做写的操作。减缓服务器的压力。

环境配置

只配置从库,不用配置主库。因为redis单机默认的就是一个主库。

127.0.0.1:6379> info replication  # 查看当前库的信息
# Replication
role:master  # 角色
connected_slaves:0  # 连接的从机有几个
master_replid:4060b666f7ca01756c3e87a22f08a16e87cca74f
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

复制三个配合文件,然后修改对应的信息

  • 修改端口号Redis学习笔记_第39张图片

  • pid名字

  • 日志名字 在这里插入图片描述

  • dump的名字 在这里插入图片描述

Windows如何搭建Redis集群

参考连接: https://blog.csdn.net/qq_44322555/article/details/104056258

在redis安装目录下新建三个配合文件如下
Redis学习笔记_第40张图片

图中的redis.63**.conf就是我们新建的配置文件。其他的都是自己生成的。还需要新建一个日志文件夹,用来存放三个服务产生的日志。
Redis学习笔记_第41张图片

# 配置文件内容
port 6380      
loglevel notice    
logfile "D:/tongjiao/Redis-x64-3.2.100/logs/redis6380_log.txt"       
appendonly yes
appendfilename "appendonly.6380.aof"   
cluster-enabled no                                    
cluster-config-file nodes.6380.conf
cluster-node-timeout 15000
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes
# 分别配置三个保存皆可

根据这些配置文件安装3个redis服务。

打开CMD控制台,进入D:\Redis\Redis-x64-5.0.10>目录,分别运行3个命令:

redis-server.exe --service-install redis.6379.conf --service-name redis6379
redis-server.exe --service-install redis.6380.conf --service-name redis6380
redis-server.exe --service-install redis.6381.conf --service-name redis6381

运行成功截图
在这里插入图片描述
Redis学习笔记_第42张图片

手动启动三个服务即可。

这样一来我们的三个环境就搭建好了。

Redis学习笔记_第43张图片
Redis学习笔记_第44张图片

一主二从

我们只需要配置从机就可以了,

认老大,主机(6379),从机(6380,6381)

127.0.0.1:6380> slaveof 127.0.0.1 6379  # 设置从节点的主节点是谁,位置 主节点的端口号
OK  # 如果报错就把cluster-enabled 设置为no
127.0.0.1:6380> info replication
# Replication
role:slave   # 现在角色就是从节点
master_host:127.0.0.1  # 主节点的ip
master_port:6379   # 主节点的端口号
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:28
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:d3e19c42e981f6bd233ecbf6ac6c0014e135ef0a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

设置了两个从节点之后再次查看主节点的状态:

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2 # 发现已经有两个从节点了
slave0:ip=127.0.0.1,port=6380,state=online,offset=308,lag=0  # 第一个从节点的一些信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=308,lag=0  # 其他的节点。。。
master_replid:d3e19c42e981f6bd233ecbf6ac6c0014e135ef0a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:308
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:308

Redis学习笔记_第45张图片

我们刚才用的是在命令行中配置主从复制,但是我们实际开发的时候我们用配置文件直接配置好然后启动的时候直接就是从节点了,省了很多麻烦。

测试主从复制

我们已经配置好了一主二从的主从复制了,我们都知道我们需要在主节点中进行写操作,在从节点中进行读的操作,我们在主节点中写的数据会实时的存储到从节点中,加入我们有多个从节点,然后我们有很多的用户来读取数据,这时候我们就可以用那些从节点来实现高性能的读操作了。这就是读写分离,负载均衡。

报错 是否开启集群 主节点和从节点的这个设置全部为no就可以了
Redis学习笔记_第46张图片
测试成功
Redis学习笔记_第47张图片
当我们的主机宕机的时候我们的从节点依旧可以用!!
Redis学习笔记_第48张图片

当我们的主机重新连接之后 我们的从机重新默认主动的连接到主机,和断开前一样。在主机宕机关机的过程中,整个服务器无法进行写的操作,只能进行读的操作,这样做是不友好的。

在MongoDB中有一种机制叫副本集群,当主机挂掉的时候他会自动的推选出一个新的主机,继续进行读写分离的操作,当主机恢复的时候再重新连接到原先的主机。

如果是使用命令行来配置的主从,如果这个时候从机重启了,那么它会自动的变回主机,变成从机之后就会取到主机的数据。

复制原理

Slave启动的之后连接到master之后会发送给master一个sync命令(同步命令)。Master接收到命令之后将数据传给从节点。

  • 全量复制:在从机连接到主机的一瞬间,就已经将主机的所有的数据存储到了从机上,完成了一次完全同步
  • 增量复制:master陆续的将添加的数据传递给slave,完成同步,即不断的set操作,就是增量复制。
  • 只要连接到主机,我们就执行了一次完全同步,主机的数据就会在从机上看到。

Redis学习笔记_第49张图片

如果主机断开了,我们可以用命令slaveof no one让自己变成主节点,然后让其他的节点连接到我,当然这些操作都是手动的。但是哨兵模式出现之后,这些操作都是自动的了。

哨兵模式类似于MongoDB的副本集群

哨兵模式 (重点,面试高频)

自动选举老大的过程,类似于MongoDB的副本集群。

概念

主从切换的技术就是当我们的主机出现故障的时候,需要手动的将一台从机设置成主服务器,这需要人为干预,费时费力,还是造成一段时间的服务不可用。这不是我们推荐的方法,我们推荐的方法是自动的来切换主机,即在Redis2.8之后就支持了哨兵模式(Sentinel)用来解决这个问题。

哨兵模式能够在后台自动的监控主机是否有故障,如果出现了故障,根据投票数自动切换主机。

Redis学习笔记_第50张图片

Redis学习笔记_第51张图片

测试哨兵模式

  1. 配置哨兵配置文件sentinel.conf 监视主节点是否出现故障
# sentinel monitor 被监控的名称 ip地址 端口号 1
sentinel monitor myredis 127.0.0.1 6379 1
后面的1代表主机挂了之后,slave进行投票,看谁能成为新的主机,票数最多的是新主机
  1. 启动哨兵
# 直接启动配置的文件就可以了
redis-server.exe sentinel.conf --sentinel
D:\Redis\Redis-x64-5.0.10>redis-server.exe sentinel.conf --sentinel
[20676] 08 May 17:36:49.201 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
[20676] 08 May 17:36:49.201 # Redis version=5.0.10, bits=64, commit=1c047b68, modified=0, pid=20676, just started
[20676] 08 May 17:36:49.202 # Configuration loaded
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 5.0.10 (1c047b68/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 20676
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

[20676] 08 May 17:36:49.217 # Sentinel ID is 9231eaa27693537530e4ce3274fd3bad00919fb8
[20676] 08 May 17:36:49.218 # +monitor master myredis 127.0.0.1 6379 quorum 1
[20676] 08 May 17:36:49.220 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
[20676] 08 May 17:36:49.231 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379

启动之后的效果图:
Redis学习笔记_第52张图片

  1. 测试

首先我们想让主机挂掉

127.0.0.1:6379> shutdown  # 挂掉主机 6379
not connected> exit

D:\Redis\Redis-x64-5.0.10>

查看其余的两个从节点是否还是从节点

127.0.0.1:6380> info replication  # 我们可以看到6380这个节点还是从节点
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:42383
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:6caf2d525cf36396f12e4f63408b96331431466a
master_replid2:763b76b1925682307b96bab81e34cc91ff11d098
master_repl_offset:42383
second_repl_offset:38859
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:42383
127.0.0.1:6381> info replication # 但是6381这个节点已经变成主节点了,这期间我们什么都没有做
# Replication
role:master  # 主节点宕机之后从节点自动变成主节点
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=43453,lag=1
master_replid:6caf2d525cf36396f12e4f63408b96331431466a
master_replid2:763b76b1925682307b96bab81e34cc91ff11d098
master_repl_offset:43585
second_repl_offset:38859
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:43585

我们再看哨兵模式中的提示

[20676] 08 May 17:44:33.511 # +sdown master myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.512 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
[20676] 08 May 17:44:33.518 # +new-epoch 1
[20676] 08 May 17:44:33.519 # +try-failover master myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.539 # +vote-for-leader 9231eaa27693537530e4ce3274fd3bad00919fb8 1
[20676] 08 May 17:44:33.539 # +elected-leader master myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.539 # +failover-state-select-slave master myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.637 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.637 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.718 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.915 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.915 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
[20676] 08 May 17:44:33.983 * +slave-reconf-sent slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:34.966 * +slave-reconf-inprog slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:34.966 * +slave-reconf-done slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
[20676] 08 May 17:44:35.028 # +failover-end master myredis 127.0.0.1 6379
[20676] 08 May 17:44:35.028 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6381
[20676] 08 May 17:44:35.034 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6381
[20676] 08 May 17:44:35.035 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381
[20676] 08 May 17:45:05.100 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381

它的大概意思就是说主节点宕机了,然后进行故障转移,最后推选出新的主节点是6381。

最后看一眼刚才配置的sentinel.conf配置文件 自动改变了
Redis学习笔记_第53张图片

如果原来的主机回来了,那么也只能是从节点,归并到新的主机下,这就是哨兵模式的规则。

哨兵模式

优点:

  • 哨兵集群基于主从复制和负载均衡,所有的主从配置优点,他都有
  • 哨兵模式就是主从模式的升级版,手动到自动,更方便
  • 主从可以切换故障可以转移,系统的可用性更高

缺点:

  • Redis不好在线扩容,集群容量一旦达到上限,在线扩容就很麻烦
  • 实现哨兵模式的配置很麻烦

哨兵模式的全部配置

# Example sentinel.conf

# *** IMPORTANT ***
#
# By default Sentinel will not be reachable from interfaces different than
# localhost, either use the 'bind' directive to bind to a list of network
# interfaces, or disable protected mode with "protected-mode no" by
# adding it to this configuration file.
#
# Before doing that MAKE SURE the instance is protected from the outside
# world via firewalling or other means.
#
# For example you may use one of the following:
#
# bind 127.0.0.1 192.168.1.1
#
# protected-mode no

# port 
# The port that this sentinel instance will run on
port 26379

# By default Redis Sentinel does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis-sentinel.pid when
# daemonized.
daemonize no

# When running daemonized, Redis Sentinel writes a pid file in
# /var/run/redis-sentinel.pid by default. You can specify a custom pid file
# location here.
pidfile /var/run/redis-sentinel.pid

# Specify the log file name. Also the empty string can be used to force
# Sentinel to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""

# sentinel announce-ip 
# sentinel announce-port 
#
# The above two configuration directives are useful in environments where,
# because of NAT, Sentinel is reachable from outside via a non-local address.
#
# When announce-ip is provided, the Sentinel will claim the specified IP address
# in HELLO messages used to gossip its presence, instead of auto-detecting the
# local address as it usually does.
#
# Similarly when announce-port is provided and is valid and non-zero, Sentinel
# will announce the specified TCP port.
#
# The two options don't need to be used together, if only announce-ip is
# provided, the Sentinel will announce the specified IP and the server port
# as specified by the "port" option. If only announce-port is provided, the
# Sentinel will announce the auto-detected local IP and the specified port.
#
# Example:
#
# sentinel announce-ip 1.2.3.4

# dir 
# Every long running process should have a well-defined working directory.
# For Redis Sentinel to chdir to /tmp at startup is the simplest thing
# for the process to don't interfere with administrative tasks such as
# unmounting filesystems.
dir /tmp

# sentinel monitor    
#
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
# (Objectively Down) state only if at least  sentinels agree.
#
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Replicas are auto-discovered, so you don't need to specify replicas in
# any way. Sentinel itself will rewrite this configuration file adding
# the replicas using additional configuration options.
# Also note that the configuration file is rewritten when a
# replica is promoted to master.
#
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor mymaster 127.0.0.1 6379 2

# sentinel auth-pass  
#
# Set the password to use to authenticate with the master and replicas.
# Useful if there is a password set in the Redis instances to monitor.
#
# Note that the master password is also used for replicas, so it is not
# possible to set a different password in masters and replicas instances
# if you want to be able to monitor these instances with Sentinel.
#
# However you can have Redis instances without the authentication enabled
# mixed with Redis instances requiring the authentication (as long as the
# password set is the same for all the instances requiring the password) as
# the AUTH command will have no effect in Redis instances with authentication
# switched off.
#
# Example:
#
# sentinel auth-pass mymaster MySUPER--secret-0123passw0rd

# sentinel down-after-milliseconds  
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
sentinel down-after-milliseconds mymaster 30000

# sentinel parallel-syncs  
#
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel parallel-syncs mymaster 1

# sentinel failover-timeout  
#
# Specifies the failover timeout in milliseconds. It is used in many ways:
#
# - The time needed to re-start a failover after a previous failover was
#   already tried against the same master by a given Sentinel, is two
#   times the failover timeout.
#
# - The time needed for a replica replicating to a wrong master according
#   to a Sentinel current configuration, to be forced to replicate
#   with the right master, is exactly the failover timeout (counting since
#   the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
#   did not produced any configuration change (SLAVEOF NO ONE yet not
#   acknowledged by the promoted replica).
#
# - The maximum time a failover in progress waits for all the replicas to be
#   reconfigured as replicas of the new master. However even after this time
#   the replicas will be reconfigured by the Sentinels anyway, but not with
#   the exact parallel-syncs progression as specified.
#
# Default is 3 minutes.
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION
#
# sentinel notification-script and sentinel reconfig-script are used in order
# to configure scripts that are called to notify the system administrator
# or to reconfigure clients after a failover. The scripts are executed
# with the following rules for error handling:
#
# If script exits with "1" the execution is retried later (up to a maximum
# number of times currently set to 10).
#
# If script exits with "2" (or an higher value) the script execution is
# not retried.
#
# If script terminates because it receives a signal the behavior is the same
# as exit code 1.
#
# A script has a maximum running time of 60 seconds. After this limit is
# reached the script is terminated with a SIGKILL and the execution retried.

# NOTIFICATION SCRIPT
#
# sentinel notification-script  
# 
# Call the specified notification script for any sentinel event that is
# generated in the WARNING level (for instance -sdown, -odown, and so forth).
# This script should notify the system administrator via email, SMS, or any
# other messaging system, that there is something wrong with the monitored
# Redis systems.
#
# The script is called with just two arguments: the first is the event type
# and the second the event description.
#
# The script must exist and be executable in order for sentinel to start if
# this option is provided.
#
# Example:
#
sentinel notification-script mymaster /var/redis/notify.sh

# CLIENTS RECONFIGURATION SCRIPT
#
# sentinel client-reconfig-script  
#
# When the master changed because of a failover a script can be called in
# order to perform application-specific tasks to notify the clients that the
# configuration has changed and the master is at a different address.
# 
# The following arguments are passed to the script:
#
#       
#
#  is currently always "failover"
#  is either "leader" or "observer"
# 
# The arguments from-ip, from-port, to-ip, to-port are used to communicate
# the old address of the master and the new address of the elected replica
# (now a master).
#
# This script should be resistant to multiple invocations.
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

# SECURITY
#
# By default SENTINEL SET will not be able to change the notification-script
# and client-reconfig-script at runtime. This avoids a trivial security issue
# where clients can set the script to anything and trigger a failover in order
# to get the program executed.

sentinel deny-scripts-reconfig yes

# REDIS COMMANDS RENAMING
#
# Sometimes the Redis server has certain commands, that are needed for Sentinel
# to work correctly, renamed to unguessable strings. This is often the case
# of CONFIG and SLAVEOF in the context of providers that provide Redis as
# a service, and don't want the customers to reconfigure the instances outside
# of the administration console.
#
# In such case it is possible to tell Sentinel to use different command names
# instead of the normal ones. For example if the master "mymaster", and the
# associated replicas, have "CONFIG" all renamed to "GUESSME", I could use:
#
# SENTINEL rename-command mymaster CONFIG GUESSME
#
# After such configuration is set, every time Sentinel would use CONFIG it will
# use GUESSME instead. Note that there is no actual need to respect the command
# case, so writing "config guessme" is the same in the example above.
#
# SENTINEL SET can also be used in order to perform this configuration at runtime.
#
# In order to set a command back to its original name (undo the renaming), it
# is possible to just rename a command to itsef:
#
# SENTINEL rename-command mymaster CONFIG CONFIG

Redis缓存穿透和雪崩 (重点,面试高频) 实现服务器的高可用

缓存穿透 (在缓存中查询不到)

Redis学习笔记_第54张图片

概念

我自己理解的意思就是我们的缓存没有用到,直接绕过缓存去读取mysql里面的数据,当恶意攻击的时候我们的MySQL很有可能就会崩。

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

解决方案

布隆过滤器

Redis学习笔记_第55张图片

缓存空对象
Redis学习笔记_第56张图片
Redis学习笔记_第57张图片

缓存击穿 (在缓存中查询的太多了)

大量数据去查询一个缓存,相当于集中火力去打一面墙上的一个点,自然很容易就被击穿。缓存自然也会被击穿,扛不住高并发。

注意区分缓存穿透和缓存击穿。

概念

缓存击穿是指一个key是热点,在不停的扛着高并发,高并发集中对一个缓存进行访问,当这个key失效的瞬间,持续的高并发就会击穿缓存,直接请求数据库服务器,那后果可想而知。

当key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问持久层的数据库,会导致数据库压力增大。

例子:微博热搜,不知道挂了几次了,例如某某某明星出轨,大量的并发请求砸在这一个缓存上(微博热搜是放在缓存里的,因为他不是持久的,一段时期内的波段信息),当这个key过期的时候,所有的并发请求就砸在了数据库的服务器上了,很容易挂掉。

解决方案

设置热点数据永不过期

也不是很好,因为不过期的话会造成内存空间的浪费,所以不是很推荐

加互斥锁

分布式锁:保证每个key只能同时有一个线程去查询后端服务,其他的线程没有获得分布式锁的权限只能等待,这样一来就很好的保证了持久层的数据库不会崩,但是对分布式锁的考验也很大,高并发的压力全部打在了分布式锁上。

缓存雪崩

当发生雪崩的时候没有一片雪花是无辜的

概念

缓存雪崩,是指一段时间内,缓存集中过期失效,Redis宕机!
Redis学习笔记_第58张图片

例子:双十一的时候为了高可用会停掉一些服务,例如退款服务,直接提示你太火爆,无法退款之类的。

解决方案

Redis高可用

既然redis有可能挂掉,那我们设置多台redis服务器,这样一来我们当一台redis服务器挂掉的时候我们用另一台,redis集群,异地多活,

限流降级

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

数据预热

数据预热的含义就是我们事先将有可能访问的数据线访问一遍,使其预先放入缓存。

只要学不死,就往死里学!!!

你可能感兴趣的:(日常学习,学习笔记,redis)