Redis笔记

1、Nosql概述

Redis是什么?

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

为什么要使用Nosql?

1、单机Mysql时代

在这里插入图片描述

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

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

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

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

在这里插入图片描述

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

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

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

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

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

在这里插入图片描述

4、如今最近的年代

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

在这里插入图片描述

5、为什么要用NoSQL ?

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

6、什么是Nosql

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

Not Only Structured Query Language

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

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

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

7、Nosql特点

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

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

- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
- ...
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展
- ...

了解:3V + 3高

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

  1. 海量Velume
  2. 多样Variety
  3. 实时Velocity

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

  1. 高并发
  2. 高可扩
  3. 高性能

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

阿里巴巴演进分析

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

# 商品信息
- 一般存放在关系型数据库:Mysql,阿里巴巴使用的Mysql都是经过内部改动的。

# 商品描述、评论(文字居多)
- 文档型数据库:MongoDB

# 图片
- 分布式文件系统 FastDFS
- 淘宝:TFS
- Google: GFS
- Hadoop: HDFS
- 阿里云: oss

# 商品关键字 用于搜索
- 搜索引擎:solr,elasticsearch
- 阿里:Isearch 多隆

# 商品热门的波段信息
- 内存数据库:Redis,Memcache

# 商品交易,外部支付接口
- 第三方应用

8、NoSQL的四大分类

KV键值对:

  • 新浪:Redis
  • 美团:Redis + Tair
  • 阿里、百度:Redis + Memcache

文档型数据库(bson数据格式):

  • MongoDB(掌握)
    • 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
    • MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的,NoSQL中最像关系型数据库的数据库。
  • ConthDB

列存储数据库

  • HBase(大数据必学)
  • 分布式文件系统

图关系数据库

用于广告推荐,社交网络

  • Neo4j、InfoGrid

在这里插入图片描述

2、Redis入门

Redis是什么?

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

免费 开源当代最热门的NoSQL技术之一

NoSQL能干嘛?

  1. 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
  2. 高效率、用于高速缓冲
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器、计数器(eg:浏览量)
  6. 。。。

特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

环境搭建

官网:https://redis.io/

中文网: http://www.redis.cn/

推荐使用Linux服务器学习。

windows版本的Redis已经停更很久了…

Windows安装

1、下载地址

  • Github下载地址:https://github.com/MicrosoftArchive/redis/releases
  • 百度网盘下载地址:https://pan.baidu.com/s/1PDYbR7PSdHNac2djSPuTRQ 提取码:tilq

2、下载后解压文件

在这里插入图片描述

3、双击redis-server.exe(默认端口6379)

在这里插入图片描述

4、链接客户端双击Redis-server.exe

在这里插入图片描述

链接成功

Linux安装

1、下载安装包!redis-7.0.10.zip ,gcc为9.0版本

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

在这里插入图片描述

3、基本环境安装,以及出现的问题

  • 1、gcc -v先查看版本是否安装

  • 2、yum install gcc-c++

  • 3、在查看版本

  • 4、

  • [root@lsa redis-7.0.0]# yum install centos-release-scl scl-utils-build
    [root@lsa redis-7.0.0]# yum install -y devtoolset-9-toolchain
    [root@lsa redis-7.0.0]# scl enable devtoolset-9 bash
    
  • 安装完后,需要注意的是scl命令启用只是临时的,退出shell或重启就会恢复原系统gcc版本

    配置长期使用gcc9

    1. [root@lsa redis-7.0.0] echo -e "\nsource /opt/rh/devtoolset-9/enable" >>/etc/profile
    2. [root@lsa redis-7.0.0] source /etc/profile
    
  • make

    出现的问题

    release.c:37:10: fatal error: release.h: 没有那个文件或目录
      37 | #include "release.h"
         |          ^~~~~~~~~~~
    compilation terminated.
    解决方法:
    1.cd 到文件中的src目录
    2.chmod +x mkreleasehdr.sh
    3.make sudo make install
    
     CC adlist.o
    In file included from adlist.c:34:0:
    zmalloc.h:50:31: 致命错误:jemalloc/jemalloc.h:没有那个文件或目录
      #include <jemalloc/jemalloc.h>
                                 ^
    解决方法:
    make MALLOC=libc
    

然后在进行make

4、make

5、 make install

在这里插入图片描述

6、Redis默认安装路径 usr/local/bin

在这里插入图片描述

7、将redis的配置文件复制到 程序安装目录 ``下

cp opt/redis-7.0.01/redis.config /usr/local/bin/lsaonfig

之后就用这个复制的配置文件进行调试

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

在这里插入图片描述

9、通过制定的配置文件启动redis服务、

在这里插入图片描述

10、使用redis-cli连接指定的端口号测试,Redis的默认端口6379

在这里插入图片描述

11、查看redis进程是否开启

在这里插入图片描述

12、关闭Redis服务 shutdown

在这里插入图片描述

性能测试

Redis官方性能测试工具

redis-benchmark [option] [option value]

在这里插入图片描述

实例测试:

以下实例同时执行 10000 个请求来检测性能

 redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 10000

在这里插入图片描述

基础知识

redis默认有16个数据库

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

127.0.0.1:6379> config get databases # 命令行查看数据库数量databases
1) "databases"
2) "16"
127.0.0.1:6379> keys *
1) "mylist"
2) "key:__rand_int__"
3) "name"
4) "counter:__rand_int__"
5) "myhash"
127.0.0.1:6379> DBSIZE # size和key个数相关
(integer) 5
127.0.0.1:6379> select 3 # 切换数据库 DB 3
OK
127.0.0.1:6379[3]> set name lsa
OK
127.0.0.1:6379[3]> dbsize # 查看数据库大小
(integer) 1
127.0.0.1:6379[3]> get name # db3中并不能获取db0中的键值对。
"lsa"
127.0.0.1:6379[3]> flushdb #清除当前数据库的内容
OK
127.0.0.1:6379[3]> get name 
(nil)
127.0.0.1:6379[3]> SELECT 0
OK

keys * :查看当前数据库中所有的key。

flushdb:清空当前数据库中的键值对。

flushall:清空所有数据库的键值对。

Redis是单线程的,Redis是基于内存操作的。

所以Redis的性能瓶颈不是CPU而是机器,内存与网络带宽

为什么Redis的速度如此之快呢,性能这么搞呢?

QPS达到10W+

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

  • 误区1:高性能的服务器一定是多线程的?
  • 误区2:多线程(CPU上下文会切换!)一定比单线程效率高!

首先我们先来说一下 处理速度

CPU>内存>硬盘

核心:而Redis就是将所有的数据都存放在内存当中的,

所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),

对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。

3、五大数据类型

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis-key

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

127.0.0.1:6379> keys * # 查看当前数据库所有key
1) "name"
2) "age"
127.0.0.1:6379> move age 1 # 将键值对移动到指定数据库
(integer) 1
127.0.0.1:6379> EXISTS age # 判断键是否存在
(integer) 0#不存在
127.0.0.1:6379> EXISTS name 
(integer) 1#存在
127.0.0.1:6379> del name# 删除键值对
(integer) 1#删除个数
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name lsa
OK
127.0.0.1:6379> set age 22
OK
127.0.0.1:6379> EXPIRE age 10 # 设置键值对的过期时间
(integer) 1
127.0.0.1:6379> ttl age
(integer) 6
127.0.0.1:6379> ttl age
(integer) 4
127.0.0.1:6379> ttl age
(integer) 4
127.0.0.1:6379> ttl age
(integer) 3
127.0.0.1:6379> ttl age
(integer) 2
127.0.0.1:6379> ttl age
(integer) -2# 设置键值对的过期时间
127.0.0.1:6379> get age
(nil)
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> type name# 查看value的数据类型
string

关于TTL命令

Redis的key,通过TTL命令返回key的过期时间,一般来说有3种:

当前key没有设置过期时间,所以会返回-1.
当前key有设置过期时间,而且key已经过期,所以会返回-2.
当前key有设置过期时间,且key还没有过期,故会返回key的正常剩余时间.
关于重命名RENAME和RENAMENX

RENAME key newkey修改 key 的名称
RENAMENX key newkey仅当 newkey 不存在时,将 key 改名为 newkey 。
更多命令学习:https://www.redis.net.cn/order/

1、String(字符串)

1、set:添加、get:查询、append追加、strlen获取长度,exists判断是否存在的操作

127.0.0.1:6379> set name lsa #设置值
OK
127.0.0.1:6379> get name   #获取值
"lsa"
127.0.0.1:6379> keys *    #获得所有的key
1) "name"
127.0.0.1:6379> EXISTS name  #判断key是否存在,存在返回1
(integer) 1
127.0.0.1:6379> EXISTS name1 #判断key是否存在,不存在返回0
(integer) 0
127.0.0.1:6379> APPEND name lovezck #Key存在,追加到key为‘name’的数据后拼接值为‘lovezck’,如果key存在类似于java中字符串‘+’,并且返回该数据的总长度
(integer) 10

127.0.0.1:6379> APPEND name1 aaaaaa #Key不存在,类似于Redis中的set name1 aaaaaa ,并且返回该数据的总长度
(integer) 6
127.0.0.1:6379>keys * 
1) "name"
2) "name1"
127.0.0.1:6379> get name1
"aaaaaa"
127.0.0.1:6379> strlen name  #查看key为‘name’的字符串长度
(integer) 10
127.0.0.1:6379> set key1 hello world#注意点:插入的数据中如果有空格的数据,请用“”双引号,否则会报错!
(error) ERR syntax error
127.0.0.1:6379> set key1 hello,world#逗号是可以的
OK

2、incr自增、decr自减操作

127.0.0.1:6379> set views 0  #插入一个初始值为0的数据
OK
127.0.0.1:6379> get views    #获取值
"0"
127.0.0.1:6379> INCR views   #指定key为‘views’的数据自增1,返回结果  相当于java中 i++
(integer) 1
127.0.0.1:6379> get views    #获取值 
"1"
127.0.0.1:6379> INCR views   #一般用来做文章浏览量、点赞数、收藏数等功能
(integer) 2
127.0.0.1:6379> INCR views
(integer) 3
127.0.0.1:6379> get views 
"3"
127.0.0.1:6379> decr views  #指定key为‘views’的数据自减1,返回结果  相当于java中 i--
(integer) 2
127.0.0.1:6379> decr views
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> decr views   #一般用来做文章取消点赞、取消收藏等功能
(integer) -1
127.0.0.1:6379> INCRBY views 10 #后面跟上by  指定key为‘views’的数据自增‘参数(10)’,返回结果
(integer) 9
127.0.0.1:6379> INCRBY views 10
(integer) 19
127.0.0.1:6379> DECRBY views 8 #后面跟上by  指定key为‘views’的数据自减‘参数(8)’,返回结果
(integer) 11
127.0.0.1:6379> DECRBY views 8
(integer) 3
127.0.0.1:6379> 

3、getrange截取替换字符串操作

截取
127.0.0.1:6379> set key1 "hello,world" #设置一个值
OK
127.0.0.1:6379> GETRANGE key1  0 3     
#截取字符串,相当于java中的subString,根据下表进行截取 
"hell"
127.0.0.1:6379> get key1
"hello,world"
127.0.0.1:6379> GETRANGE key1 0 -1     #0至-1相当于 get key1,效果一致,获取整条数据
"hello,world"
替换
127.0.0.1:6379> set key2 hello,,,world
OK
127.0.0.1:6379> get key2
"hello,,,world"
127.0.0.1:6379> SETRANGE key2 5 666  #此语句跟java中replace有点类似,下标也是从0开始,但是有区别:java中是指定替换字符,Redis中是从指定位置开始替换,替换的数据根据你所需替换的长度一致,返回值是替换后的长度,从第五个开始替换,包括第五个
(integer) 13
127.0.0.1:6379> get key2
"hello666world"

4、setex设置过期时间、 setnx不存在设置操作

# setex (set with expire) #设置过期时间
# setnx (set if not exists) #不存在设置(在分布式锁中常常使用)

127.0.0.1:6379> setex name 30 lsalsa  #新建一个key为‘name’,值为‘lsalsa’,过期时间为30秒的字符串数据
OK
127.0.0.1:6379> ttl name #查看过期时间
(integer) 26
127.0.0.1:6379> ttl name
(integer) 25
127.0.0.1:6379> ttl name
(integer) 24
127.0.0.1:6379> ttl name
(integer) 23
127.0.0.1:6379> ttl name
(integer) 23
127.0.0.1:6379> setnx name1 lsazck # 如果name1不存在就创建 创建成功返回1
(integer) 1
127.0.0.1:6379> get name2
(nil)
127.0.0.1:6379> get name1
"lsazck"
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> keys *
1) "name1"
127.0.0.1:6379> setnx name1 lsalsazck #创建name1,因为已经存在,创建失败返回0
(integer) 0
127.0.0.1:6379> get name1
"lsazck"

5、msetmget,批量获取值,批量设置值操作

127.0.0.1:6379> keys * 
1) "name1"
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 
OK
127.0.0.1:6379> keys * #查询所有数据
1) "k1"
2) "name1"
3) "k3"
4) "k2"
127.0.0.1:6379> mget k1 k2 k3 #查询key为 k1 k2 k3 多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v2 k4 v4  #msetnx 是一个原子性的操作,在一定程度上保证了事务!要么都成功,要么都失败!相当于if中中的条件&&(与)
(integer) 0
127.0.0.1:6379> msetnx k5 v5  k4 v4 
(integer) 1
127.0.0.1:6379> keys *
1) "k1"
2) "name1"
3) "k2"
4) "k3"
5) "k5"
6) "k4"

6、添加获取对象getset操作

127.0.0.1:6379> keys  * #查看全部key
(empty array)
127.0.0.1:6379> set user:1 {name:lsa,age:22}  #设置一个key为user:1的json字符串对象
OK
127.0.0.1:6379> keys * #查看全部key
1) "user:1"
127.0.0.1:6379> get user:1 #获取对象
"{name:lsa,age:22}"

#这里其实本质上还是字符串,但是我们讲其key巧妙的设计了一下。
#mset user:1:name  user 相当于类名,1 相当于id,name 相当于属性  user:{id}:{filed}
#如果所需数据全部这样设计,那么我们在java的业务代码中,就不需要关注太多的key
#只需要找到student类,下面哪个id,需要哪个属性即可,减少了代码的繁琐,在一定程度上可以理解为这个一个类的对象!
127.0.0.1:6379> mset user:1:name lsa user:1:age 22 #新增一个key为‘user:1:name’,‘user:1:age’,value为lsa  22
OK
127.0.0.1:6379> keys * #获取值
1) "user:1:name"
2) "user:1"
3) "user:1:age"

#getset
127.0.0.1:6379> getset db redis #如果不存在,先获取后创建,db就获取为null 然后在去创建值为redis
                                 #先get再set,先获取key,如果没有,set值进去,返回的是get的值
(nil)
127.0.0.1:6379> get db 
"redis"
127.0.0.1:6379> getset db redis-cli #如果已经存在,先获取后创建,db获取为存储的值,然后后面可以修改
                                    #先获取key,如果有,set(替换)最新的值进去,返回的是get的值
"redis"
127.0.0.1:6379> get db
"redis-cli"

7、总结

String是Redis中最常用的一种数据类型,也是Redis中最简单的一种数据类型。首先,表面上它是字符串,但其实他可以灵活的表示字符串、整数、浮点数3种值。Redis会自动的识别这3种值。

计数器

统计多单位的数量

2、List(有序,可重复)

①lpush(左插入)、lrange(查询集合)、rpush(右插入)操作

127.0.0.1:6379> lpush list v1  # 新增一个集合,从左侧pish一个数据为v1 可批量添加
(integer) 1
127.0.0.1:6379> lpush list v2
(integer) 2
127.0.0.1:6379> lpush list v3
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1 #查询list集合中的所有数据
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> lpush list1 v1 v2 v3 v4 v5    #批量添加集合元素
(integer) 5
127.0.0.1:6379> keys * #查看所有的key
1) "list1"
2) "list"
127.0.0.1:6379> LRANGE list1 0 -1 #查询list1集合中的所有元素
1) "v5"
2) "v4"
3) "v3"
4) "v2"
5) "v1"
#这里大家有没有注意到,先进去的会到后面,也就是我们的lpush的意思是左插入,l--left,最早进入的就会往后排
127.0.0.1:6379> LRANGE list 0 1 #指定查询列表中的元素 0 1 从下标零开始,1结束,两个元素
1) "v3"
2) "v2"
127.0.0.1:6379> LRANGE list 0 0 #指定查询列表中的元素 0 1 从下标零开始,1结束,两个元素
1) "v3"
127.0.0.1:6379> rpush list rv0  #右插入,跟lpush相反,这里添加进去元素是在尾部!r--rightt
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "v3"
2) "v2"
3) "v1"
4) "rv0"
#联想:这里我们是不是可以做一个,保存的记录值(如:账号密码的记录)
#每次都使用lpush,老的数据永远在后面,我们每次获取 0 0 位置的元素,是不是相当于更新了
#数据操作,但是数据记录还在?想要查询记录即可获取集合所有元素!

②lpop(左移除)、rpop(右移除)操作

#LPOP
127.0.0.1:6379> keys *             # 查询所有的key
1) "list1"
2) "list"
127.0.0.1:6379> lrange list1 0 -1 # 查找list1数组下的数据
1) "v5"
2) "v4"
3) "v3"
4) "v2"
5) "v1"
127.0.0.1:6379> LPOP list1        # 从头部开始移除第一个元素
"v5"
127.0.0.1:6379> lrange list1 0 -1
1) "v4"
2) "v3"
3) "v2"
4) "v1"
#RPOP
127.0.0.1:6379> RPOP list1   #从尾部开始移除第一个元素
"v1"
127.0.0.1:6379> lrange list1 0 -1
1) "v4"
2) "v3"
3) "v2"
127.0.0.1:6379> 

③lindex(查询指定下标元素)、llen(查看长度) 操作

127.0.0.1:6379> LRANGE list1 0 -1 #获取全部数据
1) "v4"
2) "v3"
3) "v2"
127.0.0.1:6379> lindex list1 1 #搜索数据list1中的下表为1的元素
"v3"
127.0.0.1:6379> lindex list1 0 #搜索数据list1中的下表为0的元素
"v4"
127.0.0.1:6379> llen list1   #获取指定集合的元素长度,相当于java中的length或者size
(integer) 3

④lrem(根据value移除指定的值)

127.0.0.1:6379> LRANGE list1 0 -1 #获取全部数据
1) "v4"
2) "v3"
3) "v2"
4) "v4"
127.0.0.1:6379> lrem list1 1 v2 #移除集合list1中的元素是v2的元素1个
(integer) 1
127.0.0.1:6379> LRANGE list1 0 -1
1) "v4"
2) "v3"
3) "v4"
127.0.0.1:6379> lrem list1 2 v4 #移除集合list1中的元素是v4的元素2个
(integer) 2
127.0.0.1:6379> LRANGE list1 0 -1
1) "v3"
127.0.0.1:6379> 

⑥ltrim(截取元素)、rpoplpush(移除指定集合中最后一个元素到一个新的集合中,类似于剪切)操作

127.0.0.1:6379> lpush mylist "hello" 
(integer) 1
127.0.0.1:6379> lpush mylist "hello1"
(integer) 2
127.0.0.1:6379> lpush mylist "hello2"
(integer) 3
127.0.0.1:6379> lpush mylist "hello3"
(integer) 4
#从坐到右hello3hello2hello1hello
127.0.0.1:6379> ltrim mylist 0  1 #通过下标截取指定的长度,这个mylist已经被改变了,只剩下我们所指定截取后的元素 就说通过mylist集合下标进行截取 从下表为0开始到1结束
OK
127.0.0.1:6379> lrange mylist 0 -1 
1) "hello3"
2) "hello2"
#rpoplpush
127.0.0.1:6379> lrange mylist 0 -1 
1) "hello1"
2) "hello"
127.0.0.1:6379> rpoplpush mylist newlist #把 mylist集合中右边第一个元素移动到newlist当中 
"hello"
127.0.0.1:6379> lrange newlist  0 -1
1) "hello"

⑦lset(更新)、linsert操作

127.0.0.1:6379> lrange list 0 -1 #查看list集合中的所有元素
1) "v3"
2) "v2"
3) "v1"
4) "rv0"
127.0.0.1:6379> lset list 0 newv3 #把list集合当中下标为0的元素替换为newv3
OK
127.0.0.1:6379> lrange list 0 -1
1) "newv3"
2) "v2"
3) "v1"
4) "rv0"
127.0.0.1:6379> lset list 5 v5 #如果替换的下标不存在,报错
(error) ERR index out of range
#linsert
127.0.0.1:6379> lrange list 0 -1 
1) "newv3"
2) "v2"
3) "v1"
4) "rv0"
127.0.0.1:6379> linsert list after rv0 insertrvo #在list集合中的rv0前面插入insertrvo
(integer) 5
127.0.0.1:6379> lrange list 0 -1 
1) "newv3"
2) "v2"
3) "v1"
4) "rv0"
5) "insertrvo"
127.0.0.1:6379> linsert list before rv0  insertrvo#在list集合中的rv0后面插入insertrvo
(integer) 6
127.0.0.1:6379> lrange list 0 -1
1) "newv3"
2) "v2"
3) "v1"
4) "insertrvo"
5) "rv0"
6) "insertrvo"
 

⑧小结:

实际上是一个链表,before Node after , left,right 都可以插入值
如果key 不存在,创建新的链表
如果key存在,新增内容
如果移除了所有值,空链表,也代表不存在!
在两边插入或者改动值,效率最高! 中间元素,相对来说效率会低一点~
消息排队!消息队列 (Lpush Rpop), 栈( Lpush Lpop)!

3、Set(无序,不可重复)

1、sadd(添加)、smembers(查看所有元素)、sismember(判断是否存在)、scard(查看长度)、srem(移除指定元素)操作

127.0.0.1:6379> sadd myset v1 v2 v3 v4 #添加set集合(可批量可单个,写法一致,与之前的写法一致)
(integer) 4
127.0.0.1:6379> SMEMBERS myset  #查看集合myset中的元素
1) "v4"
2) "v3"
3) "v2"
4) "v1"
127.0.0.1:6379> SISMEMBER myset v1  #判读集合myset中的v1元素是否存在,存在返回1
(integer) 1
127.0.0.1:6379> SISMEMBER myset v5 #判读集合myset中的v5元素是否存在,不存在返回0
(integer) 0
127.0.0.1:6379> SCARD myset        #查看集合myset的大小 ,相当于size、length
(integer) 4
127.0.0.1:6379> SREM myset v2      #移除myset中的 v2元素
(integer) 1
127.0.0.1:6379> smembers myset     #移除成功
1) "v4"
2) "v3"
3) "v1"

2、srandmember(抽随机)操作

127.0.0.1:6379> SMEMBERS myset #查看myset集合中的所有元素
1) "v2"
2) "v1"
3) "v3"
4) "v4"
127.0.0.1:6379> SRANDMEMBER myset 1 #在myset集合中随机抽取1个元素
1) "v1"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v1"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v2"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v1"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v4"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v1"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v2"
127.0.0.1:6379> SRANDMEMBER myset 1
1) "v3"
127.0.0.1:6379> SRANDMEMBER myset 3 #在myset集合中随机抽取3个元素
1) "v4"
2) "v2"
3) "v1"
127.0.0.1:6379> SRANDMEMBER myset 3
1) "v4"
2) "v3"
3) "v2"
127.0.0.1:6379> SRANDMEMBER myset 3
1) "v4"
2) "v3"
3) "v2"

3、spop(随机删除元素)、smove(移动指定元素到新的集合中)操作

127.0.0.1:6379> SMEMBERS myset #查看myset集合中的所有元素
 1) "v1"
 2) "7"
 3) "8"
 4) "3"
 5) "v3"
 6) "4"
 7) "6"
 8) "5"
 9) "1"
10) "2"
11) "v4"
12) "v2"
13) "10"
14) "9"
127.0.0.1:6379> spop myset  #随机移除myset中1个元素,不指定参数值即删除1个
"1"
127.0.0.1:6379> spop myset
"4"
127.0.0.1:6379> spop myset
"2"
127.0.0.1:6379> spop myset
"6"
127.0.0.1:6379> spop myset 1 #随机移除myset当中的指定数量1个元素
1) "7"
127.0.0.1:6379> spop myset 2 #随机移除myset当中的指定数量2个元素
1) "8"
2) "v2"
127.0.0.1:6379> SMEMBERS myset #查看移除后的myset当中的元素
1) "3"
2) "v3"
3) "5"
4) "v1"
5) "v4"
6) "10"
7) "9"
127.0.0.1:6379> SMEMBERS myset2
1) "1"
2) "2"
3) "3"
4) "5"
127.0.0.1:6379> SMOVE myset myset2 v1 #把myset集合中的 v1元素移动myset2中
(integer) 1
127.0.0.1:6379> SMEMBERS myset2
1) "5"
2) "2"
3) "3"
4) "1"
5) "v1"

4、SDIFF(差集)、DINTER(交集)、SUNION(并集)

127.0.0.1:6379> SMEMBERS myset #查看myset集合当中的所有元素
1) "3"
2) "v3"
3) "1"
4) "2"
5) "v4"
6) "10"
7) "9"
127.0.0.1:6379> SMEMBERS myset2 #查看myset2集合当中的所有元素
1) "2"
2) "3"
3) "5"
4) "1"
5) "v1"
127.0.0.1:6379> SDIFF myset myset2 #查看差集myset与myset2当中不同的元素,显示myset不同的值
1) "v4"
2) "v3"
3) "10"
4) "9"
127.0.0.1:6379> SINTER myset myset2 #查看myset与myset2当中相同的元素(交集)
1) "2"
2) "3"
3) "1"
127.0.0.1:6379> SUNION myset myset2 #查看myset与myset2当中所有的元素(并集)
1) "3"
2) "5"
3) "v3"
4) "v4"
5) "1"
6) "10"
7) "9"
8) "v1"
9) "2"

5、总结

应用于:共同好友,共同关注等业务操作

4、Hash(哈希)

Map集合key-map(map=key-value) ,这个值是一个map集合,本质和String类型没有区别

1、hset(添加hash)、hmset(添加多个hash)、hget(获取hash)、hmget(获取多个hash)、hgetall(获取所有hash)、hdel(删除hash中指定的值)、hlen(获取hash的长度)、hexists(判断key是否存在)操作

#hset、hmset
127.0.0.1:6379> hmset myhash k1 1 k2 2 k3 3 #set名为myhash的key 设置多个值,可单个设置
OK
127.0.0.1:6379> keys * 
1) "myhash"
#hgetall
127.0.0.1:6379> HGETALL myhash #获取key为myhash下的所有数据
1) "k1"
2) "1"
3) "k2"
4) "2"
5) "k3"
6) "3"
#hget、hmget
127.0.0.1:6379> hmget myhash k1 k2 k3 #获取多个字段值
1) "1"
2) "2"
3) "3"
#hdel
127.0.0.1:6379> hdel myhash k1 #删除hash指定key字段 可删除多个字段
(integer) 1
#hlen
127.0.0.1:6379> hlen myhash   #获取hash表的字段数 
(integer) 2
#HEXISTS
127.0.0.1:6379> HEXISTS myhash k1 #判断hash下key是否存在,不存在返回0
(integer) 0
127.0.0.1:6379> HEXISTS myhash k2 #判断hash下key是否存在,存在返回1
(integer) 1

2、hkeys(获取所有key)、hvals(获取所有value)、hincrby(给值加增量)、hsetnx(存在不添加)操作

127.0.0.1:6379> hgetall myhash #获取hash中的key-value
1) "k2"
2) "2"
3) "k3"
4) "3"
5) "k1"
6) "v1"
127.0.0.1:6379> HKEYS myhash #获取hash中的key
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> HVALS myhash #获取hash中的key下的value
1) "2"
2) "3"
3) "v1"
127.0.0.1:6379> hset x k4 1 #设置一个属性
(integer) 1
127.0.0.1:6379> hincrby myhash k4 1 #让hash中k4的value指定+1(自增)
(integer) 2
127.0.0.1:6379> hincrby myhash k4 1
(integer) 3
127.0.0.1:6379> hincrby myhash k4 1
(integer) 4
127.0.0.1:6379> HSETNX myhash k5 v5 #在hash中添加不存在字段,不存在返回1
(integer) 1
127.0.0.1:6379> HSETNX myhash k5 v5 #在hash中添加不存在字段,存在返回0
(integer) 0
127.0.0.1:6379> 
#对象的存储
127.0.0.1:6379> hmset user:1 name lsa  age 22 #创建一个hash下面有两个字段分别为name age
OK
127.0.0.1:6379> hgetall user:1 #获取hash下的key-value
1) "name"
2) "lsa"
3) "age"
4) "22"
127.0.0.1:6379> hmget user:1 name age
1) "lsa"
2) "22"
127.0.0.1:6379> hget user:1 name 
"lsa"

总结:比String更加适合存对象~

5、zSet(有序集合)

1、zadd(添加)、zrange(查询)、zrangebyscore(排序小-大)、zrevrange(排序大-小)、zrangebyscore withscores(查询所有值包含key)操作

127.0.0.1:6379> zadd  myzset 1 v1 2 v2 3 v3 4 v4 5 v5 #添加zset集合的值,可以添加多个
(integer) 5
127.0.0.1:6379> zrange myzset 0 -1 #查询myzset集合中所有的元素
1) "v1"
2) "v2"
3) "v3"
4) "v4"
5) "v5"
#zrangebyscore
127.0.0.1:6379> zrangebyscore myzset -inf +inf #排列myzset中的值,升序排列从小-大
1) "v1"
2) "v2"
3) "v3"
4) "v4"
5) "v5"
#zrevrange
127.0.0.1:6379> zrevrange myzset 0 -1 #从大到小排序输出 后面可加参数 新版本
1) "v5"
2) "v4"
3) "v3"
4) "v2"
5) "v1"
#zrangebyscore withscore
127.0.0.1:6379> zrangebyscore myzset -inf +inf withscores  #查询指定zset的所有值,包含序号的值
 1) "v1"
 2) "1"
 3) "v2"
 4) "2"
 5) "v3"
 6) "3"
 7) "v4"
 8) "4"
 9) "v5"
10) "5"
#指定key范围
127.0.0.1:6379> ZRANGEBYSCORE myzset 0 1 #按照顺序查找myzset集合中 指定0<=key<=1的值
1) "v1"
127.0.0.1:6379> ZRANGEBYSCORE myzset (0 2 #按照顺序查找myzset集合中 指定0<key<=2的值
1) "v1"
2) "v2"
127.0.0.1:6379> ZRANGEBYSCORE myzset (1 2 #按照顺序查找myzset集合中 指定1<key<=2的值
1) "v2"
127.0.0.1:6379> zrange myzset -inf +inf byscore #和上面查询方式一样
1) "v1"
2) "v2"
3) "v3"
4) "v4"
5) "v5"

2、zrem(移除元素)、zcard(查看元素个数)、zcount(查询指定区间内的元素个数)操作

127.0.0.1:6379> zrange myzset 0 -1 #获取myzset集合所有元素
1) "v1"
2) "v2"
3) "v3"
4) "v4"
5) "v5"
127.0.0.1:6379> zrem myzset v1  #删除myzset集合下的v1元素
(integer) 1
127.0.0.1:6379> zrange myzset 0 -1 
1) "v2"
2) "v3"
3) "v4"
4) "v5"
127.0.0.1:6379> zcard myzset #获取myzset集合的长度类似于size length
(integer) 4 
127.0.0.1:6379> zcount myzset 0 50 #查询指定区间内的元素个数
(integer) 4
127.0.0.1:6379> zcount myzset 0 2  #查询指定区间内的元素个数
(integer) 1

3、总结

案例分析:成绩表排序,工资表排序,年龄排序等需求可以用zset来实现!

普通消息:1重要消息,2.带权重进判断

排行榜应用实现: 取top n测试

4、三种特殊数据类型

1、geospatial**:**地理位置

在这里插入图片描述

城市经纬度查询:https://jingweidu.bmcx.com/
注意点1:两极无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
注意点2:有效的经度从-180度到180度。(东西)
注意点3:有效的纬度从-85.05112878度到85.05112878度。(南北)
注意点4:m 为米。km 为千米。mi 为英里。ft 为英尺。

1、geoadd(添加)、geopos(查看)、

#GEOADD
127.0.0.1:6379>GEOADD china:city 113.27324 23.15792 guangzhou 114.115097 22.53237 shenzhen  #添加城市经纬度 语法格式: geoadd key 经度(东西) 纬度(南北) name    可多个添加
(integer) 2
127.0.0.1:6379> keys * #查询key的值
1) "china:city"
127.0.0.1:6379> GEOADD china:city 106.54041 29.40268 chongqing 114.042824 33.570889 luohe 
(integer) 2  
#GEOPOS
127.0.0.1:6379> GEOPOS china:city  beijing #查询china:city集合中,指定城市的经纬度信息
1) 1) "116.39746695756912231"
   2) "39.90882092130541992"
127.0.0.1:6379> GEOPOS china:city beijing shanghai #查询china:city集合中,多个指定城市的经纬度信息
1) 1) "116.39746695756912231"
   2) "39.90882092130541992"
2) 1) "121.45570546388626099"
   2) "31.24960003848300261"
127.0.0.1:6379> zrange china:city 0 -1 #查询china:city集合中所有元素
1) "chongqing"
2) "shenzhen"
3) "guangzhou"
4) "shanghai"
5) "luohe"
6) "beijing"


2、geodist (计算距离)两点之间操作

127.0.0.1:6379> geodist china:city beijing luohe    #计算两地之间的距离,默认返回单位是m
"735439.9500"
127.0.0.1:6379> geodist china:city beijing luohe km #计算两地之间的距离,指定返回单位是km
"735.4399"
127.0.0.1:6379> geodist china:city beijing luohe m  #计算两地之间的距离,指定返回单位是m
"735439.9500"
127.0.0.1:6379> geodist china:city beijing luohe mi #计算两地之间的距离,指定返回单位是mi 英里 
"456.9823"
127.0.0.1:6379> geodist china:city beijing luohe ft #计算两地之间的距离,指定返回单位是ft 英尺
"2412860.7283"

3、georadius(以坐标查询附近位置)操作

127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "shenzhen"
3) "guangzhou"
4) "shanghai"
5) "luohe"
6) "beijing"
#georadius 查找附近的人 好友功能 因为他是自己给坐标,而georadiusbymember是在指定里的元素
127.0.0.1:6379> georadius china:city 110 30 1000 km  #查看指定位置的1000公里范围内有哪些城市
#查询在china:city集合中,以经度110,纬度30为中心范围1000km的城市
1) "chongqing"
2) "shenzhen"
3) "guangzhou"
4) "luohe"
127.0.0.1:6379> georadius china:city 110 30 500 km #查看指定位置的500公里范围内有哪些城市
1) "chongqing"
#withdist
127.0.0.1:6379> georadius china:city 110 30 500 km  withdist #查看指定位置的500公里范围内有哪些城市,withdist显示范围内该城市到中心位置的距离
1) 1) "chongqing"
   2) "340.7667"
#withcoord
#查看指定位置的550公里范围内有哪些城市,withcoord显示范围内该城市的’经纬度‘值
127.0.0.1:6379> georadius china:city 110 30 500 km  withcoord
1) 1) "chongqing"
   2) 1) "106.54040783643722534"
      2) "29.40268053517299762"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist  withcoord count 1 #筛选出至指定结果
1) 1) "chongqing"
   2) "340.7667"
   3) 1) "106.54040783643722534"
      2) "29.40268053517299762"
#查看指定位置的550公里范围内有哪些城市,withhash指定返回城市的’经纬度‘的hash值
#如果两个城市的hash值越’像‘,证明城市距离越近!      
127.0.0.1:6379> georadius china:city 110 30 500 km withdist  withcoord withhash
1) 1) "chongqing"
   2) "340.7667"
   3) (integer) 4026046519194590
   4) 1) "106.54040783643722534"
      2) "29.40268053517299762"

4、georadiusbymember(以指定元素查询附近位置)、geohash (返回经纬度的hash值)、zrange、zrem(使用zset命令操作geo)

#georadiusbymember 查找附近的城市
127.0.0.1:6379> georadiusbymember china:city shanghai 1000 km #查询以上海为中心周围1000km 的城市
1) "shanghai"
2) "luohe"
#geohash
127.0.0.1:6379> geohash china:city shanghai luohe  #返回查询城市经纬度的 hash值
1) "wtw3gbc6550"
2) "wtcngp7kw70"
127.0.0.1:6379> zrange china:city 0 -1  #查询所有城市
1) "chongqing"
2) "shenzhen"
3) "guangzhou"
4) "shanghai"
5) "luohe"
6) "beijing"
#zrem
127.0.0.1:6379> zrem china:city chongqing #删除指定城市
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "shenzhen"
2) "guangzhou"
3) "shanghai"
4) "luohe"
5) "beijing"

5、总结:

实际需求中,我们可以用来查询附近的人、计算两人之间的距离等。当然,那些所需的经纬度我们肯定要结合java代码来一次导入,手动查询和录入太过于浪费时间!

2、Hyperloglog: 基数

首先得明白什么是基数
再数学层面上可以说是:

基数是指一个集合中不重复的元素 划重点 是一个集合中不重复的 不是两个集合做对比

但是再Redis中,可能会有一定的误差性。 官方给出的误差率是0.81%
Hyperloglog的优点: 占用的内存是固定的,2^64个元素,相当于只需要12kb的内存即可。效率极高!

1、pfadd(添加数据集)、pfcount(统计数据集)、pfmerge(合并数据集-自动去重)

127.0.0.1:6379> pfadd mydata 1 2 3 4 5 6 7 8 5 4 #添加数据集
(integer) 1
127.0.0.1:6379> PFCOUNT mydata #统计数据集中的元素个数
(integer) 8
127.0.0.1:6379> PFadd mydata1 1  4 2 3 4 5 6 7 8 9 #添加数据集
(integer) 1
127.0.0.1:6379> pfcount mydata #统计数据集中的元素个数
(integer) 8
127.0.0.1:6379> pfcount mydata1 #统计数据集中的元素个数
(integer) 9
#将mydata 和mydata1  两个数据集合并成一个新的 mydata3数据集,并且自动去重
127.0.0.1:6379> pfmerge mydata2 mydata mydata1
OK
127.0.0.1:6379> pfcount mydata2
(integer) 9

2、总结

如果在实际业务中,允许一定的误差值,我们可以使用基数统计来计算~效率非常高!比如:

网站的访问量,就可以利用Hyperloglog来进行计算统计!

3、Bitmap: 位存储

1、setbit(添加)、getset(获取)、bitcount(统计)操作

127.0.0.1:6379> setbit login 1 1  #添加周一已登陆 为1
(integer) 0
127.0.0.1:6379> setbit login 2 1
(integer) 0
127.0.0.1:6379> setbit login 3 1
(integer) 0
127.0.0.1:6379> setbit login 4 0  #添加周四未登陆 为0
(integer) 0
127.0.0.1:6379> setbit login 5 1
(integer) 0
127.0.0.1:6379> setbit login 6 0
(integer) 0
127.0.0.1:6379> setbit login 7 1
(integer) 0
127.0.0.1:6379> getbit login 1  #获取周一打开状态
(integer) 1
127.0.0.1:6379> bitcount login  #统计这周登陆的天数
(integer) 5

2、总结

实际需求中,可能需要我们统计用户的登陆信息,员工的打卡信息等等。只要是事务的只有两个状态的,我们都可以用Bitmap来进行操作!!!

5、事务

1、首先什么是事务呢?

  • 原子性(atomicity)。一个事务是一个不可分割的单位,事务中包含的操作要么都成功,要么都失败
  • 一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(consistency)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

2、Redis事务的本质

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

一次性,顺序性,排他性,执行一系列的命令!

Redis不保证原子性:Redis中,单条命令是保证原子性,但事务不保证原子性,且没有回滚。

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

3、正常的执行事务

127.0.0.1:6379> multi #首先开启书屋
OK
127.0.0.1:6379(TX)> set k1 v1 #添加一个数据
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> mget k1 k2 k3  #获取多个数据
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
#执行完事务,会根据执行的事务,会按照顺序执行事务
1) OK 
2) OK
3) OK
4) 1) "v1"
   2) "v2"
   3) "v3"

4、放弃事务discard

127.0.0.1:6379> multi #开启事务
OK 
127.0.0.1:6379(TX)> set k1 v1 #添加数据
QUEUED
127.0.0.1:6379(TX)> set id 1
QUEUED
127.0.0.1:6379(TX)> set name lsa\
QUEUED
127.0.0.1:6379(TX)> discard #放弃事务
OK
127.0.0.1:6379> get id #然后获取为null
(nil)
127.0.0.1:6379> mget k1 id name #然后获取为null
1) (nil)
2) (nil)
3) (nil)

5、事务的处理机制

在redis中,如果开启了事务,某一条命令执行出错了,此时事务会怎么处理呢?你可能会说了,要么都成功,要么都失败,有一个失败,那就全失败了呗。

这么想不能算错,因为redis中的事务不保证原则性,所以redis对于命令执行错误有两种解决方式:

  • 语法错误(编译)
  • 执行错误(运行)

1、语法错误(编译错误)

127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 #添加数据
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k1 
#getset命令,先获取在设值,因为输入一个错误命令,并且当前在事务中,所以最后执行也会报错
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> EXEC  #执行事务,报错,并且所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 #获取数据为空,证明没有执行
(nil)

2、执行错误(运行)

127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 #添加数据
QUEUED
127.0.0.1:6379(TX)> incr k1 #对k1 进行自增 因为语法命令没有错误所以设置成功
QUEUED
127.0.0.1:6379(TX)> set k2 v2 
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行
1) OK 
#因为本质是一组命令,这些命令被序列化,在执行过程中会按照顺序执行
2) (error) ERR value is not an integer or out of range
3) OK
4) OK
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"

上面的事务中,语法是没有问题的,所以在运行之前redis是无法发现错误的,但是在执行的时候出现了错误

所以可以得出,在redis当中单条的命令是保证事务的(就是错了都错),而在事务中是不能保证的

6、Redis乐观锁监视测试(watch)

1、正常执行

127.0.0.1:6379> set money 100 #设置数据
OK
127.0.0.1:6379> set disburse 0
OK
127.0.0.1:6379> watch money  #监视 money 对象
OK
127.0.0.1:6379> multi #事务正常结束,期间没有不发生变化,这个时候就正常执行
OK
127.0.0.1:6379(TX)> decrby money 10 
QUEUED
127.0.0.1:6379(TX)> incrby disburse 10
QUEUED
127.0.0.1:6379(TX)> exec #执行
1) (integer) 90
2) (integer) 10

2、进行多线程修改,使用watch可以当作redis的乐观锁操作!

需要开两个窗口,其中一个线程正常进入事务,而另一个线程进行插队操作,对事务要操作的对象进行修改

线程1:

127.0.0.1:6379> watch money #对money 对象进行监控
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> decrby money 30 
QUEUED
127.0.0.1:6379(TX)> incrby disburse 30
QUEUED
127.0.0.1:6379(TX)> exec 
#执行事务,会发现返回null,因为我们开启了watch操作,在此次事务未执行前,我们让线程2先进行了对,对象的修改
(nil)

线程2:

#以下内容在执行事务前执行
127.0.0.1:6379> incrby money 50 
(integer) 140
127.0.0.1:6379> get money
"140"

不管事务执行成功与否,事务都会被取消掉,需要监控,需要重新添加

总结:乐观锁和悲观锁的区别。
悲观锁: 什么时候都会出问题,所以一直监视着,没有执行当前步骤完成前,不让任何线程执行,十分浪费性能!一般不使用!
乐观锁: 只有更新数据的时候去判断一下,在此期间是否有人修改过被监视的这个数据,没有的话正常执行事务,反之执行失败!

6、springboot整合:Redis在Jedis中如何使用和操作?

Jedis是Redis官方推荐的Java连接开发工具! 虽然现在的SpringBoot2.×版本已经将Jedis换成了Lettuce,但是我觉得还是有必要了解一下Jedis的使用!

1、如何在java项目中整合Jedis并且连接Redis数据库?

1、创建一个Maven项目

在这里插入图片描述

2、导入依赖

<!--导入jedis的包-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency> 
<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
 </dependency>

3、链接测试,这次我就用本地进行测试

  public static void main(String[] args) {
        // 1、 new Jedis 对象即可
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        // jedis 所有的命令就是我们之前的所有指令
        System.out.println(jedis.ping());
    }

在这里插入图片描述

4、常用命令练习

 // 1、 new Jedis 对象即可
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();//清空当前库的所有数据
        jedis.set("name", "lsa");
        jedis.set("age", "22");
        jedis.set("high", "176");
        System.out.println("name:" + jedis.get("name") + "\nage:" + jedis.get("age") + "\nhigh" + jedis.get("high"));

在这里插入图片描述

public static void main(String[] args) {
    // 1、 new Jedis 对象即可
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.lpush("list", "1", "2", "3");
    System.out.println("list:" + jedis.lrange("list", 0, -1));
}

在这里插入图片描述

2、jedis再次理解redis事务

 public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "kuangshen");
        // 开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();
        // jedis.watch(result)
        try {
            //在事务中设置值
            multi.set("user1", result);
            // 执行事务
            multi.exec();
        } catch (Exception e) {
            // 放弃事务
            multi.discard();
        } finally {
            // 关闭连接
            System.out.println(jedis.get("user1"));
            jedis.close();
        }
    }

可以看出上面的命令和redis一模一样,可能只是写起来不方便

3、jedis乐观锁监控实现

  public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
//        jedis.set("money", "100");
//        jedis.set("out", "0");
        jedis.watch("money");
        //上面尚未开启事务
        //开启事务
         Transaction multi = jedis.multi();
   try {
        multi.decrBy("money", 10);
        multi.incrBy("out", 10);
        multi.exec();
        } catch (Exception e) {
            //放弃事务
            multi.discard();
        } finally {
            System.out.println("现有的钱:" + jedis.get("money"));
            System.out.println("支出:" + jedis.get("out"));
        }
    }

在这里插入图片描述

7、Springboot集合Redis

详情:

SpringBoot 在1.x版本的时候,底层还是用的 使用Jedis来连接Redis的,

但是在2.×版本后,就换成了Lettuce。

springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。

两者的区别如下:

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

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

1、基本环境搭建与源码分析

在这里插入图片描述

添加以上依赖

我们在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名。Redis也不例外。

下方为Redis的配置文件:

在这里插入图片描述

详细代码内容:

在这里插入图片描述

源码分析:

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
   //如果我们自己编写一个redisTemplate可以来替换这个默认的
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
        //默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的!
        //两个<Object, Object>的类型。我们后期使用需要强制转换
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
//由于String是Redis中最常使用的类型,所以说单独提出一个bean
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

根据上部分析,在进行深层分析我们可以看到默认的redisTemplate类中有一个参数 RedisConnectionFactory,点击去可以看到,接口下有两个实现类分别点进去:在这里插入图片描述

点进去后我们可以发现,Jedis有很多类不存在,所以不生效已经弃用,这就是前面所说在2.x版本后启用更加安全的去链接

2、整合测试

1、配置redis

#配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379

2、测试

@SpringBootTest
class Redis02SpringbootApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //redisTemplate
        //opsForValue()         操作字符串        类似于Redis中的String
        //opsForList()          操作List         类似于Redis中的List
        //opsForSet()           操作set          类似于Redis中的Set
        //opsForHash()          操作Hash         类似于Redis中的Hash
        //opsForZSet()          擦作ZSet         类似于Redis中的ZSet
        //opsForGeo()           擦作Geo          类似于Redis中的Geo
        //opsForHyperLogLog()   擦作HyperLogLog  类似于Redis中的HyperLogLog

        //除了基本内容的操作,我们常用的方法都可以使用redisTemplate操作,比如事务,和基本的CRUD操作
        //获取链接对象
//        RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
//        redisConnection.flushDb();
//        redisConnection.flushAll();
        redisTemplate.opsForValue().set("k1", "卢申澳学Redis");
        System.out.println(redisTemplate.opsForValue().get("k1"));
    }

}

测试成功,但是在命令终端查看却是乱码,是因为没有进行序列化

在这里插入图片描述

==如何解决这个问题呢?如一下分析:==自定义一个RedisTemplate来替换这个默认的

3、定制RedisTemplate的模板:

1、序列化的配置如下图所示:

在这里插入图片描述

2、默认是使用jdk进行的序列化,我们可以使用JSON进行序列化,前面也提到了可以自己编写一个redisTemplate来替换这个默认的

在这里插入图片描述

3、自定义confing,redisTemplate

新增一个对象

@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private int age;
}

无序列化配置

  • 测试代码
 @Test
    public void test() throws JsonProcessingException {
        User user = new User("lsa", 22);
        //直接赋值一个对象
        redisTemplate.opsForValue().set("user", user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

保存如下:

在这里插入图片描述

因为我们的user对象没有进行序列化,所以保存

序列化配置

自定义封装RedisTemplate类

上面说了大体上可以实现,但是为了在工作中更容易操作Redis,我们一般重新封装RedisTemplate类,如下所示:

package com.lsa.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;

@Configuration
public class RedisConfig {
    // 这是我给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
    // 自己定义了一个RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

测试代码:

 @Test
    public void test() throws JsonProcessingException {
        User user = new User("lsa", 22);
        //String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user", user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

测试结果:

User(name=lsa, age=22)

3、封装RedisUtils类

在实际工作中,我们不可能用RedisTemplate 来操作Redis的,因为实在太繁琐,所以我们一般自定义一个RedisUtils工具类来操作Redis!

package com.lsa.util;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    public Set<String> keys(String keys) {
        try {
            return redisTemplate.keys(keys);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 指定缓存失效时间
     *
     * @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((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }

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

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

    /**
     * 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);
    }

    /**
     * 根据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 值
     * @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 值
     * @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;
        }
    }
}

使用RedisUtil

    @Autowired
    private RedisUtil redisUtil;

    @Test
    public void test() throws JsonProcessingException {
        User user = new User("lsa4", 22);
        redisUtil.set("user1", user);
        System.out.println(redisTemplate.opsForValue().get("user1"));
    }

测试成功

8、Redis配置文件Redis.conf详情

启动的时候就是通过配置文件进行启动的!!

单位

在这里插入图片描述

1、配置文件unti单位,对大小不敏感

包含

在这里插入图片描述

就类似于java中的improt,include

网络

bind 127.0.0.1 -::1 # 绑定的ip
protected-mode yes #保护模式
port 6379 #端口设置

通用 GENERAL

daemonize yes #刚开始默认是no,后来我们改为yes 后台运行守护进程

pidfile /var/run/redis_6379.pid 
#如果 daemonize yes 为yes表示后台运行,我们就需要指定一个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 "" # 日志的标准文件名,如果为null就是标准的输出
databases 16 # 数据库的数量,默认是16个数据库
always-show-logo no # 是否显示logo

快照 SNAPSHOTTING

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

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

# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 change was performed
#   * After 300 seconds (5 minutes) if at least 100 changes were performed
#   * After 60 seconds if at least 10000 changes were performed
#
# You can set these explicitly by uncommenting the following line.
#
# save 3600 1 300 100 60 10000

save 3600 1      # 如果3600内有,如果至少有1个key进行了修改,我们就进行持久化操作
save 300 100     # 如果300内有,如果至少有100个key进行了修改,我们就进行持久化操作
save 60 10000    # 如果60内有,如果至少有10000个key进行了修改,我们就进行持久化操作

stop-writes-on-bgsave-error yes # 持久化出现错误的时候,是否继续工作 yes为是

rdbcompression yes # 是否压缩rdb文件,需要消化cpu资源

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

dir ./ #rdb 文件保存的目录

REPLICATION 主从复制 ,后面讲解

#   +------------------+      +---------------+
#   |      Master      | ---> |    Replica    |
#   | (receive writes) |      |  (exact copy) |
#   +------------------+      +---------------+

SECURITY 安全

1、通过命令设置密码

127.0.0.1:6379> config get requirepass   # 获取Redis密码 默认为null
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456"  #设置Redis密码 
OK
#退出当前链接重新连接
[root@localhost bin]# redis-cli -p 6379
127.0.0.1:6379> ping #在进行ping 返回错误
(error) NOAUTH Authentication required. 
127.0.0.1:6379> set name lsa #设置一个值 也返回错误 无权限
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # auth + 密码 进行登录
OK
127.0.0.1:6379> ping #正常使用
PONG
127.0.0.1:6379> config get requirepass  #获取密码,正常
1) "requirepass"
2) "123456"

2、配置文件进行修改密码

在这里插入图片描述

限制 CLIENTS

maxclients 10000 # 设置能连接上redis的最大客户端的数量

maxmemory <bytes>  # redis 配置最大的内存容量  

maxmemory-policy noeviction  # 内存到达上限之后的处理策略 
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
2、allkeys-lru : 删除lru算法的key 
3、volatile-random:随机删除即将过期key 
4、allkeys-random:随机删除 
5、volatile-ttl : 删除即将过期的 
6、noeviction : 永不过期,返回错误

APPEND ONLY MODE aof的配置

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

9、Redis持久化之RDB和AOF

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

一. RDB(Redis DataBase)

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

有时候我们在生产环境会备份

在这里插入图片描述

1、首先我们进入服务器找到dump.rdb文件:

路径:/usr/local/bin

在这里插入图片描述

2、触发机制:

  • save规则满足会触发机制

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

    • 首先删除dump.rdb然后

      在这里插入图片描述

    • 然后执行flushall

    • 会发现du mp.rdb会再次出现

  • 退出Redis(shutdown),也会触发rdb规则

    在这里插入图片描述

    • 在次查看rump.rdb,也会出现了

3、恢复rdb文件

  • 1、只需将备份的rdb文件放在我们的redis启动目录即可,Redis启动的时候会自动检查dump.rdb文件并恢复其中的数据!
  • 2、查找文件位置的命令:
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"  # 只需要在这个目录下存在 dump.rdb 文件,启动就会自动恢复其中的数据

4、优缺点:

  • 优点:

    • 1、适合大规模的数据恢复!

    • 2、对数据的完整性要求不高

  • 缺点:

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

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

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

二. AOF(Append Only File)

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

1、Redis默认使用的是RDB模式,所以需要手动开启AOF模式!

在这里插入图片描述

默认是不开启的,需要手动配置

重启服务器!

查看多了一个文件进入可以看到

在这里插入图片描述

由于redis7的新特性在Redis7之后以上版本,appendonly.aof不会以单独文件出现,而会在appendonlydir文件夹内存在

进入文件可以看到:

在这里插入图片描述

在redis7以及以后的版本中,AOF持久化的数据将存在于一个文件夹中
 
仅追加文件名是由Redis按照特定的模式创建的。 
 
文件名前缀是基于'appendfilename'配置的 
 
参数,后面是关于序列和类型的附加信息。
 
例如,如果appendfilename设置为appendonly.Aof,下面的文件 
 
names可以被派生: 
 
appendonly.aof.1.base.RDB作为基本文件。 
 
appendonly.aof.1.incr.aof appendonly.aof.2.incr.aof 作为增量文件的Aof。 
 
将appendonly.aof.manifest作为一个manifest文件。

2、aof中的内容

  • 我们添加几个值

在这里插入图片描述

  • 去查看vim appendonly.aof.1.incr.aof

在这里插入图片描述

里面存储的就是我们刚才执行的命令

3、修复aof文件:

  • 1、如果有个 将我们的aof文件给修改了,加了点乱七八糟的东西,我们该如何修复呢?如下图所示:

在这里插入图片描述

  • 2、重启下Redis看看:发现重启失败!报错配置信息加载失败
    在这里插入图片描述

  • 3、我们可以使用redis-check-aof文件来进行修复!

    redis-check-aof --fix appendonly.aof  #修复appendonly.aof文件
    

在这里插入图片描述

可以看到正常了

4、AOF重写规则!

aof默认的就是文件的无限追加,文件会越来越大!在配置文件中可以设置文件的大小!

在这里插入图片描述

# appendfsync always # 每次修改都会 sync。消耗性能 
appendfsync everysec # 每秒执行一次 sync,可能会丢失这1s的数据! # appendfsync no # 不执行 sync,这个时候操作系统自己同步数据,速度最快!


appendonly no # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下, rdb完全够用!

auto-aof-rewrite-percentage 100  #写入百分比
auto-aof-rewrite-min-size 64mb  #写入的文件最大值是多少,一般在实际工作中我们会将其设置为5gb左右!

5、优缺点!
优点:
1、每一次修改都同步,文件的完整性会更加好!
2、每秒同步一次,最多会丢失一秒的数据!
3、从不同步,效率最高的!

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

6、总结:
1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式

在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

7、性能建议

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这条规则。
如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

10、Redis如何实现发布订阅功能?

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。
Redis客户端可以订阅任意数量的频道!

在这里插入图片描述

一、实现方式:

1、命令:

这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
命令 描述
PSUBSCRIBE pattern [pattern…] 订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE pattern [pattern…] 退订一个或多个符合给定模式的频道。
PUBSUB subcommand [argument[argument]] 查看订阅与发布系统状态。
PUBLISH channel message 向指定频道发布消息
SUBSCRIBE channel [channel…] 订阅给定的一个或多个频道。
SUBSCRIBE channel [channel…] 退订一个或多个频道

在这里插入图片描述

2、发布订阅的实现:

1、订阅端

127.0.0.1:6379> SUBSCRIBE lsastudyredis  #订阅带码  订阅名称
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lsastudyredis"
3) (integer) 1
#等待推送的信息
1) "message"  #消息
2) "lsastudyredis" #来自哪个频道
3) "lsaloveredis" #消息内容
1) "message" 
2) "lsastudyredis"
3) "lsaloveJAVA"

2、发送端

127.0.0.1:6379> publish lsastudyredis "lsaloveredis" #发送消息到lsastudyredis 频道
(integer) 1
127.0.0.1:6379> publish lsastudyredis "lsaloveJAVA"
(integer) 1

3、命令详情

1、PSUBSCRIBE 命令: 订阅指定频道!

PSUBSCRIBE + 频道。。 #订阅给定的模式,可多个

在这里插入图片描述

2、PUBLISH 命令:发送消息至指定频道!

PUBLISH + 频道 +消息  #将信息 message 发送到指定的频道 channel

在这里插入图片描述

3、PUNSUBSCRIBE命令:退订!

PUNSUBSCRIBE 客户端 #指示客户端退订指定模式,若果没有提供模式则退出所有模式。

4、SUBSCRIBE:订阅

5、UNSUBSCRIBE:退订

6、总结:

  • 缺点
    • 1、如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
    • 2、这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
  • 应用
    消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
    多人在线聊天室。
    稍微复杂的场景,我们就会使用消息中间件MQ处理。

11、Redis集群之如何配置主从复制模式?

一、概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主。
主要作用:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

二、环境配置(单机集群)

1、基本查看命令:

在这里插入图片描述

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:29740cba6ae07720f214eb9a4dc6cd867964f690
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:03

2、配置并开启三台服务:

配置需要修改的内容:1.端口 2.pid名字 3.log文件名 4.dump.rdb名

在这里插入图片描述

修改完后全部开启:

三、一主二从(单机测试)

意思:就是一个主人,两个仆人,主人可以读写,而仆人只能查看

1、找主人 (slaveof 主机名 端口号)

在这里插入图片描述

127.0.0.1:6380> ping
PONG
127.0.0.1:6380> slaveof 127.0.0.1 6379  #让本机认6379的机器为大哥!
OK
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:3
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0

2、主机链接信息

在这里插入图片描述

127.0.0.1:6379> info replication
# Replication
role:master #角色 : 主人
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=532,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=532,lag=1
master_failover_state:no-failover
master_replid:60f0be71723d77d32b8d719a9586c6c7040b473d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:532
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:532

以上内容,已经配置完成

3、注意:通过命令配置的是"一次性de",如果机器出现问题,如宕机、断电,就需要重新认大哥,在实际工作中一般在配置文件中,如下图所示:

在这里插入图片描述

4、测试读写范围和操作

  • 1.主机写,从机读

    在这里插入图片描述

  • 2.如果主机断开,从机正常读

在这里插入图片描述

从机信息:

在这里插入图片描述

证明,虽然主机断开了,但是从机还是可以正常读取原先就有的数据的!

3、如果断开的主机重新连接上

在这里插入图片描述

从机也可以正常查看,因为在从机上配置了,会自动查询主机

4、如果从机重新链接呢?

在从机断开后,主机设置一个值,我们查看,从机是否可以获取到

我们让80端口的从机保持不动,81的端口,然后设置一个值

  • 1.断开81端口的从机

  • 2.在79端口主机设置一个值并查看链接信息:
    在这里插入图片描述
    在这里插入图片描述

  • 查看未断开的80端口:

    在这里插入图片描述

  • 查看断开的81端口以及链接信息:
    在这里插入图片描述
    在这里插入图片描述

    发现81的信息已经断开了

证明:如果从机断开重连,不会自动连接上主机!因为我们的配置是在从机上写的,而且是命令写的,重启时会重置!

5、从机能写嘛?

在这里插入图片描述

从机只能读,不能写!

6、复制原理:

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

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

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

增量复制: Master 继续将新的所有收集到的修改命令依次传给slave,完成同步但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中
看到!

7、层层链路

在这里插入图片描述

这时候也可以完成我们的主从复制!

就是79为主机,而80链接主机,81链接80,这个时候80即使从机也是主机

8、谋朝篡位

如果主机断开了连接,我们可以使用

SLAVEOF no one

让自己变成主机!其他的节点就可以手动连接到最新的这个主节点(手动)!如果这个时候原来的老大修复了,那就重新连接成为小弟!

在这里插入图片描述

注意哦!!
老大没挂,也可以使用这个命令直接让自己变成老大!

四、总结

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:

1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;

2、从容量上,单个Redis服务器内存容量有限,就算一Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该20G。

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

只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!

12、Redis集群之哨兵模式

接着上面来说,前面是如果我们的主机挂了,我们需要手动的把一台服务器,变成主机,这样非常繁琐,下面我们用哨兵模式来简化前面的操作

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式

1、概述

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

就是Redis的哨兵它会给服务器发生心跳包进行确认是否存在

在这里插入图片描述

这里的哨兵有两个作用

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

2、配置哨兵模式

1、添加哨兵模式需要的配置文件内容

在这里插入图片描述

内容:

# sentinel monitor 被监控的名称 host     port 1 
  sentinel monitor myredis   127.0.0.1 6379 1

2、启动哨兵
命令:

redis-sentinel lsaconfig/sentinel.conf 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DeDX8NR2-1681886165949)(C:\Users\卢申澳\Desktop\上课笔记\JAVA全栈\Redis\img\6abed4f9529b9f585c50543954433e8.png)]

3、测试(一个主机二个从机)

1、准备

在这里插入图片描述

2、现在我们把主机关机进行查看变化

在这里插入图片描述

可以看到,当我把主机进行关机后,80从从机变成了主机,redis哨兵会自动检查进行重新设置一个新的主机

现在将刚才的79主机从新链接看一下

在这里插入图片描述

可以看到刚才主机变成了从机,并且链接到了新主机80上面

3、总结

1、优点
①哨兵集群,基于主从复制模式 ,所有的主从配置优点,它全有
②主从可以切换,故障可以转移 ,系统的 可用性 就会更好
③哨兵模式就是主从模式的升级,手动到自动,更加健壮!
2、缺点
①Redis 不好在线扩容 的,集群容量一旦到达上限,在线扩容就十分麻烦!
②实现哨兵模式的配置其实是很 麻烦 的,里面有很多选择!

3、注意点:以上所有的配置因为条件所限都是基于单机集群的前提下!有兴趣的可以自己搭建下正式集群下的多哨兵模式来监控!如下图:(后期会进行尝试多集群)
在这里插入图片描述

4、哨兵的配置文件解析(网上找的,只需要关注几个重点!):

# Example sentinel.conf 

# 哨兵sentinel实例运行的端口 默认26379 
port 26379 

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

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

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

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

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


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


# SCRIPTS EXECUTION #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知 相关人员。 
#对于脚本的运行结果有以下规则: 
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等), 将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信 息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配 置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无 法正常启动成功。 
#通知脚本 
# shell编程 
# sentinel notification-script <master-name> <script-path> 
sentinel notification-script mymaster /var/redis/notify.sh 


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

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

1、缓存穿透(查不到)

缓存穿透产生的原因:请求根本不存在的资源(DB本身就不存在,Redis更是不存在)

在这里插入图片描述

概念

在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。

解决方法

1.布隆过滤器

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

在这里插入图片描述

2.对空值进行缓存

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

例如:

虽然数据库中没有id=-3872的用户的数据,但是在redis中对他进行缓存(key=-3872,value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上

在这里插入图片描述

上面解决方法存在问题:

1.使用空值作为缓存的时候,key设置的过期时间不能太长,防止占用太多redis资源
2.对空值缓存是一种被动的防御方式,当遇到黑客暴力请求很多不存在的数据就需要写入大量的null值到Redis中,可能导致Redis内存占用不足的情况
3.使用布隆过滤器,可以在用户访问的时候判断该资源是否存在,不存在则直接拒绝访问
4.布隆过滤器是有一定的误差,所以一般需要配合一些接口流量的限制(规定用户在一段时间内访问的频率)、权限校验、黑名单等来解决缓存穿透的问题

2、缓存击穿(某个key过期)

产生缓存击穿的原因:redis中的某个热点key过期,但是此时有大量的用户访问该过期key

是指一个非常热点的key,在不停的扛着大并发,当这个key失效时,一瞬间大量的请求冲到持久层的数据库中,就像在一堵墙上某个点凿开了一个洞!

举例:

类似于“某男明星塌房事件”上了热搜,这时候大量的“粉丝”都在访问该热点事件,但是可能优于某种原因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,导致整个DB瘫痪。

在这里插入图片描述

概念

缓存击穿是指当缓存中某个热点数据过期了,在该热点数据重新载入缓存之前,有大量的查询请求穿过缓存,直接查询数据库。这种情况会导致数据库压力瞬间骤增,造成大量请求阻塞,甚至直接挂掉。

解决方案:

1.提前对热点数据进行设置

类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中

2.设置热点key过期时长

监控哪些数据是热门数据,实时的调整key的过期时长

3.使用分布式锁

只有一个请求可以获取到互斥锁,然后到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应

3、Redis的缓存雪崩(大量的key集体过期)

缓存雪崩产生的原因:redis中大量的key集体过期

在这里插入图片描述

例子:

当redis中的大量key集体过期,可以理解为redis中的大部分数据都被清空了(失效了),那么这时候如果有大量并发的请求来到,那么redis就无法进行有效的响应(命中率急剧下降),请求就都打到DB上了,到时DB直接崩溃

概述

缓存雪崩是指当缓存中有大量的key在同一时刻过期,或者Redis直接宕机了,导致大量的查询请求全部到达数据库,造成数据库查询压力骤增,甚至直接挂掉。

解决方法

1.将失效时间分散开

通过使用自动生成随机数使得key的过期时间是随机的,防止集体过期

2.使用Redis高可用

搭建Redis集群,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)

3.设置缓存标记

记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去跟新实际的key

4.限流降级

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

4、总结

三者出现的根本原因:Redis命中率下降,请求直接打在DB上
本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知 相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等), 将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信 息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配 置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无 法正常启动成功。
#通知脚本

shell编程

sentinel notification-script

sentinel notification-script mymaster /var/redis/notify.sh

客户端重新配置主节点参数脚本

当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已 经发生改变的信息。

以下参数将会在调用脚本时传给脚本:

目前总是“failover”,

是“leader”或者“observer”中的一个。

参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通 信的

这个脚本应该是通用的,能被多次调用,不是针对性的。

sentinel client-reconfig-script

sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

一般都是由运维来配置!

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

1、缓存穿透(查不到)

缓存穿透产生的原因:请求根本不存在的资源(DB本身就不存在,Redis更是不存在)

在这里插入图片描述

概念

在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。

解决方法

1.布隆过滤器

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

在这里插入图片描述

2.对空值进行缓存

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

例如:

虽然数据库中没有id=-3872的用户的数据,但是在redis中对他进行缓存(key=-3872,value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上

在这里插入图片描述

上面解决方法存在问题:

1.使用空值作为缓存的时候,key设置的过期时间不能太长,防止占用太多redis资源
2.对空值缓存是一种被动的防御方式,当遇到黑客暴力请求很多不存在的数据就需要写入大量的null值到Redis中,可能导致Redis内存占用不足的情况
3.使用布隆过滤器,可以在用户访问的时候判断该资源是否存在,不存在则直接拒绝访问
4.布隆过滤器是有一定的误差,所以一般需要配合一些接口流量的限制(规定用户在一段时间内访问的频率)、权限校验、黑名单等来解决缓存穿透的问题

2、缓存击穿(某个key过期)

产生缓存击穿的原因:redis中的某个热点key过期,但是此时有大量的用户访问该过期key

是指一个非常热点的key,在不停的扛着大并发,当这个key失效时,一瞬间大量的请求冲到持久层的数据库中,就像在一堵墙上某个点凿开了一个洞!

举例:

类似于“某男明星塌房事件”上了热搜,这时候大量的“粉丝”都在访问该热点事件,但是可能优于某种原因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,导致整个DB瘫痪。

在这里插入图片描述

概念

缓存击穿是指当缓存中某个热点数据过期了,在该热点数据重新载入缓存之前,有大量的查询请求穿过缓存,直接查询数据库。这种情况会导致数据库压力瞬间骤增,造成大量请求阻塞,甚至直接挂掉。

解决方案:

1.提前对热点数据进行设置

类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中

2.设置热点key过期时长

监控哪些数据是热门数据,实时的调整key的过期时长

3.使用分布式锁

只有一个请求可以获取到互斥锁,然后到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应

3、Redis的缓存雪崩(大量的key集体过期)

缓存雪崩产生的原因:redis中大量的key集体过期

在这里插入图片描述

例子:

当redis中的大量key集体过期,可以理解为redis中的大部分数据都被清空了(失效了),那么这时候如果有大量并发的请求来到,那么redis就无法进行有效的响应(命中率急剧下降),请求就都打到DB上了,到时DB直接崩溃

概述

缓存雪崩是指当缓存中有大量的key在同一时刻过期,或者Redis直接宕机了,导致大量的查询请求全部到达数据库,造成数据库查询压力骤增,甚至直接挂掉。

解决方法

1.将失效时间分散开

通过使用自动生成随机数使得key的过期时间是随机的,防止集体过期

2.使用Redis高可用

搭建Redis集群,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)

3.设置缓存标记

记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去跟新实际的key

4.限流降级

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

4、总结

三者出现的根本原因:Redis命中率下降,请求直接打在DB上

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值