狂神说Redis笔记,Redis【入门】就这一篇就够了!

Redis学习笔记

视频链接:狂神说Redis链接

1、Nosql概述

1.1、为什么要使用Nosql

1、单片机Mysql时代

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRT7uGxm-1680869584903)(E:\笔记\typora-user-images\image-20230403155000825.png)]

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

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

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

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

发展过程:优化数据结构索引–>文件缓存–>Memcached(当时的热门技术)

在这里插入图片描述

  1. 优化数据库的数据结构和索引 —>难度稍微有点大
  2. 文件缓存,通过IO流获取比每次都访问数据库效率高,但是随着流量爆炸式增长,IO流也承受不了了。
  3. MemCache,当时最热门的技术,通过在数据库和数据库访问层加一层缓存,第一次访问时候查数据库,将结果存到缓存,之后查询会先查缓存,如果缓存有就直接拿去用了,效率得到了显著的提升。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AveLF2GG-1680869584904)(E:\笔记\typora-user-images\image-20230403160117077.png)]

4、如今的时代

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zaHnYWd2-1680869584904)(E:\笔记\typora-user-images\image-20230403162354369.png)]

为什么要用Nosql?

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

1.2、什么是NoSQL

NoSQL

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

关系型数据库:表格,是由行和列组成的(POI)

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

很多的数据类型用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式!不需要多余的操作就可以横向扩展的!

Map<String,Obiect>就是一的典型的NoSQL

NoSQL的特点

  1. 方便扩展(数据之间没有关系,很好扩展!)
  2. 大数据量高性能(Redis一秒可以写8w次,读取11万次!高性能。NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高)
  3. 数据类型是多样型的!(不需要事先设计数据库!随去随用!)
  4. 传统RDBMS和NoSQL
#传统的RDBMS
- 结构化组织
- SQL
- 数据库和关系都存在单独的表中  row  col
- 操作操作,数据库定义语言
- 严格的一致性
- 基础得到事务
- .....

#NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE (异地多活)!
- 高性能,高可用,高可扩(保证三高)
- .....

了解:3V+3高(不要求掌握)

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

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

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

  1. 高可用
  2. 高可拓(比如随时水平拆分,机器不够可以加台机器)
  3. 高性能(保证用户体验和性能)

真正在公司中时间:NoSQL+RDBMS一起使用才是最厉害的!

技术没有高低之分,就看怎么使用!(提升内功–>思维的提高!)

1.3、阿里巴巴演进分析

思考问题:一个网站那么多东西是在一个数据库吗?

任何一家互联网的公司,都不可能只是简简单单让用户能用就好了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XoAwxI9G-1680869584905)(E:\笔记\typora-user-images\image-20230403164725408.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fAu92a8r-1680869584906)(E:\笔记\typora-user-images\image-20230403165403730.png)]

大量公司做的都是相同的业务

随着这样的竞争,业务是越来越完善,然后对于开发者的要求也是越来越高!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oPwZXjQd-1680869584907)(E:\笔记\typora-user-images\image-20230403165956078.png)]

没有什么是加一层解决不了的,如果不能那就再加一层

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

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

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

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

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

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

大型互联网应用问题:

  • 数据类型太多了
  • 数据源繁多,经常重构!
  • 数据要改造,大面积改造。

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YIAyNSl3-1680869584908)(E:\笔记\typora-user-images\image-20230403171314136.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kAglYtgo-1680869584908)(E:\笔记\typora-user-images\image-20230403171503675.png)]

1.4、NoSQL的四大分类

KV键值对

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

文档型数据库(bson格式和json一样):

  • MongoDB(一般必须要掌握)
    • MongoDB是一个基于分布式文件存储的数据库,c++编写,主要用来处理大量的文档!
    • MongoDB是一个介于关系型数据库和非关系型数据库中间的产品!MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的!
  • ConthDB(暂时不了解)

列存储数据库

  • HBase
  • 分布式文件系统

图关系数据库

  • 他不是存图形的,放的是关系,如:朋友圈社交网络,广告推荐等等
  • Neo4j,InfoGrid

四者的对比

分类Examples举例典型应用场景数据模型优点缺点
键值对(key-value)Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。Key 指向 Value 的键值对,通常用hash table来实现查找速度快数据无结构化,通常只被当作字符串或者二进制数据
列存储数据库Cassandra, HBase, Riak分布式的文件系统以列簇式存储,将同一列数据存在一起查找速度快,可扩展性强,更容易进行分布式扩展功能相对局限
文档型数据库CouchDB, MongoDbWeb应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容)Key-Value对应的键值对,Value为结构化数据数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构查询性能不高,而且缺乏统一的查询语法。
图形(Graph)数据库Neo4J, InfoGrid, Infinite Graph社交网络,推荐系统等。专注于构建关系图谱图结构利用图结构相关算法。比如最短路径寻址,N度关系查找等很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群

2、Redis入门

2.1、概述

Redis是什么

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

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

Redis能干什么

  1. 内存存储,持久化,内存中是断电即失,所以说持久化很重要(rdb、aof)
  2. 效率高,可以用于高速缓存
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器,计数器(浏览量)

特性

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

学习中需要用到的东西

  1. 官网:Redis
  2. 中文官网:Redis中文网
  3. 也可以去github上找去Redis(还是建议去官网)

Redis推荐都是在Linux服务器上搭建的

2.2、windos安装

**下载地址:**https://github.com/tporadowski/redis/releases。

  1. 下载到的Redis支持32bit和64bit。根据自己实际情况选择,将64bit的内容cp到自定义盘符安装目录取名redis。 如 C:\reids
  2. 打开一个cmd窗口 使用cd命令切换目录到 C:\redis 运行 redis-server.exe redis.conf
  3. 如果想方便的话,可以把redis的路径加到系统的环境变量里,这样就省得再输路径了,后面的那个redis.conf可以省略,如果省略,会启用默认的。输入之后,会显示如下界面:

Redis 安装

  1. 这时候另启一个cmd窗口,原来的不要关闭,不然就无法访问服务端了。

  2. 切换到redis目录下运行 redis-cli.exe -h 127.0.0.1 -p 6379

  3. 设置键值对 set myKey abc

  4. 取出键值对 get myKey

Redis 安装

2.3、Linux安装

  1. 下载redis压缩包

    wget http://download.redis.io/releases/redis-5.0.8.tar.gz
    
  2. 把redis安装包放到/opt目录下并且解压

    [root@root home]# ls
    hwt2  install.sh  redis  redis-5.0.8.tar.gz  www
    [root@root home]# mv redis-5.0.8.tar.gz /opt
    [root@root home]# cd /opt
    [root@root opt]# ls
    containerd  redis-5.0.8.tar.gz  rh  threatbook
    [root@root opt]# tar -zxvf redis-5.0.8.tar.gz 
    
  3. 进入reais目录

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t4wwH0OF-1680869584911)(E:\笔记\typora-user-images\image-20230403181247063.png)]

  4. 基本环境安装

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

    出现当前图片说明安装完成

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dk8hi0He-1680869584911)(E:\笔记\typora-user-images\image-20230403181648602.png)]

  5. redis默认安装路径 /usr/local/bin

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MzRhwpMp-1680869584912)(E:\笔记\typora-user-images\image-20230403182037768.png)]

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

    #创建文件夹
    mkdir hconfig
    #复制到/usr/local/bin/hconfig目录下
    cp /opt/redis-5.0.8/redis.conf hconfig
    
    cd hconfig/
    
    ls
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-93qLIX73-1680869584912)(E:\笔记\typora-user-images\image-20230403182625412.png)]

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

    vim redis.conf
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dU9o5LSb-1680869584913)(E:\笔记\typora-user-images\image-20230403183253525.png)]

  3. 启动Redis服务

    # 切换到 /usr/local/bin
    # 使用配置文件启动
    redis-server hconfig/redis.conf 
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uYSuJ8bJ-1680869584913)(E:\笔记\typora-user-images\image-20230403184340440.png)]

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

    #使用Redis客户端进行连接
    redis-cli -p 6379   #Redis的默认端口6379
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GhP7TkoR-1680869584914)(E:\笔记\typora-user-images\image-20230403184944046.png)]

  5. 查看redis进程是否开启

    ps -ef|grep redis
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXWsHVX9-1680869584915)(E:\笔记\typora-user-images\image-20230403185700098.png)]

  6. 如何关闭Redis服务

    shutdown
    exit
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tMrueocO-1680869584915)(E:\笔记\typora-user-images\image-20230403190105385.png)]

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qQkcoJe-1680869584916)(E:\笔记\typora-user-images\image-20230403190254573.png)]

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

2.3、测试性能

redis-benchmark是一个压力测试工具(位置就在你解压的redis那个文件里面就是/usr/local/bin里面–>而且你的redis-cli也在这个里面的)

官方自带的性能测试工具!

redis-benchmark的命令参数

序号选项描述默认值
1-h指定服务器主机名127.0.0.1
2-p指定服务器端口6379
3-s指定服务器 socket
4-c指定并发连接数50
5-n指定请求数10000
6-d以字节的形式指定 SET/GET 值的数据大小2
7-k1=keep alive 0=reconnect1
8-rSET/GET/INCR 使用随机 key, SADD 使用随机值
9-P通过管道传输 请求1
10-q强制退出 redis。仅显示 query/sec 值
11–csv以 CSV 格式输出
12*-l*(L 的小写字母)生成循环,
13-t仅运行以逗号分隔的测试命令列表。
14*-I*(i 的大写字母)Idle 模式。仅打开 N 个

你可以简单测试下:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bURRK9dS-1680869584917)(E:\笔记\typora-user-images\image-20230403191538297.png)]

2.4、基础的知识

redis默认有16个数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oED9lLC5-1680869584917)(E:\笔记\typora-user-images\image-20230403191813150.png)]

因为配置文件的databases为16

默认使用的第0个数据库

127.0.0.1:6379> SELECT 3(切换到第几个数据库)

127.0.0.1:6379> DBSIZE(查看数据库DB大小)

127.0.0.1:6379> keys *(查看数据库所有的key)

127.0.0.1:6379> set name kong(存一个键值对)

127.0.0.1:6379> get get name(取name的值)

127.0.0.1:6379> flushdb(清除当前数据库的数据)

127.0.0.1:6379> flushall(清除所有数据库的数据) 

Redis 是单线程的

明白Redis是很快的,官方表示,Redis是基于内存操作的,cpu不是Redis性能瓶颈,Redis的瓶颈是更具机器的内存和网络带宽,既然可以使用单线程来实现,就是用单线程了。

Redis是C语言写的,官方提供的数据是100000+的QFS,完全不比同样使用key-value的Memecache差

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

  1. 误区1:高性能的服务器一定是多线程的

  2. 误区2:多线程(CPU上下文会切换!)一定比单线程效率高。这里可以先去看看狂神的JUC就能理解CPU>内存>硬盘的速度要有所了解。

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

3、五大数据类型

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

3.1、Redis-key

127.0.0.1:6379> keys *     #查看所有的key
(empty list or set)
127.0.0.1:6379> set name hwt   #set    key
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> set age 1
OK
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> exists name    #判断当前key是否存在
(integer) 1
127.0.0.1:6379> exists name1
(integer) 0
127.0.0.1:6379> move name 1    #移除当前的key
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name hwt
OK
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> get name
"hwt"
127.0.0.1:6379> espire name 10
(error) ERR unknown command `espire`, with args beginning with: `name`, `10`, 
127.0.0.1:6379> EXPIRE name 10    #设置当前key的过期时间,单位是秒 命令不能使用小写
(integer) 1
127.0.0.1:6379> ttl name    #查看当前key的剩余时间
(integer) 5
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> ttl name
(integer) 1
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> 
127.0.0.1:6379> type age    #查看当前类的类型
string
127.0.0.1:6379> 

后面如果遇到什么不会的命令可以到官网去查看帮助文档

中文帮助文档:redis命令手册

3.2、String(字符串)

90%的Java程序员使用redis只会使用String类型

##########################################################
127.0.0.1:6379> set key1 v1   #设置值
OK
127.0.0.1:6379> get key1      #获得值
"v1"
127.0.0.1:6379> keys *        #获得所有的key
1) "key1"
127.0.0.1:6379> EXISTS key1   #判断一个key是否存在
(integer) 1
127.0.0.1:6379> APPEND key1 "hello"
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> STRLEN key1
(integer) 7
127.0.0.1:6379> APPEND key1 ",Redis"
(integer) 13
127.0.0.1:6379> STRLEN key1
(integer) 13
127.0.0.1:6379> get key1
"v1hello,Redis"
##########################################################
#i++
#步长 i+=
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       #自增1 浏览量为1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views       #自减1,浏览量为1
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> INCRBY views 10    #
(integer) 10
127.0.0.1:6379> get views
"10"
127.0.0.1:6379> INCRBY views 5
(integer) 15
127.0.0.1:6379> get views
"15"
##########################################################
#字符串范围 range
127.0.0.1:6379> set key1 "hello,redis"  #设置key的值
OK
127.0.0.1:6379> get key1
"hello,redis"
127.0.0.1:6379> GETRANGE key1 0 3        #截取字符串[0,3]
"hell"
127.0.0.1:6379> GETRANGE key1 0 -1       #获取全部的字符串  和get key是一样的
"hello,redis"
127.0.0.1:6379> 

#替换
127.0.0.1:6379> get key2
"zbcdefg"
127.0.0.1:6379> SETRANGE key2 1 xx    #替换指定位置开始的字符串
(integer) 7
127.0.0.1:6379> get key2
"zxxdefg"

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

127.0.0.1:6379> setex key3 30 "hello"  #设置key3的值为hello  30秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 23
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis"     #如果mykey不存在 ,创建mykey
(integer) 1
127.0.0.1:6379> key *
(error) ERR unknown command `key`, with args beginning with: `*`, 
127.0.0.1:6379> keys *
1) "mykey"
2) "key1"
3) "key2"
127.0.0.1:6379> setnx mykey "MongoDB"      #如果mykey存在常见失败
(integer) 0
127.0.0.1:6379> keys *
1) "mykey"
2) "key1"
3) "key2"
127.0.0.1:6379> get mykey
"redis"

##########################################################
#mset
#mget

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3   # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3            # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4       # msetnx 是一个原子性的操作,要么一起成功,要么一起失败
(integer) 0
127.0.0.1:6379> get key4
(nil)
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> 

##########################################################
#对象
set user:1{name:zhangsan,age:3}  #设置一个user:1对象 值为json字符串来保存一个对象
# 这里的key是一个巧妙的设计: user:{id}:{filed}
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"

##########################################################
getset  #先get然后set

127.0.0.1:6379> getset db redis   #如果存在值,则返回原来的值,并没有新的值
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb
"redis"
127.0.0.1:6379> get db
"mongodb"
127.0.0.1:6379> 

数据结构是相同的

String类似的使用场景:value除了是字符串还可以是数字,用途举例:

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存存储

3.3、List(列表)

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SnP0fvA8-1680869584918)(E:\笔记\typora-user-images\image-20230403232435113.png)]

Redis里面,可以把list完成,栈、队列、阻塞队列

##########################################################
127.0.0.1:6379> LPUSH list one  #将一个值或者多个值插入到列表的头部(左)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1  #获取list中的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1
1) "three"
2) "two"
127.0.0.1:6379> Rpush list right  #将一个值或者多个值插入到列表的尾部(右)
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
##########################################################
#LPOP
#RPOP
127.0.0.1:6379> clear
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> Lpop list   #移除list列表的第一个元素
"three"
127.0.0.1:6379> Rpop list    #移除list列表的最后一个元素
"right"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"

##########################################################
# lindex
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1   #通过下标获得list中的值
"one"
127.0.0.1:6379> lindex list 0
"two"

##########################################################
#Llen
127.0.0.1:6379> lpush list one
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> Llen list    #返回列表的长度
(integer) 3
127.0.0.1:6379> 

##########################################################
移除指定的值!
取关 uid
Lrem
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one # 移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 1 three
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> Lpush list three
(integer) 3
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "two"

##########################################################################
trim 修剪。; list 截断!
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> Rpush mylist "hello"
(integer) 1
127.0.0.1:6379> Rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> Rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> Rpush mylist "hello3"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2 # 通过下标截取指定的长度,这个list已经被改变了,截断了
只剩下截取的元素!
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello1"
2) "hello2"

##########################################################################
rpoplpush # 移除列表的最后一个元素,将他移动到新的列表中!
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> rpoplpush mylist myotherlist # 移除列表的最后一个元素,将他移动到新的
列表中!
"hello2"
127.0.0.1:6379> lrange mylist 0 -1 # 查看原来的列表
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1 # 查看目标列表中,确实存在改值!
1) "hello2"

##########################################################################
lset 将列表中指定下标的值替换为另外一个值,更新操作
127.0.0.1:6379> EXISTS list # 判断这个列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果不存在列表我们去更新就会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> LRANGE list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item # 如果存在,更新当前下标的值
OK
127.0.0.1:6379> LRANGE list 0 0
1) "item"
127.0.0.1:6379> lset list 1 other # 如果不存在,则会报错!
(error) ERR index out of range

##########################################################################
linsert # 将某个具体的value插入到列把你中某个元素的前面或者后面!
127.0.0.1:6379> Rpush mylist "hello"
(integer) 1
127.0.0.1:6379> Rpush mylist "world"
(integer) 2
127.0.0.1:6379> LINSERT mylist before "world" "other"
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> LINSERT mylist after world new
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"

小结

  • 它实际上是一个链表
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除所有的值,空链表,也代表不存在
  • 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一点~

消息队列!消息队列(Lpush Rpop) 栈(Lpush Lpop)

3.4、Set(集合)

set中的值是不能重复的!

##########################################################################
127.0.0.1:6379> sadd myset "hello" # set集合中添加匀速
(integer) 1
127.0.0.1:6379> sadd myset "kuangshen"
(integer) 1
127.0.0.1:6379> sadd myset "lovekuangshen"
(integer) 1
127.0.0.1:6379> SMEMBERS myset # 查看指定set的所有值
1) "hello"
2) "lovekuangshen"
3) "kuangshen"
127.0.0.1:6379> SISMEMBER myset hello # 判断某一个值是不是在set集合中!
(integer) 1
127.0.0.1:6379> SISMEMBER myset world
(integer) 0

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

127.0.0.1:6379> scard myset # 获取set集合中的内容元素个数!
(integer) 4
##########################################################################
rem
127.0.0.1:6379> srem myset hello # 移除set集合中的指定元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "lovekuangshen2"
2) "lovekuangshen"
3) "kuangshen"

##########################################################################
set 无序不重复集合。抽随机!
127.0.0.1:6379> SMEMBERS myset
1) "lovekuangshen2"
2) "lovekuangshen"
3) "kuangshen"
127.0.0.1:6379> SRANDMEMBER myset # 随机抽选出一个元素
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset 2 # 随机抽选出指定个数的元素
1) "lovekuangshen"
2) "lovekuangshen2"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "lovekuangshen"
2) "lovekuangshen2"
127.0.0.1:6379> SRANDMEMBER myset # 随机抽选出一个元素
"lovekuangshen2"

##########################################################################
删除定的key,随机删除key!
127.0.0.1:6379> SMEMBERS myset
1) "lovekuangshen2"
2) "lovekuangshen"
3) "kuangshen"
127.0.0.1:6379> spop myset # 随机删除一些set集合中的元素!
"lovekuangshen2"
127.0.0.1:6379> spop myset
"lovekuangshen"
127.0.0.1:6379> SMEMBERS myset
1) "kuangshen"

##########################################################################
将一个指定的值,移动到另外一个set集合!
127.0.0.1:6379> sadd myset "hello"
(integer) 1
127.0.0.1:6379> sadd myset "world"
(integer) 1
127.0.0.1:6379> sadd myset "kuangshen"
(integer) 1
127.0.0.1:6379> sadd myset2 "set2"
(integer) 1
127.0.0.1:6379> smove myset myset2 "kuangshen" # 将一个指定的值,移动到另外一个set集
合!
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "world"
2) "hello"
127.0.0.1:6379> SMEMBERS myset2
1) "kuangshen"
2) "set2"

##########################################################################
微博,B站,共同关注!(并集)
数字集合类:
- 差集 SDIFF
- 交集
- 并集
127.0.0.1:6379[8]> sadd key2 a
(integer) 1
127.0.0.1:6379[8]> sadd key2 b
(integer) 1
127.0.0.1:6379[8]> sadd key2 c
(integer) 1
127.0.0.1:6379[8]> sadd key3 c
(integer) 1
127.0.0.1:6379[8]> sadd key3 d
(integer) 1
127.0.0.1:6379[8]> sadd key3 e
(integer) 1
127.0.0.1:6379> SDIFF key2 key3 # 差集
1) "b"
2) "a"
127.0.0.1:6379> SINTER key2 key3 # 交集 共同好友就可以这样实现
1) "c"
127.0.0.1:6379> SUNION key2 key3 # 并集
1) "b"
2) "c"
3) "e"
4) "a"
5) "d"

set是无序不重复集合。可以实现抽随机

微博,可以将A用户所关注的所有人放在一个set集合中!将它的粉丝也放在一个set集合中

3.5、Hash(哈希)

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

##########################################################################
127.0.0.1:6379> hset myhash field1 kuangshen # set一个具体 key-vlaue
(integer) 1
127.0.0.1:6379> hget myhash field1 # 获取一个字段值
"kuangshen"
127.0.0.1:6379> hmset myhash field1 hello field2 world # set多个 key-vlaue
OK
127.0.0.1:6379> hmget myhash field1 field2 # 获取多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall myhash # 获取全部的数据,
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> hdel myhash field1 # 删除hash指定key字段!对应的value值也就消失了!
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"

##########################################################################
hlen
127.0.0.1:6379> hmset myhash field1 hello field2 world
OK
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> hlen myhash # 获取hash表的字段数量!
(integer) 2

##########################################################################
127.0.0.1:6379> HEXISTS myhash field1 # 判断hash中指定字段是否存在!
(integer) 1
127.0.0.1:6379> HEXISTS myhash field3
(integer) 0

##########################################################################
# 只获得所有field
# 只获得所有value
127.0.0.1:6379> hkeys myhash # 只获得所有field
1) "field2"
2) "field1"
127.0.0.1:6379> hvals myhash # 只获得所有value
1) "world"
2) "hello"

##########################################################################
incr 
127.0.0.1:6379> hset myhash field3 5 #指定增量!
(integer) 1
127.0.0.1:6379> HINCRBY myhash field3 1
(integer) 6
127.0.0.1:6379> HINCRBY myhash field3 -1
(integer) 5
127.0.0.1:6379> hsetnx myhash field4 hello # 如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 world # 如果存在则不能设置
(integer) 0

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

3.6、Zset(有序集合)

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

在set的基础上,增加了一个值,set k1 v1 zset k1 score1 v1
127.0.0.1:6379> hvals myhash # 只获得所有value
1) "world"
2) "hello"

##########################################################################
incr decr
127.0.0.1:6379> hset myhash field3 5 #指定增量!
(integer) 1
127.0.0.1:6379> HINCRBY myhash field3 1
(integer) 6
127.0.0.1:6379> HINCRBY myhash field3 -1
(integer) 5
127.0.0.1:6379> hsetnx myhash field4 hello # 如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 world # 如果存在则不能设置
(integer) 0
127.0.0.1:6379> zadd myset 1 one # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 添加多个值
(integer) 2
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"

##########################################################################
排序如何实现
127.0.0.1:6379> zadd salary 2500 xiaohong # 添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 kaungshen
(integer) 1
# ZRANGEBYSCORE key min max
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示全部的用户 从小到大!
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZREVRANGE salary 0 -1 # 从大到进行排序!
1) "zhangsan"
2) "kaungshen"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 显示全部的用户并且附带成1) "kaungshen"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores # 显示工资小于2500员工的升
序排序!
1) "kaungshen"
2) "500"
3) "xiaohong"
4) "2500"

##########################################################################
# 移除rem中的元素
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong # 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "zhangsan"
127.0.0.1:6379> zcard salary # 获取有序集合中的个数
(integer) 2

##########################################################################
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 kuangshen
(integer) 2
127.0.0.1:6379> zcount myset 1 3 # 获取指定区间的成员数量!
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2

其余的一些API,去通过官方的帮助文档即可!

zset可以搞班级表成绩单、工资表排序!

普通消息、重要消息等等

4、三种特殊数据类型

4.1、geospatial 地理位置

朋友的定位,妇女进的人,打车距离计算?

Redis的Geo在Redis3.2版本就推出了!这个功能可以推算地理位置信息,两地之间的距离,方圆几里的人!

只有六个命令

GEOADD
GEODIST
GEOHASH
GEOPOS
GEORADIUS
GEORADIUSBYMEMBER

geoadd(添加地理位置)

geoadd key 经度 维度 城市(这个参数算是备注)

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

例子: geoadd china:city 经度 维度 beijing

# getadd 添加地理位置
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
# 有效的经度从-180度到180度。
# 有效的纬度从-85.05112878度到85.05112878度。
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
# 参数 key 值()
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shengzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2

geopos(查询所存的地理位置)

geopos key 城市名

例子:geopos china:city beijing

127.0.0.1:6379> GEOPOS china:city beijing # 获取指定的城市的经度和纬度!
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> GEOPOS china:city beijing chongqing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"

geodist(返回两个给定位置之间的距离)

  • m 表示单位为米。

  • km 表示单位为千米。

  • mi 表示单位为英里。

  • ft 表示单位为英尺。

geodist key 城市名 单位

例子:geodist china:city shanghai beijing km

127.0.0.1:6379> GEODIST china:city beijing shanghai km # 查看上海到北京的直线距离
"1067.3788"
127.0.0.1:6379> GEODIST china:city beijing chongqing km # 查看重庆到北京的直线距离
"1464.0708"

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

我附近的人?(获得所有附近的人的地址,定位)通过半径来查询!

georadius key 某个用户的经度 某个用户的纬度 距离 单位 withdist(带上距离) withcoord(带上经纬度) count 1(限制只能查出来一个人)

例子:georadius china:city 110 30 1000 km

127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 以110,30 这个经纬度为中心,寻
找方圆1000km内的城市
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1) "chongqi"
2) "xian"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist # 显示到中间距离的位置
1) 1) "chongqing"
2) "341.9374"
2) 1) "xian"
2) "483.8340"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withcoord # 显示他人的定位信息
1) 1) "chongqi"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 1 #筛选出指定的结果!
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist withcoord count 2
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"

georadiusbymember(找出位于指定元素周围的其他元素)

georadiusbymember key 以添加过的人或地方为中心的名字 距离 单位

例子:georadiusbymember china:city beijing 1000 km(后面和上面一样依旧可以带参数)

# 找出位于指定元素周围的其他元素!
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km
1) "beijing"
2) "xian"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"

geohash(返回一个位置或多个位置元素的geohash)

目前用不到但有人表示很重要以后再了解即可!

geohash key 元素名字

例子:geohash china:city beijing

# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!
127.0.0.1:6379> geohash china:city beijing chongqing
1) "wx4fbxxfke0"
2) "xt4purb89n0"

GEO底层的实现原理其实就是Zset!我们可以用Zset命令来操作geo

zrange china:city 0 -1
zrem china:city beijing
127.0.0.1:6379> ZRANGE china:city 0 -1 # 查看地图中全部的元素
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 移除指定元素!
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"

4.2、hyperloglog

什么是基数?

A{1,3,5,7,8,7}

B{1,3,5,7,8}

基数就是一个集合中不重复元素的个数比如上面两个的基数就为5

简介

Redis2.8.9版本就出来了

Redis Hyperloglog 基数统计的算法!

优点:占用的内存是固定的,2的64次方不同的元素基数,只需要12kb内存!如果从内存角度来比的化hyperloglog首选!

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

传统的方式,set保存用户id,然后就可以统计set中的元素数量作为标准判断!但个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id

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

命令描述
PFADD key element1 [elememt2..]添加指定元素到 HyperLogLog 中
PFCOUNT key [key]返回给定 HyperLogLog 的基数估算值。
PFMERGE destkey sourcekey [sourcekey..]将多个 HyperLogLog 合并为一个 HyperLogLog
----------PFADD--PFCOUNT---------------------
127.0.0.1:6379> PFADD myelemx a b c d e f g h i j k # 添加元素
(integer) 1
127.0.0.1:6379> type myelemx # hyperloglog底层使用String
string
127.0.0.1:6379> PFCOUNT myelemx # 估算myelemx的基数
(integer) 11
127.0.0.1:6379> PFADD myelemy i j k z m c b v p q s
(integer) 1
127.0.0.1:6379> PFCOUNT myelemy
(integer) 11

----------------PFMERGE-----------------------
127.0.0.1:6379> PFMERGE myelemz myelemx myelemy # 合并myelemx和myelemy 成为myelemz
OK
127.0.0.1:6379> PFCOUNT myelemz # 估算基数
(integer) 17

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

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

4.3、bitmap

位存储

使用位存储,信息状态只有 0 和 1

Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。

命令描述
setbit key offset value为指定key的offset位设置值
getbit key offset获取offset位的值
bitcount key [start end]统计字符串被设置为1的bit数,也可以指定统计范围按字节
bitop operration destkey key[key..]对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
BITPOS key bit [start] [end]返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位

可以用来统计用户信息,活跃,不活跃!登录、未登入!打卡,356打卡!

只有两个状态的,都可以用BIitmaps!都是操作二进制位进行记录,就只有0和1两个状态!

使用bitmap模拟来记录周一到周日的打卡!

下面则0 1 2 3 4 5 6代表周一到周日

而0代表没打卡,1代表打卡

setbit key 数字 0/1

------------setbit---------------
127.0.0.1:6379> setbit sign 0 1 # 设置sign的第0位为 1 
(integer) 0
127.0.0.1:6379> setbit sign 2 1 # 设置sign的第2位为 1  不设置默认 是0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> type sign
string

---------------getbit--------------
127.0.0.1:6379> getbit sign 2 # 获取第2位的数值查    看某一天是否有打卡!
(integer) 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 4 # 未设置默认是0
(integer) 0

-----------bitcount------------------
127.0.0.1:6379> BITCOUNT sign # 统计sign中为1的位数   统计这周的打卡记录,就可以看到是否有全勤!
(integer) 3
(integer) 4

5、事务

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

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

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

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

所有命令在食物中,并没有直接呗执行!只有发起执行命令的时候才会执行!exec

Redis单条命令是保证原子性的

但Redis事务是不保证原子性的!!!!!

redis的事务:

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

multi (开始事务)

exec (执行事务)(会按照顺序执行)

discard (取消事务)

正常执行事务!

127.0.0.1:6379> multi # 开启事务
OK
# 命令入队
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "v2"
4) OK

放弃事务!(DISCARD )

127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> DISCARD # 取消事务
OK
127.0.0.1:6379> get k4 # 事务队列中命令都不会被执行!
(nil)

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

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec # 执行事务报错!
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 # 所有的命令都不会被执行!
(nil)

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

127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 会执行的时候失败!
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range # 虽然第一条命令报错了,但是
依旧正常执行成功了!
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"

悲观锁

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

乐观锁

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

watch key

watch key可以理解为乐观锁操作

如果执行事务时候监视的值被其他地方修改了就会失败

且不管事务执行成不成功监视都会自动解除(只监视一次)

unwatch key(解除监视)

unwatch key

Redis测监视测试 Watch

正常执行成功

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

测试多线程修改值 , 使用watch 可以当做redis的乐观锁操作!

127.0.0.1:6379> watch money # 监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失
败!
(nil)

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

redis事务的三大特性

单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

6、Jedis

测试

1、导入对应的依赖

    <dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
    </dependencies>

2、编码测试

  • 连接数据库
public class TestPing {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        System.out.println(jedis.ping());
    }
}

输出PONG即为成功

  • 操作命令
  • 断开连接

常用API

String

List

Set

Hash

Zset

所有的api命令,就是我们对应的上面的学习指令,一个也没有改变!

String测试

public class TestString {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("===========增加数据===========");
        System.out.println(jedis.set("key1","value1"));
        System.out.println(jedis.set("key2","value2"));
        System.out.println(jedis.set("key3", "value3"));
        System.out.println("删除键key2:"+jedis.del("key2"));
        System.out.println("获取键key2:"+jedis.get("key2"));
        System.out.println("修改key1:"+jedis.set("key1", "value1Changed"));
        System.out.println("获取key1的值:"+jedis.get("key1"));
        System.out.println("在key3后面加入值:"+jedis.append("key3", "End"));
        System.out.println("key3的值:"+jedis.get("key3"));
        System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03","key04"));
        System.out.println("删除多个键值对:"+jedis.del("key01","key02"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));
 
        jedis.flushDB();
        System.out.println("===========新增键值对防止覆盖原先值==============");
        System.out.println(jedis.setnx("key1", "value1"));
        System.out.println(jedis.setnx("key2", "value2"));
        System.out.println(jedis.setnx("key2", "value2-new"));
        System.out.println(jedis.get("key1"));
        System.out.println(jedis.get("key2"));
 
        System.out.println("===========新增键值对并设置有效时间=============");
        System.out.println(jedis.setex("key3", 2, "value3"));
        System.out.println(jedis.get("key3"));
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(jedis.get("key3"));
 
        System.out.println("===========获取原值,更新为新值==========");
        System.out.println(jedis.getSet("key2", "key2GetSet"));
        System.out.println(jedis.get("key2"));
 
        System.out.println("获得key2的值的字串:"+jedis.getrange("key2", 2, 4));
    }
}

List测试

public class TestList {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("===========添加一个list===========");
        jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
        jedis.lpush("collections", "HashSet");
        jedis.lpush("collections", "TreeSet");
        jedis.lpush("collections", "TreeMap");
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部
        System.out.println("collections区间0-3的元素:"+jedis.lrange("collections",0,3));
        System.out.println("===============================");
        // 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
        System.out.println("删除指定元素个数:"+jedis.lrem("collections", 2, "HashMap"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("删除下表0-3区间之外的元素:"+jedis.ltrim("collections", 0, 3));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections列表出栈(左端):"+jedis.lpop("collections"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections添加元素,从列表右端,与lpush相对应:"+jedis.rpush("collections", "EnumMap"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections列表出栈(右端):"+jedis.rpop("collections"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("修改collections指定下标1的内容:"+jedis.lset("collections", 1, "LinkedArrayList"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("===============================");
        System.out.println("collections的长度:"+jedis.llen("collections"));
        System.out.println("获取collections下标为2的元素:"+jedis.lindex("collections", 2));
        System.out.println("===============================");
        jedis.lpush("sortedList", "3","6","2","0","7","4");
        System.out.println("sortedList排序前:"+jedis.lrange("sortedList", 0, -1));
        System.out.println(jedis.sort("sortedList"));
        System.out.println("sortedList排序后:"+jedis.lrange("sortedList", 0, -1));
    }
}

Hash测试

public class TestHash {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        Map<String,String> map = new HashMap<String,String>();
        map.put("key1","value1");
        map.put("key2","value2");
        map.put("key3","value3");
        map.put("key4","value4");
        //添加名称为hash(key)的hash元素
        jedis.hmset("hash",map);
        //向名称为hash的hash中添加key为key5,value为value5元素
        jedis.hset("hash", "key5", "value5");
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));//return Map<String,String>
        System.out.println("散列hash的所有键为:"+jedis.hkeys("hash"));//return Set<String>
        System.out.println("散列hash的所有值为:"+jedis.hvals("hash"));//return List<String>
        System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 6));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 3));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("删除一个或者多个键值对:"+jedis.hdel("hash", "key2"));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("散列hash中键值对的个数:"+jedis.hlen("hash"));
        System.out.println("判断hash中是否存在key2:"+jedis.hexists("hash","key2"));
        System.out.println("判断hash中是否存在key3:"+jedis.hexists("hash","key3"));
        System.out.println("获取hash中的值:"+jedis.hmget("hash","key3"));
        System.out.println("获取hash中的值:"+jedis.hmget("hash","key3","key4"));
    }
}

事务

public class Test01 {
    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.set("user2", result);
            // 执行事务
            multi.exec();
        }catch (Exception e){
            // 放弃事务
            multi.discard();
        } finally {
            // 关闭连接
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();
        }
    }
}

7、SpringBoot整合

springBoot操作数据库:spring-data jpa jdbc mongodb redis!

SpringData也是和SpringBoot齐名的项目!

说明:在springboot2.x之后,原来使用的jedis被替换成了lettuce

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

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

源码分析:

@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 我们可以自己定义一个redisTemplate来替换这个默认的!
    public RedisTemplate<Object, Object>redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {
        // 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
        // 两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, 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;
    }

整合测试

1、导入依赖

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

2、配置连接

spring.redis.host=127.0.0.1
spring.redis.port=6379

3、测试

@SpringBootTest
class Redis02SpringbootApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Test
    void contextLoads() {

        // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
        // opsForValue 操作字符串 类似String
        // opsForList 操作List 类似List
        // opsForSet
        // opsForHash
        // opsForZSet
        // opsForGeo
        // opsForHyperLogLog
        // 除了进本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的 CRUD
        // 获取redis的连接对象
        // RedisConnection connection =
        //redisTemplate.getConnectionFactory().getConnection();
        // connection.flushDb();
        // connection.flushAll();
        redisTemplate.opsForValue().set("高启强","想吃🐟了");
        System.out.println(redisTemplate.opsForValue().get("高启强"));
    }
}

关于对象的保存

如果要存对象的话需要将对象序列化

  1. //首先你要去创建个对象用于测试
    User user = new User();
    //用obicetMapper来序列化对象
    String json = new  ObjectMapper().writeValueAsString(user);
          
    redisTemplate.opsForValue().set("key",json);
    
    System.out.println(redisTemplate.opsForValue().get("key"));
    
  2. //也可以实现Serializable接口来使对象序列化
    public class User implements Serializable {
        private String name;
        private Integer age;
    }
    

企业开发中:redisTemplate会被封装到一个工具类使用起来比较方便

RedisTemplete模板

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

自定义Redis工具类

使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。

package com.softeem.springbootredis02.utils;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
 
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
 
//在我们真实的开发中, 或者你们在公司, 一般都可以看到一个公司自己封装redis的工具
@Component
public final class RedisUtil {
 
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
 
    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    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));
            }
        }
    }
 
 
    // ============================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 delta 要增加几(大于0)
     */
    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)
     */
    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
     */
    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 对应多个键值
     */
    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)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
 
 
    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
 
 
    // ============================set=============================
 
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    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 键
     */
    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代表所有值
     */
    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 键
     */
    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倒数第二个元素,依次类推
     */
    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 值
     */
    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  时间(秒)
     */
    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;
        }
 
    }
 
}

8、Redis.conf详解

启动的时候,就通过配置文件启动的。

8.1、redis的配置文件 unit单位 对大小写不敏感!
# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
8.2、可以包含多个配置文件
################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# Included paths may contain wildcards. All files matching the wildcards will
# be included in alphabetical order.
# Note that if an include path contains a wildcards but no files match it when
# the server is started, the include statement will be ignored and no error will
# be emitted.  It is safe, therefore, to include wildcard files from empty
# directories.
#
# include /path/to/local.conf
# include /path/to/other.conf
# include /path/to/fragments/*.conf
#
8.3、网络
bind 127.0.0.1 (绑定的ip)
protected-mode yes(是否受保护)
如果相连接服务器可以把bind改为0.0.0.0  保护模式改为no就能连接了
port 6379(端口号)
8.4通用
daemonize yes(以守护进程的方式运行默任为no我们需要手动改为yes)
prdfile /var/run/redis_6379.pid(如果以后台方式运行,我们就要指定一个pid文件)

日志设置
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)生产环境
# warning (only very important / critical messages are logged)
loglevel notice---->默认就行

logfile ""(日志的文件位置名如果为空就是输出)
databases 16(数据库的数量,默认是16个数据库)
always-show-logo no(是否显示logo)
8.5、快照

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

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

# Save the DB to disk.
#
# save <seconds> <changes> [<seconds> <changes> ...]
#
# Redis will save the DB if the given number of seconds elapsed and it
# surpassed the given number of write operations against the DB.
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# save ""

#可以在这里加sava xx xx
sava 900 1 #(如果900s内如果至少有1个key进行修改,我们就进行持久化操作)

stop-writes-on-bgsave-error yes   #(持久化如果出错是否继续工作)
rdbcompression yes                #(是否压缩rdb文件,需要消耗一些cpu资源)
rdbchecksum yes                   #(保存rdb文件的时候,进行错误的检查校验)
dir ./                            #rdb文件保存的目录
8.6、REPLICATION 复制
8.7、SECURITY安全
requirepass foobared              #(设置密码默认这样)
                                  #插入requirepass 密码 来设置你自己的密码
                                  #也可以命令设置但重启就失效
                                  
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass # 获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置redis的密码
OK
127.0.0.1:6379> config get requirepass # 发现所有的命令都没有权限了
(error) NOAUTH Authentication required.
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # 使用密码进行登录!
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456" 
8.8、CLIENTS客户端限制
maxclients 10000                      #(设置最大客户端的数量)
maxmemory <bytes>                     #(redis配置最大的内存容量默认为bytes)
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 : 永不过期,返回错误
8.9、APPEND ONLY MODE 模式 aof配置
appendonly no                          #(默认不开始aof的因为默认使用rdb方式的持久化)
appendfilename "appendonly.aof"        #(持久化文件的名字)

# appendfsync always(每次修改都会sync 消耗性能)
appendfsync everysec(每秒执行一次sync,可能会丢失1s数据)
# appendfsync no(不同步sync)sync:同步

具体配置在持久化中详细了解

9、Redis持久化

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

RDB(Redis DataBase)

什么是RDB

在主从复制中,rdb就是备用的!从机上面!

image.png

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

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

rdb保存的文件是dump.reb

触发规则

  1. 如果满足save规则,就会触发rdb规则,例如save 60 5(意思为在60s内修改了5次key那么就会触发rdb操作)
  2. 执行flushall命令的时候也会触发rdb规则!
  3. 退出Redis,也会产生rdb文件!

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

如何恢复rdb

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

  2. 查看我们需要存放的位置直接用config get dir

  3. 如果config get dir文件下存在dump.rdb他就会自动恢复其中的数据

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

因此redis它默认的配置就够用,但还是要学习的

优点:

  1. 适合大规模的数据恢复
  2. 如果对数据完整性不高

缺点:

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

dump.rdb重要的数据一般要备份

AOF(Append Only File)

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

是什么

image.png

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

Aof保存的是 appendonly.aof文件

appendonly no(默认是不开启的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LcxemFn-1680869584922)(E:\笔记\typora-user-images\image-20230407152913941.png)]

只需要no改为yes就可以开启aof

重启Redis就可以生效在bin目录下生成aof文件

redis给我们提供了一个工具redis-check-aof用以修复aof文件

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

如果文件正常就直接重启就能连接了~!

aof重写规则

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDRUsrmN-1680869584923)(E:\笔记\typora-user-images\image-20230407155435663.png)]

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

优点和缺点

优点:

  1. 每一次修改都同步,文件的完整性会更好
  2. 默认是每秒同步一次,可能会丢失一秒数据
  3. 从不同步,效率是最高的

缺点:

  1. 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
  2. Aof运行效率也要比rdb慢,所以我们redis默认配置为rdb持久化
扩展
  1. RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
  2. AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始 的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重 写,使得AOF文件的体积不至于过大。
  3. 只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
  4. 同时开启两种持久化方式
  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF 文件保存的数据集要比RDB文件保存的数据集要完整。
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有 AOF可能潜在的Bug,留着作为一个万一的手段。
  1. 性能
  • 因为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文件,载入较新的那个,微博就是这种架构。

Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。微信、微博、关注系统!

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

第一个消息发布者 第二个频道 第三个消息订阅者

订阅/发布消息图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nmJNJZvS-1680869584923)(E:\笔记\typora-user-images\image-20230407160803237.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eegPCVAD-1680869584924)(E:\笔记\typora-user-images\image-20230407161150163.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PNiiNU4S-1680869584925)(E:\笔记\typora-user-images\image-20230407161204957.png)]

命令

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0un5KMzV-1680869584926)(E:\笔记\typora-user-images\image-20230407161230963.png)]

订阅端

127.0.0.1:6379> subscribe hwt     #订阅一个频道hwt
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hwt"
3) (integer) 1
#等待读取推送信息
1) "message"    #消息
2) "hwt"        #那个频道的消息
3) "hello"      #消息的具体内容
1) "message"
2) "hwt"
3) "redis"

服务端

127.0.0.1:6379> publish hwt "hello"   #发布者发布消息到频道
(integer) 1
127.0.0.1:6379> publish hwt "redis"
(integer) 1
127.0.0.1:6379> 

原理

Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅制的底层实现,籍此加深对Redis的理解。

Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。

微信:

通过 SUBSCRIBE 命令订阅某频道后,redis-server里维护了一个字典,字典的键是一个个 频道!, 而字典的值则是一个链表,链表中保存了所有订阅这个channel客户端。SUBSCRIBE 命令的关键, 就是将客户端添加到给定 channel的订阅链表中。

通过 PUBLISH 命令向订阅者发送消息,redis-server会使用给定的频道作为键,它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这表,将消息发布给所有订阅者。

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,可以设定对某一个 key值进行消息发布及消息订阅,当一个key值上进行了消息发后,所有订阅它的客户端都会收到相应的消息。这一功能明显的用法就是用作实时消系统,比如普通的即时聊天,群聊等功能。

使用场景:

  1. 实时消息系统
  2. 实时聊天(频道当作聊天室,将消息回复给所有人即可)
  3. 订阅,关注系统都是可以的

稍微复杂的场景我们会使用消息中间件MQ(专业的交给专业的人来做)

10、Redis主从复制

10.1、概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器、前者为主节点(master/leadaer),后者称为从节点(slave/follower)

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

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

主从复制的作用主要

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

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

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

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

    电商网站上的商品,一般都是一次上传,无数次浏览,说专业点也就是“多读少写”。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Vy7JGpD-1680869584927)(E:\笔记\typora-user-images\image-20230407163929624.png)]

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

10.2、环境配置

只配从库不用配主库!

127.0.0.1:6379> info replication   #查看当前库的信息
# Replication
role:master  # 角色
connected_slaves:0    #没有主机
master_replid:88e464c9d558cac433114ab68ed16ebe35067928
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

复制三个配置文件,并修改其对应信息

  1. 端口 port
  2. pid进程名字
  3. log文件名字
  4. dump.rdb名字

然后用三个配置文件去启动启动之后用ps -ef|grep redis查看是否开启

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WV7f6Yfe-1680869584928)(E:\笔记\typora-user-images\image-20230407174439316.png)]

10.3、一主二从

默认情况下,每台Redis服务器都是主节点

可以在从机上面用

slaveof ip 端口 来实现选取主机自己当从机的操作

info replication来看具体信息

(如果看不到那就说明你主机和从机设置了密码的原因把密码注释掉就行了)

127.0.0.1:6382> slaveof 127.0.0.1 6380   #slaveof ip 端口选取主机自己当从机
OK
127.0.0.1:6382> info replication
# Replication
role:slave    #当前角色从机
master_host:127.0.0.1    #可以看到主机的信息
master_port:6380
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:756ce276d102d7b8982fe09c3b76afb4507b29b7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14

#在主机中查看
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:1     #多了从机的配置
slave0:ip=127.0.0.1,port=6382,state=online,offset=28,lag=0
master_replid:756ce276d102d7b8982fe09c3b76afb4507b29b7
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

两个配置完主机下面显示两个从机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLhn42Yi-1680869584928)(E:\笔记\typora-user-images\image-20230407180622385.png)]

通过命令配置的是重启就消息正常要去配置文件配就是永久的

主从特点

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

如果主机断开连接了,从机依旧是连接到主机的,但是写操作做不了了,但如果主机又回来了,从机依旧可以直接获取到主机写进去的信息!

复制原理

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

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

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

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

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

层层链路

上一个M连接下一个从S

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1EfuJ5fX-1680869584929)(E:\笔记\typora-user-images\image-20230407182133517.png)]

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

如果没有老大了,这个时候能不能选择一个老大出来呢?哨兵模式出来前这个过程要手动操作的

81自动成为主节点

如果主机断了的话我们可以slaveof no one来让自己变成主机!其他节点就可以手动连接到最新的主节点(手动)

10.4、哨兵模式

(自动选举老大的模式)

概述

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

谋权错位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDOBMbdX-1680869584929)(E:\笔记\typora-user-images\image-20230407182640750.png)]

这里的哨兵有两个作用:

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

吐槽:这东西有点像心跳

上面为单机哨兵但如果哨兵g了怎么办所以我们可以使用多个哨兵进行监控。各个哨兵之间互相监控,这样就形成了多哨兵模式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wPRfqa1V-1680869584930)(E:\笔记\typora-user-images\image-20230407182728936.png)]

假设主服务器宕机,哨兵一先检测到这个结果,系统并不会马上进行failover过程仅仅是哨兵1主管的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让哥哥哨兵把自己监控的从服务器实现切换主机,这个过程成为客观下线

测试

我们目前状态时1主2从

  1. 配置哨兵配置文件sentinel.conf(没有就新建呗就是没有)
#第一个值为哨兵  第二个为监视  第三个为哨兵的自定义名字
#第四个值时ip地址  第五个为端口  最后的1时当主机g了会让他们重新投票
sentinel monitor myredis 127.0.0.1 6379 1

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

  1. 配置完之后启动哨兵(用redis中的redis-sentinel以及你的配置文件来启动)
[root@root bin]# redis-sentinel hconfig/sentinel.conf
7433:X 07 Apr 2023 18:38:24.597 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7433:X 07 Apr 2023 18:38:24.597 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=7433, just started
7433:X 07 Apr 2023 18:38:24.597 # Configuration loaded
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.8 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 7433
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                   
  `-._    `-._`-.__.-'_.-'    _.-'                     
      `-._    `-.__.-'    _.-'                         
          `-._        _.-'                          
              `-.__.-'                                               

7433:X 07 Apr 2023 18:38:24.603 # Sentinel ID is 0236b232d243de98983a9b529d9fe1b295dd56ed
7433:X 07 Apr 2023 18:38:24.603 # +monitor master myredis 127.0.0.1 6380 quorum 1
7433:X 07 Apr 2023 18:38:24.604 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6380

哨兵日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zc9LdiNb-1680869584930)(E:\笔记\typora-user-images\image-20230407184613696.png)]

如果主机回来了就会自动变成从机,这就是哨兵模式的规则!

哨兵模式

优点:

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

缺点:

  1. Redis不好在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦!
  2. 实现烧饼模式的配置很麻烦的,里面由很多选择

哨兵模式全部配置

# 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 

# 当在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> <milliseconds>
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 # 一般都是由运维来配置!

停用哨兵的方法(下面是杀掉redis所有进程了不建议尝试)

使用ps -ef锁定redis的进程,然后kill掉,命令如下:
ps -ef|grep redis|grep -v grep|awk '{print $2}'|xargs kill -9
解释一波:
查看redis进程:
ps -ef|grep redis|grep -v grep
这里用管道查看redis相关进程(grep redis),同时过滤掉不需要的(grep -v {关键字}),以免误伤。
可以边查边看,如果还需过滤,添加grep -v {关键字}即可。
也可以锁定指定IP、指定端口的redis,以6389端口为例,查看命令如下:
ps -ef|grep redis|grep 6389|grep -v grep
过滤到只剩下redis的进程,使用awk '{print $2}'获取进程信息的第2项,即进程号(pid)。
最后kill -9强制杀死该进程。

11、Redis缓存穿透和雪崩(面试高频,工作常用!)

服务的高可用问题!

在这里我们不会详细的区分析解决方案的底层!

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一 些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据 的一致性要求很高,那么就不能使用缓存。

另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kjnz6Tz0-1680869584931)(E:\笔记\typora-user-images\image-20230407185439151.png)]

11.1、缓存穿透(查不到)

概念

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

解决方案

11.2、布隆过滤器

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dzZVZ6xL-1680869584931)(E:\笔记\typora-user-images\image-20230407185534180.png)]

11.3、缓存空对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YddWQNxi-1680869584932)(E:\笔记\typora-user-images\image-20230407185556911.png)]

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

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

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中 对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一 个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访 问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期

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

加互斥锁

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YtDqlG2D-1680869584932)(E:\笔记\typora-user-images\image-20230407185949614.png)]

11.5、缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!

产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bvhcAI2c-1680869584933)(E:\笔记\typora-user-images\image-20230407185908163.png)]

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然 形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就 是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知 的,很有可能瞬间就把数据库压垮。

解决方案

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

限流降级(在SpringCloud讲解过!)

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

,那么该脚本稍后将会被再次执行,重复次数目前默认为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 # 一般都是由运维来配置!


> 停用哨兵的方法(下面是杀掉redis所有进程了不建议尝试)

```bash
使用ps -ef锁定redis的进程,然后kill掉,命令如下:
ps -ef|grep redis|grep -v grep|awk '{print $2}'|xargs kill -9
解释一波:
查看redis进程:
ps -ef|grep redis|grep -v grep
这里用管道查看redis相关进程(grep redis),同时过滤掉不需要的(grep -v {关键字}),以免误伤。
可以边查边看,如果还需过滤,添加grep -v {关键字}即可。
也可以锁定指定IP、指定端口的redis,以6389端口为例,查看命令如下:
ps -ef|grep redis|grep 6389|grep -v grep
过滤到只剩下redis的进程,使用awk '{print $2}'获取进程信息的第2项,即进程号(pid)。
最后kill -9强制杀死该进程。

11、Redis缓存穿透和雪崩(面试高频,工作常用!)

服务的高可用问题!

在这里我们不会详细的区分析解决方案的底层!

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一 些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据 的一致性要求很高,那么就不能使用缓存。

另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

[外链图片转存中…(img-kjnz6Tz0-1680869584931)]

11.1、缓存穿透(查不到)

概念

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

解决方案

11.2、布隆过滤器

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

[外链图片转存中…(img-dzZVZ6xL-1680869584931)]

11.3、缓存空对象

[外链图片转存中…(img-YddWQNxi-1680869584932)]

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

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

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中 对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一 个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访 问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期

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

加互斥锁

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

[外链图片转存中...(img-YtDqlG2D-1680869584932)]

11.5、缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!

产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

[外链图片转存中...(img-bvhcAI2c-1680869584933)]

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然 形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就 是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知 的,很有可能瞬间就把数据库压垮。

解决方案

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

限流降级(在SpringCloud讲解过!)

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值