Redis笔记

Redis 是一个高性能的key-value数据库,非关系型数据库。


NoSQL概述

NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,特别是大数据应用难题。摘自百度百科


为什么要使用NoSQL

  1. 单机mysql的年代:

90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!那个时候,更多的去使用静态网页html,服务器根本没有太大的压力。

瓶颈:

  • 数据量太大,一个机器放不下了
  • 数据的索引(B+Tree),一个机器内存也放不下
  • 访问量(读写混合),一个服务器承受不了
  1. Memcached(缓存)+ MySQL + 垂直拆分(读写分离)

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

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

  1. 分库分表 + 水平拆分 + MySQL集群

技术和业务在发展的同时,对人的要求也越来越高!
本质:数据库(读,写)
早些年MyISAM:表锁,十分影响效率!高并发下就会出现严重的锁问题
转战lnnodb:行锁
慢慢的就开始使用分库分表来解决写的压力!MySQL 在那个年代推出了表分区!这个并没有多少公司使用!
MySQL的集群,很好满足哪个年代的所有需求!

  1. 如今的年代

2010-2020 十年之间,世界已经发生了翻天覆地的变化;(定位,也是一种数据,音乐,热榜!)
MSQL 等关系型数据库就不够用了!数据量很多,变化很快~
MySQL有的使用它来存储一些比较大的文件,博客,图片!数据库表很大,效率就低了!如果有一种数据库来专门处理这种数据,MySQL压力就变得十分小(研究如何处理这些问题!)大数据的IO压力下,表几乎没法更大!

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

为什么要用NoSQL

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


什么是NoSQL

NOSQL= Not Only SQL(不仅仅是SQL)
关系型数据库:表格,行,列

泛指非关系型数据库的,随着veb2.0互联网的诞生!传统的关系型数据库很难应对web2.0时代!尤其是超大规模的高并发的社区!暴露出来很多难以克服的问题,NOSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的,而且是我们当下必须要掌握的一个技术!
很多的数据类型:用户的个人信息、社交网络、地理位置等。这些数据类型的存储不需要一个固定的格式!不需要多余的操作就可以橫向扩展的!Map<String,object>使用键值对来控制!

NoSQL特点

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

传统的 RDBMS(Relational Database Management System,关系数据库管理系统)
- 结构化组织
- SOL
- 数据和关系都存在单独的表中(row,col)
- 操作操作,数据定义语言
- 严格的一致性
- 基础的事务
- ......
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性,
- CAP定理和BASE(异地多活) 初级架构师!(狂神理念:只要学不死,就往死里学!)
- 高性能,高可用,高可扩
- ......

了解:3v+3高

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

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

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

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

真正在公司中的实践:NOSQL + RDBMS 一起使用才是最强的!
技术没有高低之分,就看你如何去使用!(提升内功,思维的提高!)


阿里巴巴演进分析

思考问题:这么多东西难道都是在一个数据库中的吗?

技术急不得,越是慢慢学 ,才能越扎实!
开源才是技术的王道!
任何一家互联网的公司,都不可能只是简简单单让用户能用就好了!
大量公司做的都是相同的业务;(竞品协议)
随着这样的竞争,业务是越来越完善,然后对于开发者的要求也是越来越高!

没有什么是加一层解决不了的!

# 1、商品的基本信息
名称、价格、商家信息
关系型数据库就可以解决了! MySQL / oracle(淘宝早年就去IOE了!一 王坚。推荐文章:阿里云的这群疯子)
淘宝内部的 MySQL 不是大家用的 MySQL

# 2、商品的描述、评论(文字比较多)
文档型数据库中,MongoDB

# 3、图片
分布式文件系统 FastDFS
- 淘宝自己的 TFS
- Google的 GFS
- Hadoop HDFS
- 阿里云的 OSS

# 4、商品的关键字 (搜索)
- 搜素引擎 solr elasticsearch
- Isearch:多隆(多去了解一下这些技术大佬!)
所有的牛人都有一段艰苦奋斗的岁月!但是你只要像SB一样的去坚持,终将牛逼!

# 5、热门商品的波动信息
- 内存数据库
- Redis Tair、Memache...

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

要知道,一个简单地网页背后的技术一定不是大家所想的那么简单!
大型互联网应用问题:

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

解决问题:

这里以上都是NoSQL入门概述,不仅能够提高大家的知识,还可以帮助大家了解大厂的工作内容!


NoSQL的四大分类

KV键值对:

  • 新浪:Redis

  • 美团:Redis + Tair

  • 阿里、百度:Redis + memecache

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

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

列存储数据库:

  • HBase

图形关系数据库:

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

四者对比

敬畏之心可以使人进步!



Redis入门

概述

Redis是什么?

Redis(Remote Dictionary Server ),即远程字典服务。

是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费和开源!是当下最热门的 NoSQL技术之一!也被人们称之为结构化数据库!

Redis 能干嘛?

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

特性

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

学习中需要用到的东西

官网:http://www.redis.cn

下载地址:http://www.redis.cn/download.html

注意:Windows版本在Github上下载(停更很久了),Redis推荐在Linux系统上搭建。


MacOS/Linux安装

参考官网"下载页面"下方的文档:http://www.redis.cn/download.html

进入要保存redis源程序的路径,使用下边的命令下载redis:wget http://download.redis.io/releases/redis-6.0.6.tar.gz

扩展:建议使用homebrew安装redis,见后边笔记

解压

解压Redis的安装包!程序一般放在/opt

暂时申请root权限(输入当前用户的密码):
sudo su

复制到opt目录(Mac OS下载的文件一般保存在~/Downloads目录下):
cp ~/Downloads/redis-6.2.4.tar.gz redis-6.2.4.tar.gz

解压:
tar -zxvf redis-6.2.4.tar.gz

进入解压后的文件目录:
cd redis-6.2.4

编译&安装

编译redis(进入解压后的文件目录后):
make

确认是否编译成功:
make install

若编译失败,则需要安装make命令相关的依赖,gcc-c++。Mac OS自带无需担心。

Redis的默认安装路径/usr/local/bin(用户自行编译安装时默认的可执行程序的安装位置)

homebrew安装

笔记摘自:

注意:使用homebrew安装的软件,会安装在homebrew的根目录下(一般是/opt/homebrew)。所以redis的相关文件也在其中。本案例中,redis的配置文件在/opt/homebrew/etc/redis.conf

使用homebrew安装redis。

# 命令安装
brew install redis

# 初次启动
brew services start redis

# 连接redis
redis-cli
> set name nazimei
OK
> ping
PONG

# 关闭redis服务器,并退出
> shutdown
not connected> quit

配置

  • 可执行程序,在/usr/local/bin

    • 使用homebrew安装的redis,在/opt/homebrew/bin
  • 相关的配置文件,在/opt

    • 使用homebrew安装的redis,在/opt/homebrew/etc/redis.conf

扩展:opt有可选的意思,用来安装附加软件包。是用户级的程序目录,安装到/opt目录下的程序,它所有的数据、库文件等等都是放在同个目录下面。

/usr/local/bin下新建一个myconf文件夹,专门保存程序运行所需要的配置文件。将opt下的redis默认配置文件拷贝一份到myconf文件夹中,初始配置文件redis.conf,一般在/opt/redis-版本号/redis.conf

//创建myconf文件夹,并将配置文件redis.conf备份到该文件夹下:
mkdir /usr/local/bin/myconf && cp /opt/redis-6.2.4/redis.conf /usr/local/bin/myconf

//homebrew安装的redis,备份配置文件为 myredis.conf
qsdbl@macbook ~ % cd /opt/homebrew/etc
qsdbl@macbook etc % sudo cp redis.conf myredis.conf

//备份文件夹可以在任意路径下创建,但是建议放在/usr/local/bin下,跟可执行程序放一起。

后台启动

1.Redis默认不是后台启动的,需要修改配置文件。(**注意:**homebrew下载的redis,使用brew命令启动,默认是以守护线程的方式运行)

//使用vim编辑器打开配置文件:
vim redis.conf

输入英文冒号:进入底线命令模式
输入命令set nu回车显示行号
输入命令/daemonize回车定位到字符daemonize(作为守护线程运行)
按i键,进入编辑模式。将后边的no更改为yes

按ESC键退出编辑模式,输入英文冒号:进入底线命令模式
输入wq回车,保存并退出vim编辑器

//默认的配置文件,不做修改:
/opt/redis-6.2.4/redis.conf
//修改 拷贝到myconf的配置文件(自定义):
/usr/local/bin/myconf/redis.conf

protected

protected模式,会导致远程连接失败。在学习阶段可将protected模式关闭,方便远程连接。

2.关闭protected模式,以使用redis可视化工具redisinsight连接虚拟机中的redis服务器。

1.修改配置文件
找到 protected-mode yes,修改为protected-mode no

2.临时关闭。启动时添加选项--protected-mode no
redis-server --protected-mode no

3.若不生效,可两个都试试(或注释掉配置)

配置文件详细介绍,见后边的笔记

启动&关闭

启动Redis服务

#会默认调用/usr/local/bin路径下的redis-server程序(默认安装路径),后边指定配置文件(前边配置后台方式运行的默认配置文件,这种启动方式不会打印出redis的logo)
#若不指定配置文件,则会使用默认的/opt/redis-6.2.4/redis.conf


#启动:使用/usr/local/bin/myconf下的配置文件启动
redis-server /usr/local/bin/myconf/redis.conf

#homebrew安装的redis,启动:
redis-server /opt/homebrew/etc/myredis.conf
#或:(使用brew启动,使用默认配置文件。若要自定义配置文件,使用上边的命令启动)
brew services start redis
#关闭(只能通过下边命令关闭,否则homebrew会自动启动)
brew services stop redis


#关闭:后台运行的服务,可通过redis客户端连接后使用命令shutdown关闭服务或系统命令关闭服务

测试:

#使用内置的客户端与Redis进行交互(会默认调用/usr/local/bin路径下的redis-cli程序),后边指定已启动的Redis服务的端口号,默认为6379(以配置文件中的为准)
redis-cli -p 6379

命令ping,用于测试连接
命令set key名 value值,用于设置key/value键值对
命令get key名,用于获取value值
命令keys *,用于查询所有的key 
命令shutdown,用于关闭Redis服务(服务端程序)
命令exit,用于退出内置的redis客户端

#命令ps查看启动的redis服务进程:
ps -ef | grep redis

#字段含义如下:
UID  PID  PPID  C  STIME  TTY  TIME  CMD
UID:程序被该 UID 所拥有
PID:就是这个程序的 ID 
PPID:则是其上级父程序的ID
C:CPU使用的资源百分比
STIME:系统启动时间
TTY:登入者的终端机位置
TIME:使用掉的CPU时间。
CMD:所下达的是什么指令

#除了前边的命令shutdown可以关闭redis服务,还可以使用kill命令结束程序
kill -9 程序PID

c


测试性能

redis-benchmark 是一个压力测试工具!官方自带的性能测试工具!

命令参数:

测试:

#测试本地redis服务(默认6379端口),100个并发,10万条请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
#可简写为:
redis-benchmark -c 100 -n 100000



#若内存不够,可能会报“Error from server: MISCONF Redis is configured to save RDB snapshots, but it is...“类似的错误。



#启动redis服务(配置文件我拷贝到/usr/local/bin下的myconf文件夹下了,与前边有点出入)
qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % redis-server /usr/local/bin/myconf/redis.conf 
#开始测试
qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % redis-benchmark -c 100 -n 100000 
====== PING_INLINE ====== #ping操作                                                     
  100000 requests completed in 0.50 seconds #在0.50秒内完成100000个请求
  100 parallel clients #100个并行客户端
  3 bytes payload #3字节有效负载
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

Latency by percentile distribution:
0.000% <= 0.111 milliseconds (cumulative count 1)
50.000% <= 0.255 milliseconds (cumulative count 57343) #0.255毫秒完成50%
...
====== SET ====== #set操作                                                      
  100000 requests completed in 0.47 seconds
  100 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

Latency by percentile distribution:
0.000% <= 0.087 milliseconds (cumulative count 1)
50.000% <= 0.255 milliseconds (cumulative count 59445)
...
====== GET ====== #get操作                                                      
  100000 requests completed in 0.46 seconds
  100 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

Latency by percentile distribution:
0.000% <= 0.119 milliseconds (cumulative count 1)
50.000% <= 0.247 milliseconds (cumulative count 51945)
...

基础的知识

redis默认有16个数据库。

#打开配置文件
vim /usr/local/bin/myconf/redis.conf

输入英文冒号:进入底线命令模式
输入命令set nu回车显示行号
输入命令/databases回车定位到字符databases/跟字符,搜索命令)
可以看到默认有16个数据库

默认使用的是第0个(0-15)。

qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % redis-cli
127.0.0.1:6379> ping
PONG
#查看数据库大小(数据量)
127.0.0.1:6379> dbsize
(integer) 6
127.0.0.1:6379> keys *
1) "passwd"
2) "myhash"
3) "key:__rand_int__"
4) "mylist"
5) "name"
6) "counter:__rand_int__"

#切换数据库
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> select 2
OK
127.0.0.1:6379[2]> select 15
OK
#16个数据库,0到15,所以切换到16时报错
127.0.0.1:6379[15]> select 16
(error) ERR DB index is out of range
#查看数据库大小
127.0.0.1:6379[15]> dbsize
(integer) 0
127.0.0.1:6379[15]> keys *
(empty array)
127.0.0.1:6379[15]> 

Redis是单线程的

明白Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!
Redis 是C 语言写的,官方提供的数据为 100000+ 的QPS(每秒查询率),完全不比同样是使用key-vale的Memecache差!

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

  • 误区1:高性能的服务器一定是多线程的?
  • 误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
  • 先去CPU>内存>硬盘的速度要有所了解!

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



五大数据类型

官网文档

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


Redis Key

键值对&数据库

  • keys命令,查看所有的key
  • flushdb命令,清空当前数据库中的所有 key
  • flushall命令,清空所有数据库中的 key
  • set命令,添加一个key/value键值对
  • get命令,查看value
  • move命令,移动键值对到其他数据库
  • select命令,切换数据库(默认16个,0-15)
#启动redis,并指定配置文件:
[root@localhost bin]# redis-server myconf/redis.conf 

#打开内置的客户端:
[root@localhost bin]# redis-cli

#查看所有的key
127.0.0.1:6379> keys *
1) "key:__rand_int__"
2) "myhash"
3) "counter:__rand_int__"
4) "mylist"
5) "name"

#清空当前数据库中的所有 key
127.0.0.1:6379> flushdb
OK

#清空所有数据库中的 key
127.0.0.1:6379> flushall
OK
#查看所有的key
127.0.0.1:6379> keys *
(empty array)

#添加一个key/value键值对
127.0.0.1:6379> set name qsdbl
OK
#添加一个key/value键值对
127.0.0.1:6379> set passwd 123
OK
#查看所有的key
127.0.0.1:6379> keys *
1) "passwd"
2) "name"

#移动key为name的键值对到数据库1(默认有16个数据库,0-15,默认进入数据库0)
127.0.0.1:6379> move name 1
(integer) 1
#查看所有的key
127.0.0.1:6379> keys *
1) "passwd"

#切换到数据库1
127.0.0.1:6379> select 1
OK
#查看所有的key
127.0.0.1:6379[1]> keys *
1) "name"

过期时间

  • exists命令,查看是否存在某个key
  • expire命令,设置过期时间
  • persist命令,移除过期时间
  • ttl命令,查看过期时间
#查看是否存在key为name的键值对,1-存在,0-不存在
127.0.0.1:6379[1]> exists name
(integer) 1

#给key为name的键值对设置过期时间(单位秒)
127.0.0.1:6379[1]> expire name 5
(integer) 1

#查看key为name的键值的过期时间(单位秒)
127.0.0.1:6379[1]> ttl name
(integer) 3
#查看所有的key
127.0.0.1:6379[1]> keys *
1) "name"
#查看所有的key(过期时间到后,自动删除)
127.0.0.1:6379[1]> keys *
(empty array)

#添加一个key/value键值对
127.0.0.1:6379[1]> set passwd 123345
OK
#查看过期时间,-1表示没有设置过期时间
127.0.0.1:6379[1]> ttl passwd
(integer) -1

#设置过期时间为30秒
127.0.0.1:6379> expire passwd 30
(integer) 1
127.0.0.1:6379> ttl passwd
(integer) 27
#移除过期时间
127.0.0.1:6379> persist passwd
(integer) 1
#移除成功
127.0.0.1:6379> ttl passwd
(integer) -1

类型&删除

  • type命令,查看value的类型
  • del命令,删除键值对
#查看类型
127.0.0.1:6379[1]> type passwd
string

#删除key为passwd的键值对
127.0.0.1:6379[1]> del passwd
(integer) 1
#查看所有的key(空,说明删除成功)
127.0.0.1:6379[1]> keys *
(empty array)

String

追加&长度

  • append命令,往value值后边追加字符(拼接到后边)
  • strlen命令,获取value的长度(string length ==> strlen)
#查看redis服务是否已启动
qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % ps -ef | grep redis
  501  1358  1289   0  9:55下午 ttys000    0:00.00 grep redis
#redis服务还没启动,启动redis(使用myconf文件夹下的配置文件)
qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % redis-server /usr/local/bin/myconf/redis.conf 
#再次查看redis服务是否已启动
qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % ps -ef | grep redis
  501  1373     1   0  9:55下午 ??         0:00.02 redis-server 127.0.0.1:6379 
  501  1376  1289   0  9:55下午 ttys000    0:00.00 grep redis

#打开内置的客户端:
qingshuaidebaoluo@qingshuaidebaoluoMac-mini ~ % redis-cli 
#测试连接
127.0.0.1:6379> ping
PONG
#获取(查看)所有key
127.0.0.1:6379> keys *
1) "mylist"
2) "myhash"
3) "passwd"
4) "key:__rand_int__"
5) "counter:__rand_int__"
6) "name"

#查看key为name的value值
127.0.0.1:6379> get name
"qsdbl"
#往key为name的value后边追加字符“-01”,返回值为value的长度
127.0.0.1:6379> append name "-01"
(integer) 8
#再次查看key为name的value值
127.0.0.1:6379> get name
"qsdbl-01"

#判断是否存在key为name的键值对,0-不存在,1-存在
127.0.0.1:6379> exists name
(integer) 1

#查看key为name的value值的长度(string length ==> strlen)
127.0.0.1:6379> strlen name
(integer) 8

数值增减

  • incr命令,数字值增一(increase,增加)

  • decr命令,数字值减一(decrease,减少)

  • incrby命令,增加指定的数量

  • decrby命令,减少指定的数量

#添加一个键值对。key为num,value为0
127.0.0.1:6379> set num 0
OK
#增一
127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> get num
"1"

#减一
127.0.0.1:6379> decr num
(integer) 0
127.0.0.1:6379> get num
"0"

#增5
127.0.0.1:6379> incrby num 5
(integer) 5
127.0.0.1:6379> get num
"5"

#减5
127.0.0.1:6379> decrby num 5
(integer) 0
127.0.0.1:6379> get num
"0"

范围

  • getrange / substr命令,获取value中某个范围内的字符(头尾索引都包括)
  • setrange命令,替换指定位置开始的字符串(注意是替换不是插入)
#查看key为name的键值对的value
127.0.0.1:6379> get name
"qsdbl-01"
#获取key为name的键值对的value值中索引在1到4之间的字符(包括前后索引的字符)
127.0.0.1:6379> getrange name 1 4
"sdbl"
#后一个索引值为-1,则获取到最后一个字符。例如:0 -1,获取所有字符(从0开始到最后)
127.0.0.1:6379> getrange name 0 -1
"qsdbl-01"

#substr命令与getrange命令作用相同
127.0.0.1:6379> substr name 1 4
"sdbl"
127.0.0.1:6379> substr name 0 -1
"qsdbl-01"

#替换指定位置开始的字符串(不是插入,而是替换掉。例如下边的例子,字符zeroOne长度为7,则从索引6开始后边的7个字符替换为zeroOne,没有则新增)
127.0.0.1:6379> setrange name 6 zeroOne
(integer) 13
127.0.0.1:6379> get name
"qsdbl-zeroOne"
127.0.0.1:6379> setrange name 6 0000
(integer) 13
127.0.0.1:6379> get name
"qsdbl-0000One"

新建相关

  • setex命令,set with expire,添加键值对与设置过期时间
  • setnx命令,set if not exist,键值对不存在则新建(在分布式锁中会常常使用)
  • mset命令,同时创建多对键值对
  • msetnx命令,如果不存在,则创建键值对(同时创建多对)。原子性操作,要么全部创建,要么全部不创建。
#查看所有key
127.0.0.1:6379> keys *
1) "mylist"
2) "myhash"
3) "passwd"
4) "num"
5) "key1"
6) "key:__rand_int__"
7) "counter:__rand_int__"
8) "name"

#创建键值对,key为key01,value为hello。同时设置过期时间为10秒
127.0.0.1:6379> setex key01 10 "hello"
OK
127.0.0.1:6379> ttl key01
(integer) 6
127.0.0.1:6379> get key01
"hello"
#10秒后,key为key01的键值对被删掉了
127.0.0.1:6379> ttl key01
(integer) -2
127.0.0.1:6379> get key01
(nil)

#如果不存在,则创建键值对。key为num,value为6666。由前边keys命令执行结果可知,已存在key为num的键值对,所以创建失败,返回0
127.0.0.1:6379> setnx num 6666
(integer) 0
#创建成功,返回1
127.0.0.1:6379> setnx num02 6666
(integer) 1
127.0.0.1:6379> get num
"0"
127.0.0.1:6379> get num02
"6666"

#切换到数据库1(默认16个,0-15)
127.0.0.1:6379> select 1
OK
#同时创建三对键值对
127.0.0.1:6379[1]> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379[1]> keys *
1) "k2"
2) "k1"
3) "k3"

#如果不存在,则创建键值对。原子性操作,要么全部创建,要么全部不创建。
127.0.0.1:6379[1]> msetnx k1 v1 k4 v4
(integer) 0
#因为已经存在k1所以全部都不创建,k4没有被创建
127.0.0.1:6379[1]> keys *
1) "k2"
2) "k1"
3) "k3"
  • getset命令,先获取再设置(键值对),不存在则返回nil
#如果不存在值,则返回nil
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"

应用场景

value除了可以是字符串还可以是数字

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

案例

  • 利用mset命令、mget命令,保存、获取对象。(对比Hash中的案例,比较两者的优缺点)
#一般用法:使用set、get命令保存对象
#user:001,作为key,user:后边为用户id。value中保存对象各属性值
127.0.0.1:6379[1]> set user:001 {name:zhansan,age:3}
OK
127.0.0.1:6379[1]> get user:001
"{name:zhansan,age:3}"

#使用mset,同时设置多个键值对。下边的例子,同时保存了用户名、年龄、性别。
#第一对键值对:
#key为user:002:name,用:分隔,前边的为类名,中间的为id,后边的为属性名
#value为zhansan
127.0.0.1:6379[1]> mset user:002:name zhansan user:002:age 18 user:002:sex man
OK
127.0.0.1:6379[1]> mget user:002:name user:002:age user:002:sex
1) "zhansan"
2) "18"
3) "man"

总结:Hash更适合对象信息的保存(经常变动),String更适合字符串存储。


List

List列表,基本的数据类型,有序可重复。在redis里面,我们可以把list当作栈、队列、阻塞队列使用。

所有的list命令都是L开头的。(命令不区分大小写)

基本命令

  • 将List看作是一个横着放的容器,添加元素时从左边放入,取出时从右边取

  • Lpush命令,L - list,push - 推进,往list中添加元素(同时创建list)。L也可以看作是left,即从左边推进(添加)元素(一个或多个元素)。

  • Rpush命令,R - right,push - 推进,往list的右边推进(添加)元素

  • Lrange命令,L - list,range - 范围,根据索引获取list中某段范围内的元素。用法与前边的getrange类似。

#切换到数据库2(默认16个,0-15)
127.0.0.1:6379[1]> select 2
OK
#查看当前数据库中所有的键值对(为空)
127.0.0.1:6379[2]> keys *
(empty array)

#创建list,名为list1,往list1中添加元素1
127.0.0.1:6379[2]> Lpush list1 1
(integer) 1
#往list1中添加元素2
127.0.0.1:6379[2]> Lpush list1 2
(integer) 2
#往list1中添加元素3
127.0.0.1:6379[2]> Lpush list1 3
(integer) 3

#根据索引获取list中某段范围内的元素。索引0到-1,表示获取该list中的全部元素
#我们发现取出的元素的顺序是3、2、1,而我们前边往list中添加元素时的顺序是1、2、3,这是因为list左进右出,见前边的解释。
127.0.0.1:6379[2]> Lrange list1 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379[2]> Lrange list1 0 1
1) "3"
2) "2"

#往list的右边推进(添加)元素
127.0.0.1:6379[2]> Rpush list1 4
(integer) 4
#取出全部的元素
#可以发现,取出的元素的顺序为3、2、1、4。元素4,添加到了最右边。
127.0.0.1:6379[2]> Lrange list1 0 -1
1) "3"
2) "2"
3) "1"
4) "4"
#获取最后一个元素(最右边的)
127.0.0.1:6379[2]> lrange list1 3 3
1) "4"

移除

  • Lpop命令,L - list,pop - 移除(爆裂),从list中移除元素。L也可以看作是left,从list的左边开始移除元素(一个或多个)
  • Rpop命令,R - right,pop - 移除,从list的右边开始移除元素
#查看名为list1的list中的所有元素
127.0.0.1:6379[2]> lrange list1 0 -1
1) "3"
2) "2"
3) "1"
4) "4"

#移除左边的第一个元素
127.0.0.1:6379[2]> Lpop list1
"3"
#查看所有元素,可以发现最左边的元素3已被移除
127.0.0.1:6379[2]> lrange list1 0 -1
1) "2"
2) "1"
3) "4"

#移除右边的第一个元素
127.0.0.1:6379[2]> Rpop list1
"4"
#查看所有元素,可以发现最右边的元素4已被移除
127.0.0.1:6379[2]> lrange list1 0 -1
1) "2"
2) "1"

#移除多个元素。从list1的左边开始,移除两个元素
127.0.0.1:6379[2]> Lpop list1 2
1) "2"
2) "1"
#查看所有元素,可以发现仅剩的两个元素已被移除,list1为空
127.0.0.1:6379[2]> lrange list1 0 -1
(empty array)
127.0.0.1:6379[2]> keys *
(empty array)
  • Lrem命令,L - list,也可以看作是left,rem - remove移除。移除指定的值,从左边开始 移除list中的n个 指定的元素
#新建mylist,同时往其中添加元素one two three four two one(list中可以有重复的元素)
127.0.0.1:6379[2]> Lpush mylist one two three four two one
(integer) 6
#查看所有元素(左进右出)
127.0.0.1:6379[2]> Lrange mylist 0 -1
1) "one"
2) "two"
3) "four"
4) "three"
5) "two"
6) "one"

#从左边开始 移除mylist中的一个 one元素(从左边开始)
127.0.0.1:6379[2]> Lrem mylist 1 one
(integer) 1
#查看mylist中的所有元素,可以发现最左边的one已被移除
127.0.0.1:6379[2]> Lrange mylist 0 -1
1) "two"
2) "four"
3) "three"
4) "two"
5) "one"

#从左边开始 移除mylist中的两个 two元素
127.0.0.1:6379[2]> Lrem mylist 2 two
(integer) 2
#查看所有元素,一左一右的两个two均已被移除
127.0.0.1:6379[2]> Lrange mylist 0 -1
1) "four"
2) "three"
3) "one"
  • Ltrim命令,L - list,也可以看作left,trim - 修剪。截取保留指定索引的元素,修剪掉其他元素。
#清空当前数据库
127.0.0.1:6379[2]> flushdb
OK
127.0.0.1:6379[2]> keys *
(empty array)

#新建mylist,同时往其中添加元素0 1 2 3 4
127.0.0.1:6379[2]> Lpush mylist 0 1 2 3 4
(integer) 5
#查看mylist中的所有元素
127.0.0.1:6379[2]> Lrange mylist 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"

#截取保留mylist中索引1到2的元素,修剪掉其他元素
127.0.0.1:6379[2]> Ltrim mylist 1 2
OK
#左进右出,索引从左边开始。故mylist[0]=4,mylist[1]=3,mylist[2]=2,...
#截取了元素3、2,修剪了其他元素
127.0.0.1:6379[2]> Lrange mylist 0 -1
1) "3"
2) "2"
  • rpoplpush命令,由rpop命令与lpush命令组成。先将列表最后一个元素移除,再将其移到其他列表中
#新建列表mylist
127.0.0.1:6379> lpush mylist 1 2 3 4 5
(integer) 5
#查看全部的元素(左进右出)
127.0.0.1:6379> lrange mylist 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"

#将mylist中最后一个元素移除,并将其移到mylist02中
127.0.0.1:6379> rpoplpush mylist mylist02
"1"
#查看mylist中全部的元素
127.0.0.1:6379> lrange mylist 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
#查看mylist02中全部的元素
127.0.0.1:6379> lrange mylist02 0 -1
1) "1"

添加

  • Lset命令,L - list,set - 添加元素(参考前边的set命令)。向list中指定索引位置添加元素,前提是存在该list(使用keys *命令或exists命令查看是否存在某个list)
#查看mylist02中全部的元素
127.0.0.1:6379> lrange mylist02 0 -1
1) "1"
#存在名为mylist02的list
127.0.0.1:6379> exists mylist02
(integer) 1

#设置索引为0的元素为one(将one放到mylist02中索引为0的位置)
127.0.0.1:6379> lset mylist02 0 one
OK
#查看mylist02中全部的元素
127.0.0.1:6379> lrange mylist02 0 -1
1) "one"

#不存在名为mylist03的list
127.0.0.1:6379> exists mylist03
(integer) 0
#无法使用lset命令向其中添加元素
127.0.0.1:6379> lset mylist03 0 one
(error) ERR no such key
  • Linsert命令,L - list,insert - 插入,往list中插入元素。
#创建名为mylist的list,并往其中添加元素0 1 2 3 4
127.0.0.1:6379> Lpush mylist 0 1 2 3 4
(integer) 5
#查看所有元素
127.0.0.1:6379> Lrange mylist 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"

#插入元素。往元素“1”的前边(before)插入元素“1.5”
127.0.0.1:6379> Linsert mylist Before 1 1.5
(integer) 6
#查看所有元素(左进右出,索引从左开始。所以mylist[0]=4,...,mylist[4]=0,元素4是在前边,元素0在最后)
127.0.0.1:6379> Lrange mylist 0 -1
1) "4"
2) "3"
3) "2"
4) "1.5"
5) "1"
6) "0"

#插入元素。往元素“1”的后边(after)插入元素“0.5”
127.0.0.1:6379> Linsert mylist After 1 0.5
(integer) 7
#查看所有元素
127.0.0.1:6379> Lrange mylist 0 -1
1) "4"
2) "3"
3) "2"
4) "1.5"
5) "1"
6) "0.5"
7) "0"

索引&长度

  • Lindex命令,L - list,index - 索引,根据索引获取list中的某个元素
  • Llen命令,L - list,len - length长度,获取list的长度
#新建list1,同时往其中添加元素1到10
127.0.0.1:6379[2]> lpush list1 1 2 3 4 5 6 7 8 9 10
(integer) 10

#获取索引为5的元素
127.0.0.1:6379[2]> Lindex list1 5
"5"
#别忘了,list是左进右出,索引从左开始。即list[0]=10,list[1]=9,...,list[5]=5,...
127.0.0.1:6379[2]> Lrange list1 0 -1
 1) "10"
 2) "9"
 3) "8"
 4) "7"
 5) "6"
 6) "5"
 7) "4"
 8) "3"
 9) "2"
10) "1"

#获取list的长度(数据量)
127.0.0.1:6379[2]> Llen list1
(integer) 10

小结

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

应用场景:消息排队,消息队列 (Lpush Rpop),栈( Lpush Lpop)等。


Set

set集合。set中的值是不能重复的(无序不可重复)。

基本命令

  • sadd命令,s - set集合,add - 添加元素。往set集合中添加元素,若该集合不存在则创建集合并添加元素,若存在则往其中添加元素。(可同时添加多个)
  • smembers命令,s - set集合,members - 成员的复数即很多的成员。查看set集合中的所有元素
  • sIsMember命令,s - set集合,is - 是否存在,member - 成员即set集合中的元素。判断某set集合中是否存在某一元素
  • scard命令,s - set集合,card - 卡片、梳理。获取set集合中的元素个数
#往名为myset的set集合中添加元素1314,若不存在集合myset则创建再往其中添加元素
127.0.0.1:6379> sadd myset 1314
(in teger) 1
127.0.0.1:6379> sadd myset 6666
(integer) 1

#查看名为myset的set集合中的所有元素
127.0.0.1:6379> smembers myset
1) "1314"
2) "6666"

#查看名为myset的set集合中 是否存在元素1314。1-存在,0-不存在
127.0.0.1:6379> sIsMember myset 1314
(integer) 1
#查看名为myset的set集合中 是否存在元素520
127.0.0.1:6379> sIsMember myset 520
(integer) 0

#获取set集合中的元素个数
127.0.0.1:6379> scard myset
(integer) 2

移除

  • srem命令,s - set集合,rem - remove移除。移除集合中的某个元素
  • spop命令,s - set集合,pop - 砰,可理解为移除。随机移除集合中的一个元素
  • smove命令,s - set集合,move - 移动。将元素从一个集合移动到另一个集合中
#移除集合中的元素1314
127.0.0.1:6379> srem myset 1314
(integer) 1
#查看myset集合中的所有元素
127.0.0.1:6379> smembers myset
1) "6666"

#往myset集合中添加元素520与1314
127.0.0.1:6379> sadd myset 520 1314
(integer) 1
#查看myset集合中的所有元素
127.0.0.1:6379> smembers myset
1) "520"
2) "1314"
3) "6666"
#随机弹出(删除)一个元素
127.0.0.1:6379> spop myset
"1314"
#查看myset集合中的所有元素
127.0.0.1:6379> smembers myset
1) "520"
2) "6666"
#随机弹出(删除)一个元素
127.0.0.1:6379> spop myset
"520"

#清空当前数据库所有的数据
127.0.0.1:6379> flushdb
OK
#创建myset集合并往其中添加元素1 2 3 4
127.0.0.1:6379> sadd myset 1 2 3 4
(integer) 4
#创建myset2集合并往其中添加元素5 6 7 8
127.0.0.1:6379> sadd myset2 5 6 7 8
(integer) 4
#将myset集合中的元素4 移动到myset2集合中
127.0.0.1:6379> smove myset myset2 4
(integer) 1
#查看myset集合中的所有元素
127.0.0.1:6379> smembers myset
1) "1"
2) "2"
3) "3"
#查看myset2集合中的所有元素
127.0.0.1:6379> smembers myset2
1) "4"
2) "5"
3) "6"
4) "7"
5) "8"

获取

  • srandmember命令,s - set集合,rand - random随机,member - 成员。随机获取集合中的元素
  • sDiff命令,s - set集合,diff - difference差异。求两集合的差集
  • sInter命令,s - set集合,inter - intersection交叉。求两集合的交集
  • sUnion命令,s - set集合,union - 联盟。求两集合的并集
#往myset集合中添加元素 520与1314
127.0.0.1:6379> sadd myset 520 1314
(integer) 1

#随机获取集合中的一个元素
127.0.0.1:6379> srandmember myset
"520"
127.0.0.1:6379> srandmember myset
"6666"

#随机获取集合中的两个元素
127.0.0.1:6379> srandmember myset 2
1) "1314"
2) "520"
127.0.0.1:6379> srandmember myset 2
1) "1314"
2) "520"
127.0.0.1:6379> srandmember myset 2
1) "1314"
2) "6666"

#清空当前数据库所有的数据
127.0.0.1:6379> flushdb
OK
#创建集合myset1与myset2
127.0.0.1:6379> sadd myset1 1 2 3 4
(integer) 4
127.0.0.1:6379> sadd myset2 3 4 5 6
(integer) 4
#求两集合的 差集(前者为基础)
127.0.0.1:6379> sDiff myset1 myset2
1) "1"
2) "2"
127.0.0.1:6379> sDiff myset2 myset1
1) "5"
2) "6"
#求两集合的 交集(应用场景:查看共同好友)
127.0.0.1:6379> sInter myset1 myset2
1) "3"
2) "4"
#求两集合的 并集
127.0.0.1:6379> sUnion myset1 myset2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"

应用场景

微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!
共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)


Hash(哈希)

Hash是一个 string类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。(field - value映射可以看作是键值对key - value)

创建

  • hset命令,h - hash,set - 创建/添加。创建一个Hash,并往其中添加键值对(一个或多个)
  • hsetnx命令,set if not exist,键值对不存在则新建
#清空所有数据库
127.0.0.1:6379> flushall
OK
#查看当前数据库的所有数据
127.0.0.1:6379> keys *
(empty array)

#创建一个Hash,名为myhash1。并往其中添加键值对,key为key1,value为value1
127.0.0.1:6379> hset myhash1 key1 value1
(integer) 1
#创建一个Hash,名为myhash2。并往其中添加两个键值对
127.0.0.1:6379> hset myhash2 key1 value1 key2 value2
(integer) 2
#创建一个Hash,并往其中添加多个键值对,该命令已废弃,建议使用hset。
127.0.0.1:6379> hmset myhash3 key1 value1 key2 value2 key3 value3
OK

127.0.0.1:6379> hgetall myhash1
1) "key1"
2) "value1"
#键值对不存在则新建(返回1,创建成功)
127.0.0.1:6379> hsetnx myhash1 key3 value3
(integer) 1
#键值对不存在则新建(返回0,创建失败)
127.0.0.1:6379> hsetnx myhash1 key3 value3
(integer) 0
127.0.0.1:6379> hgetall myhash1
1) "key1"
2) "value1"
3) "key3"
4) "value3"

获取

  • hget命令,h - hash,get - 获取。获取Hash中的某个field的value值
  • hmget命令,h - hash,m - more多个,get - 获取。获取Hash中的多个field的value值
  • hgetall命令,h - hash,get - 获取,all - 全部。获取Hash中的全部的键值对(键与值,field与value)
  • hlen命令,h - hash,len - length长度。获取某个Hash的长度(多少对键值对)
  • hkeys命令,h - hash,keys - 所有的key。获取某个Hash的所有key(即键值对的键 - key)
  • hvals命令,h - hash,vals - 所有的value。获取某个Hash的所有value(即键值对的值 - value)

#获取名为myhash1的Hash中的,field为key1的value值
127.0.0.1:6379> hget myhash1 key1
"value1"

#获取名为myhash2的Hash中的,field为key1、key2的value值
127.0.0.1:6379> hmget myhash2 key1 key2
1) "value1"
2) "value2"

#获取名为myhash2的Hash中的所有键值对,field与value
127.0.0.1:6379> hgetall myhash2
1) "key1"
2) "value1"
3) "key2"
4) "value2"

127.0.0.1:6379> hset myhash f1 v1 f2 v2 f3 v3
(integer) 3
127.0.0.1:6379> hgetall myhash
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
#获取名为myhash的Hash的长度(多少对键值对)
127.0.0.1:6379> hlen myhash
(integer) 3

#获取所有的field(即键值对的键 - key)
127.0.0.1:6379> hkeys myhash
1) "f1"
2) "f2"
3) "f3"
#获取所有的value(即键值对的值 - value)
127.0.0.1:6379> hvals myhash
1) "v1"
2) "v2"

删除

  • hdel命令,h - hash,del - delete删除。根据field,删除Hash中的某个键值对
#删除 名为myhash2的Hash中的 field为key1的键值对
127.0.0.1:6379> hdel myhash2 key1
(integer) 1
#获取名为myhash2的Hash中的所有键值对
127.0.0.1:6379> hgetall myhash2
1) "key2"
2) "value2"

数值增减

String中的incrby相似,但又不同。这里增减都是使用一个命令

  • hincrby命令,增加指定的数量(使用负数,实现减少效果)
127.0.0.1:6379> hset myhash f5 5
(integer) 1
#增加指定的数量
127.0.0.1:6379> hincrby myhash f5 1
(integer) 6
127.0.0.1:6379> hget myhash f5
"6"
#减去指定的数量
127.0.0.1:6379> hincrby myhash f5 -1
(integer) 5

案例

  • 利用mset命令、mget命令,保存、获取对象。(对比String中的案例,比较两者的优缺点)
#使用hset创建一个名为user:1的Hash(以"类名:id"的方式命名)保存对象信息,将对象的属性以键值对的形式保存在Hash中
127.0.0.1:6379> hset user:1 name lily age 18 email 1135637451@qq.com
(integer) 3
127.0.0.1:6379> hset user:2 name pete age 16 email 15423xx2312@163.com 
(integer) 3
#获取名为user:1的Hash的所有键值对
127.0.0.1:6379> hgetall user:1
1) "name"
2) "lily"
3) "age"
4) "18"
5) "email"
6) "1135637451@qq.com"
#获取名为user:1的Hash中 键为name的值
127.0.0.1:6379> hget user:1 name
"lily"

127.0.0.1:6379> hgetall user:2
1) "name"
2) "pete"
3) "age"
4) "16"
5) "email"
6) "15423xx2312@163.com"

#对比 String中的案例:

#使用mset,同时设置多个键值对。下边的例子,同时保存了用户名、年龄、性别。
#第一对键值对:
#key为user:002:name,用:分隔,前边的为类名,中间的为id,后边的为属性名
#value为zhansan
127.0.0.1:6379[1]> mset user:002:name zhansan user:002:age 18 user:002:sex man
OK
127.0.0.1:6379[1]> mget user:002:name user:002:age user:002:sex
1) "zhansan"
2) "18"
3) "man"

总结:Hash更适合对象信息的保存(经常变动),String更适合字符串存储。


Zset

Zset - 有序集合。在set的基础上增加了一个值。可用于排序。

set k1 v1
zset k1 score1 v1
//score - 成绩、分值

创建

  • zadd命令,z - zset,add - 添加。添加一个或多个值。
#创建名为myset的zset,添加一个值,key为myset,value为one,score为1
127.0.0.1:6379> zadd myzset 1 one
(integer) 1
#添加多个值
127.0.0.1:6379> zadd myzset 2 two 3 three
(integer) 2

获取

获取值
  • zrange命令,z - zset,range - 范围。根据索引范围获取值。(0 到 -1,获取全部)
#获取全部值
127.0.0.1:6379> zrange myzset 0 -1
1) "one"
2) "two"
3) "three"
获取值&排序
  • zrangebyscore命令,score - 成绩、分值。根据score的值,从小到大排序。
    • zrangebyscore zset名称 区间-小 区间-大
  • zrevrangebyscore命令,rev - reverse,颠倒。根据score的值,从大到小排序。
    • zrevrangebyscore zset名称 区间-大 区间-小
    • withscores,获取zset中的数据并带上对应的score
    • -inf 无穷小,+inf 无穷大
#根据score的值,从小到大排序,区间是无穷小到无穷大即所有数据
> zrangebyscore myset -inf +inf
1) "one"
2) "two"
3) "tree"
> zrangebyscore myset  2 7
1) "two"
2) "tree"
#错误示例,从小到大排序,区间写成了大到小
> zrangebyscore myset  +inf -inf
(empty array)

#根据score的值,从大到小排序
> zrevrangebyscore myset 7 2
1) "tree"
2) "two"

#应用场景:通过升序排序(小到大),获取名为myset的zset中小于2的数据
127.0.0.1:6379> zrangebyscore myset -inf 2 
1) "one"
2) "two"
#并带上对应的score
127.0.0.1:6379> zrangebyscore myset -inf 2 withscores 
1) "one"
2) "1"
3) "two"
4) "2"
获取数量
  • zcard命令,card - 卡片、梳理。获取集合中的元素个数。
  • zcount命令,count - 数量。查看指定区间的元素数量。
#清空数据库
> flushdb
OK
#查看名为myset的zset中的元素个数。myset不存在了,也不会报错而是返回0
> zcard myset
(integer) 0
#查看所有的key(myset已被清除)
> keys *
(empty array)

#新增一个zset,名为myset,添加6个元素(值),score和元素相同
> zadd myset 1 1 3 3 7 7 9 9 13 13 77 77
(integer) 6
#查看myset中的所有元素
> zrange myset 0 -1
1) "1"
2) "3"
3) "7"
4) "9"
5) "13"
6) "77"
#查看指定区间的元素数量
> zcount myset 3 9
(integer) 3
#查看指定区间的元素数量
> zcount myset 3 79
(integer) 5
#获取集合中的元素个数
> zcard myset
(integer) 6
#对集合myset进行升序排序,获取score在3到79之间的元素
> zrangebyscore myset 3 79
1) "3"
2) "7"
3) "9"
4) "13"
5) "77"
#包括区间边界3和79

删除

  • zrem命令,rem - remove,移除。移除指定的元素(zset,有序不重复集合)
#移除指定的元素(zset,有序不重复集合)
> zrem myset 77 9
(integer) 2
#查看所有元素,77与9已被删除
> zrange myset 0 -1
1) "1"
2) "3"
3) "7"
4) "13"

应用场景

思路:set排序,存储班级成绩表,工资表排序。

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

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



三种特殊数据类型


geospatial

geospatial,地理位置。geo可用于推算地理位置的信息,两地之间的距离,方圆几里的人。

应用场景:朋友的定位,附近的人,打车距离计算。

创建

  • geoadd命令,geo - geospatial,地理位置,add - 添加。添加一个或多个值(经纬度)。
#geoadd 
#规则:两级无法直接添加,一般是下载城市数据,直接通过java程序一次性导入
#参数: key 值(经度、纬度、名称)
#经纬度查询:https://jingweidu.bmcx.com/

#longitude 经度、 latitude 纬度、 member 成员 

#创建一个名为 china:city 的geospatial,并添加一个值(经度、纬度、名称)
> geoadd china:city 116.40 39.90 beijin
(integer) 1
#添加第二个值
> geoadd china:city 121.47 31.23 shanghai
(integer) 1
#一次添加多个值
> geoadd china:city 106.50 29.53 chongqin 114.05 22.52 shengzheng 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 4

获取

  • geopos命令,获取当前定位(经纬度,先经度再纬度)
  • geodist命令,dist - 距离。计算两地之间的距离。(假设地球是完美的球形)
  • georadius命令。详情见下边。
  • georadiusbymember命令。详情见下边。
  • geohash命令。详情见下边。
#:输入地名(前边设置的value,值),返回
#获取“名称”(前边添加值时,一个值包括“经度、纬度、名称”)为“beijin”的值的经纬度
> geopos china:city beijin
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
#获取xian的经纬度
> geopos china:city beijin xian
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
2) 1) "108.96000176668167114"
   2) "34.25999964418929977"
   
# m 表示单位为米。
# km 表示单位为千米。
# mi 表示单位为英里。
# ft 表示单位为英尺。
#查看 beijin 与 beijin 之间的距离,单位为 km
> geodist china:city beijin shanghai km
"1067.3788"
  • georadius命令,radius - 半径。以给定的经纬度为中心,找出某一半径内的元素。
#获取 以给定的经纬度(110 30)为中心,1000km内的元素
> georadius china:city 110 30 1000 km
1) "chongqin"
2) "xian"
3) "shengzheng"
4) "hangzhou"
#限定数量
> georadius china:city 110 30 1000 km count 2
1) "chongqin"
2) "xian"

#返回的结果中 带上距离(距离指定的经纬度)
> georadius china:city 110 30 500 km withdist
1) 1) "chongqin"
   2) "341.9374"
2) 1) "xian"
   2) "483.8340"
#带上经纬度
> georadius china:city 110 30 500 km withcoord
1) 1) "chongqin"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) 1) "108.96000176668167114"
      2) "34.25999964418929977"
#带上经纬度 和 距离
> georadius china:city 110 30 500 km withcoord withdist
1) 1) "chongqin"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) "483.8340"
   3) 1) "108.96000176668167114"
      2) "34.25999964418929977"
  • georadiusbymember,radius - 半径,bymember - 基于member(前边添加值时,一个值包括“经度、纬度、名称member”)。找出位于指定元素周围的其它元素。
#以 beijin 为中心,获取半径为1000km的范围的其它元素
> georadiusbymember china:city beijin 1000 km
1) "beijin"
2) "xian"
#以 shanghai 为中心,获取半径为500km的范围的其它元素
> georadiusbymember china:city shanghai 500 km
1) "hangzhou"
2) "shanghai"
  • geohash命令,hash - 哈希。返回一个或多个位置元素的geohash表示。将二维的经纬度转化为一维的字符串,如果两个字符串越接近,那么距离越近。
#获取 beijin 与 chongqin 的位置信息的哈希表示
> geohash china:city beijin chongqin
1) "wx4fbxxfke0"
2) "wm5xzrybty0"

#获取 beijin 与 chongqin 的位置信息(经纬度)
> geopos china:city beijin chongqin
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
2) 1) "106.49999767541885376"
   2) "29.52999957900659211"

删除

  • geo底层的实现原理其实就是zset,我们可以使用zset命令来操作geo。例如使用“zrem”来删除geospatial中的元素
#使用type命令查看其属于5大数据类型中的哪一种,即可以使用对应的命令来操作
> type china:city
zset

#查看所有元素
> zrange china:city 0 -1
1) "chongqin"
2) "xian"
3) "shengzheng"
4) "hangzhou"
5) "shanghai"
6) "beijin"

#移除指定元素
> zrem china:city beijin
(integer) 1
> zrange china:city 0 -1
1) "chongqin"
2) "xian"
3) "shengzheng"
4) "hangzhou"
5) "shanghai"

hyperloglog

redis hyperloglog 基数统计的算法。redis2.8.9版本开始支持。

优点:占用的内存是固定,2^64不同的元素的基数,只需要12KB内存,如果要从内存角度来比较的话hyperloglog 是首选。

A{1,3,5,7,9}、B{1,3,5,9}

基数(不重复的元素) = 4,可以接受误差

应用场景:网页的UV(一个人访问一个网站多次,但是还是算作一个人)。传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断。这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数而不是保存用户id。

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

  • PFadd命令,创建。
  • PFcount命令,统计。
  • PFmerge命令,合并。
#创建第一组元素 mykey
> PFadd mykey a b c d e f g 
(integer) 1
#统计第一组元素mykey中的 基数 数量
> PFcount mykey
(integer) 7
#创建第二组元素 mykey2
> PFadd mykey2 d e f a
(integer) 1
> PFcount mykey2
(integer) 4
#创建第三组元素 mykey3,通过合并mykey与mykey2
> PFmerge mykey3 mykey mykey2
OK
> PFcount mykey3
(integer) 7

#创建第四组元素 mykey4,与mykey元素基本相同,不同点在于元素a重复了几个
> PFadd mykey4 a a a b c d e f g 
(integer) 1
#查看mykey4中的基数数量,与mykey是一致的(去重)
> PFcount mykey4
(integer) 7

bitmaps

bitmaps,位存储。

应用场景:统计疫情感染人数:0 0 0 1。统计用户信息,活跃,不活跃。登录,未登录。两个状态的,都可以使用Bitmaps

  • setbit命令,创建
    • setbit key offset value
  • getbit命令,查看
  • bitcount命令,统计
#案例:使用bitmaps来记录周一到周日的打卡情况。
#1代表已打卡,0代表未打卡
#- 周一:1,周二:1。。。

#创建一个名为daka的bitmaps
#连续往其中添加值(模拟每日打卡)
> setbit daka 0 1
(integer) 0
> setbit daka 1 1
(integer) 0
> setbit daka 2 0
(integer) 0
> setbit daka 3 1
(integer) 0
> setbit daka 4 0
(integer) 0
> setbit daka 5 0
(integer) 0
> setbit daka 6 1
(integer) 0

#查看某一天是否打卡
#查看daka中offset为0(代表星期一)的值,查看星期一的打卡情况(0 - 未打卡,1-已打卡)
> getbit daka 0
(integer) 1
#查看星期三的打卡情况
> getbit daka 2
(integer) 0

#统计操作,统计打卡的天数
> bitcount daka
(integer) 4


事务

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

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

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

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

注意:Redis单条命令是保证原子性的(要么都执行要么都不执行),但是事务不保证原子性。

Redis的事务:

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

锁:redis可以实现乐观锁,watch

执行事务

正常执行事务:

#开启事务
127.0.0.1:6379> multi
OK
#命令入队(此时命令未执行)
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
#执行事务(开始执行事务中的命令)
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v1"

放弃事务

discard,放弃事务

#开启事务
127.0.0.1:6379> multi
OK
#命令入队
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
#放弃事务
127.0.0.1:6379(TX)> discard
OK


#由于事务未执行,所以k5没有创建成功,获取不到对应的值(v5)
127.0.0.1:6379> get k5
(nil)

编译型异常

编译型异常(代码有问题,redis命令拼写错误,能在编译时检测到),事务中所有的命令都不会被执行

#清空数据库
127.0.0.1:6379> flushdb
OK

#开启事务
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
#使用了错误的redis命令,getmyset 应该为 get
127.0.0.1:6379(TX)> getmyset k2
(error) ERR unknown command `getmyset`, with args beginning with: `k2`, 
127.0.0.1:6379(TX)> set k3 v3
QUEUED
#执行事务
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

#尝试获取k1、k3的值。均没有获取到相关值,说明事务中所有的命令都没有被执行。
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> get k3
(nil)

运行时异常

运行时异常,如果事务队列中存在语法性错误(redis命令的拼写没有错误,使用上有错误,只有在运行时才会发现),那么执行命令的时候,其它命令是可以正常执行的。

#清空数据库
127.0.0.1:6379> flushdb
OK

#设置一个键值对,k1/v1,值“v1”是字符串
127.0.0.1:6379> set k1 v1
OK
#开启事务
127.0.0.1:6379> multi
OK
#命令入队
127.0.0.1:6379(TX)> set k2 v2
QUEUED
#对字符串“v1”执行命令“incr”,数字值增一。显然会报错
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> get k3
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) "v2"
5) "v3"

#获取k3的值,可以正常获取到“v3”。说明执行事务的过程中,其它命令是不受命令“incr k1”的影响,可以正常执行
#redis事务中,不保证原子性
127.0.0.1:6379> get k3
"v3"

监控,watch。面试常问

悲观锁:认为什么时候都会出问题,无论做什么都会加锁

乐观锁

  • 认为什么时候都不会出问题,不会上锁。更新数据的时候去判断一下,在此期间是否有人修改过这个数据

  • 获取version

  • 更新的时候比较version

Redis监听案例:

正常执行:

#添加两个key/value,用于记录 总金额 和 支出金额
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK

#监听money
127.0.0.1:6379> watch money
OK
#事务操作
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20

#事务操作过程中监听money,期间没有发生变动,事务能正常执行结束
#事务结束后,结束监听

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

  • watch key名,监听(加乐观锁)
  • 事务执行结束后(失败或成功都会)自动解除监听(解锁)
  • unwatch,解除监听(手动解锁)
#模拟进程1:
#查看 money 与 out 的值
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> get out
"20"
#监听money(只有在事务结束后,才会结束监听)
#此时money的值为80
127.0.0.1:6379> watch money
OK
#开启事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 200
QUEUED
127.0.0.1:6379(TX)> incrby out 200
QUEUED


#模拟进程2:
#进程1中监听money,和开启了事务(还未执行)。进程2中修改了所监听的money
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> get out
"20"
#money被修改为了88
127.0.0.1:6379> incrby money 8
(integer) 88


#模拟进程1:
#执行事务
#因为在 监听money 到 money解除监听(事务结束) 之间,被监听的money值发生了改变(由80变成了88)
#所以事务中的命令均不会被执行,并退出了事务(同时监听也解除)
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> 


#也可以使用命令 unwatch 解除监听
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> unwatch
OK

Jedis

Jedis,使用Java来操作Redis。

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

1、导入依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<!-- json工具-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.60</version>
</dependency>

2、编码测试

  • 连接数据库
  • 操作命令
  • 断开连接

案例

@SpringBootTest
class MyTest {
    @Test
    public void test() throws SQLException {
        //1、连接数据库
        Jedis jedis = new Jedis("192.168.23.131", 6379);
        //2、操作命令
        //jedis 命令 对应 方法名
        System.out.println("ping测试:"+jedis.ping());//ping 测试
        jedis.set("name","小黑");//创建key
        jedis.set("age","16");
        System.out.println("当前数据库所有key的数量:"+jedis.dbSize());//查看 当前数据库 所有key的数量
        Set<String> keys = jedis.keys("*");
        Iterator<String> iterator = keys.iterator();
        System.out.print("所有的key = ");
        String str = "[";
        while(iterator.hasNext()){
            String next = iterator.next();
            str += next+"="+jedis.get(next)+",";
        }
        if(str.length() > 1){
            System.out.println(str.substring(0,str.length()-1)+"]");
        }else{
            System.out.println(str+"]");
        }
//        System.out.println(jedis.flushDB());//删除 当前数据库 中的所有key
//        System.out.println(jedis.flushAll());//删除 所有数据库 中的所有key
        //3、断开连接
        if(jedis!=null){
            jedis.close();//释放连接
        }
//        jedis.shutdown();//关闭redis服务器
        System.out.println("ping测试:"+jedis.ping());//ping 测试。调用close()后,还是能使用jedis对象?
    }
}



ping测试:PONG
当前数据库所有key的数量:2
所有的key = [name=小黑,age=16]
ping测试:PONG

事务操作

@SpringBootTest
class MyTest {
    @Test
    public void test() throws SQLException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("sex","男");
        jsonObject.put("addr","北京朝阳");

        Jedis jedis = new Jedis("192.168.23.131", 6379);
        //开启事务
        Transaction multi = jedis.multi();
        String userInfo = jsonObject.toJSONString();
        try {
            multi.set("user1",userInfo);
            multi.set("user2",userInfo);
            int i = 1/0;//代码抛出异常,事务执行失败
            multi.exec();//执行事务
        }catch(Exception e){
            multi.discard();//放弃事务
            e.printStackTrace();
        }finally{
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();//关闭连接
        }
    }
}

//运行结果:
java.lang.ArithmeticException: / by zero
	at com.qsdbl.nazox_demo.MyTest.test(MyTest.java:39)
	...
null
null
    
    
//将错误代码”int i = 1/0;“移除后,运行结果为:
{"sex":"男","addr":"北京朝阳"}
{"sex":"男","addr":"北京朝阳"}


SpringBoot整合

jedis -> lettuce

SpringBoot操作数据:springData,jpa、jdbc、mongodb、redis

SpringBoot是和SpringData齐名的项目

说明:在SpringBoot 2.x之后,原来使用的jedis被替换为了lettuce。

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

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

SpringBoot整合:

  1. 导入依赖
  2. 配置连接
  3. 测试

导入依赖

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

配置连接

源码分析

//maven的autoconfigure包下的spring.factories中找到redis的auto配置类,使用注解”@EnableConfigurationProperties“绑定的文件就是redis的配置文件,通过配置文件我们可以了解到redis哪些参数可配置

//RedisAutoConfiguration  
@Bean
@ConditionalOnMissingBean(
    name = {"redisTemplate"}//我们可以自己定义一个redisTemplate来替换这个默认的
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    //默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的
    //两个泛型都是Object,Object的类型,需要强制转换成<String, Object>
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean//由于string是redis中最常使用的类型,所以官方单独提出一个bean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

配置:

#application.properties文件

#SpringBoot 所有的配置类都有一个自动配置类 RedisAutoConfiguration
#自动配置类都会绑定一个 properties 配置文件 RedisProperties
#maven的autoconfigure包下的spring.factories中找到redis的auto配置类,使用注解”@EnableConfigurationProperties“绑定的文件就是redis的配置文件,通过配置文件我们可以了解到redis哪些参数可配置
#配置redis
spring.redis.host=192.168.23.131
spring.redis.port=6379

测试

@SpringBootTest
class MyTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void test() throws SQLException {
        //redisTemplate 操作不同的数据类型
        //ops,operations,操作
        //opsForValue 操作字符串
        //opsForList 操作List
        //opsForSet
        //opsForHash
        //。。。

        //redisTemplate.opsForValue().set("name","小黑");
        //除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务、基本的CRUD

        //获取redis的连接对象
        //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //connection.flushDb();//清空当前数据库的所有key

        //示例:
        redisTemplate.opsForValue().set("age","18");
        redisTemplate.opsForValue().set("addr","北京朝阳zhaoyan");
        System.out.println(redisTemplate.opsForValue().get("age"));
        System.out.println(redisTemplate.opsForValue().get("addr"));
    }
}


//在服务器中,所有命令行查看刚刚创建的key/value
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04addr"
2) "\xac\xed\x00\x05t\x00\x03age"
127.0.0.1:6379> get addr
(nil)
127.0.0.1:6379> get age
(nil)

问题

问题:出现乱码。

原因是默认序列化配置导致,默认序列化方式是JDK序列化,会对字符串进行转义。要解决这个问题,需要自定义配置类,使用redis提供的序列化实现(有string、json等)。


自定义配置类

自定义配置类,替换默认的redisTemplate。类RedisConfig中,返回bean,名为myRedisTemplate。

package com.qsdbl.nazox_demo.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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;

/**
 * @description 配置 redis
 * @author 轻率的保罗
 * @since 2022/4/12
 */
@Configuration
public class RedisConfig {
    //编写配置类 redisTemplate,参考RedisAutoConfiguration的内部类RedisTemplate
    //bean的标识为方法名 myRedisTemplate
    @Bean
    public RedisTemplate<String, Object> myRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        //为了开发方便,直接使用<String, Object> (Object,Object    更改为 String,Object)
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        //序列化配置:
        //json 序列化
        Jackson2JsonRedisSerializer jsonSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jsonSerializer.setObjectMapper(objectMapper);
        //string 序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        //key采用string的序列化方式,解决jdk序列化造成的乱码问题(key一般都为string类型,采用string序列化方式即可)
        template.setKeySerializer(stringSerializer);
        //hash的key也采用string的序列化方式
        template.setHashKeySerializer(stringSerializer);
        //value使用json序列化方式,方便java对象的保存(将value转化为json字符串)
        template.setValueSerializer(jsonSerializer);
        //hash的value序列化方式同上
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

使用&测试

指定使用我们自定义的myRedisTemplate

@SpringBootTest
class MyTest {

    @Autowired
    @Qualifier("myRedisTemplate")//指定使用我们自定义的配置类
    private RedisTemplate redisTemplate;

    @Test
    public void test() throws SQLException {
      
        redisTemplate.opsForValue().set("age","18");
        redisTemplate.opsForValue().set("addr","北京朝阳zhaoyan");
        System.out.println(redisTemplate.opsForValue().get("age"));
        System.out.println(redisTemplate.opsForValue().get("addr"));
        StoreMat storeMat = new StoreMat();
        storeMat.setMat_name("钢笔");
        storeMat.setMat_price("15");
        redisTemplate.opsForValue().set("mat",storeMat);
        System.out.println(redisTemplate.opsForValue().get("mat"));

    }
}


//运行结果:
18
北京朝阳zhaoyan
StoreMat(mat_code=null, mat_name=钢笔, mat_size=null, mat_unit=null, mat_price=15, mat_desc=null, mat_id=null, type_id=null, typeCode=null, typeName=null, add_userid=null, add_date=null, modify_userid=null, modify_date=null, tenant_id=null, in_num=null, in_money=null, out_num=null, out_money=null, local_code=null)

//redis-cli中查看的key:
127.0.0.1:6379> keys *
1) "age"
2) "mat"
3) "addr"

定义工具类

自定义redis工具类,简化redisTemplate的使用。

package com.qsdbl.nazox_demo.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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;

/**
 * @author: 轻率的保罗
 * @since: 2022-04-20 20:13
 * @Description: redis工具类
 */
@Component
public class RedisUtil {
    @Autowired
    @Qualifier("myRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 过期时间(秒)
     * @return true - 成功;false - 失败
     */
    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 键
     * @return 过期时间
     */
    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;
        }
    }

    /**
     * 删除key
     * @param key key名,不定参数(没有或一个或多个)
     */
    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 值
     * @param time 过期时间(秒),time要大于0,如果小于等于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 num 数量(要增加几,大于0)
     * @return 增加后的值
     */
    public long incrby(String key, long num){
        if(num < 0){
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key,num);
    }

    /**
     * 减少指定的数量
     * @param key 键
     * @param num 数量(要减少几,大于0)
     * @return 减少后的值
     */
    public long decrby(String key, long num){
        if(num < 0){
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().decrement(key,num);
    }

    //hash适合用来保存对象:

    /**
     * 获取Hash中的某个键值对
     * @param key 键(hash的名),不能为null
     * @param item 项(该hash中的哪一项,键名),不能为null
     * @return 值
     */
    public Object hget(String key, String item){
        return redisTemplate.opsForHash().get(key,item);
    }

    /**
     * 获取Hash中的所有键值
     * @param key 键(hash的名),不能为null
     * @return 所有键值
     */
    public Map<Object, Object> hmget(String key){
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 创建一个Hash,并往其中添加多个键值对
     * @param key 键(hash的名),不能为null
     * @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;
        }
    }

    /**
     * 创建一个Hash,并往其中添加多个键值对,并设置过期时间
     * @param key 键(hash的名),不能为null
     * @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 键(hash的名)
     * @param item 项(hash中一个键值对的键)
     * @param value 值(hash中一个键值对的值)
     * @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 键(hash的名)
     * @param item 项(hash中一个键值对的键)
     * @param value 值(hash中一个键值对的值)
     * @param time 过期时间(秒,当前所设置的键值对),若已存在则会替换原来的过期时间
     * @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 键(hash的名)
     * @param item 项(可以多个,不能为null)
     */
    public void hdel(String key, Object... item){
        redisTemplate.opsForHash().delete(key,item);
    }

    /**
     * 判断hash中是否有该项(键值对)
     * @param key 键
     * @param item 项
     * @return true - 有;false - 没有
     */
    public boolean hHasKey(String key, String item){
        return redisTemplate.opsForHash().hasKey(key,item);
    }

    /**
     * hash中,某键值对的值 增加 指定的数量,不存在就会创建
     * @param key 键(hash的名)
     * @param item 项(hash中一个键值对的键)
     * @param num 数量(大于0)
     * @return 增加后的值
     */
    public double hincrby(String key, String item, double num){
        return redisTemplate.opsForHash().increment(key,item,num);
    }

    /**
     * hash中,某键值对的值 减少 指定的数量,不存在就会创建
     * @param key 键(hash的名)
     * @param item 项(hash中一个键值对的键)
     * @param num 数量(大于0)
     * @return 减少后的值
     */
    public double hdecrby(String key, String item, double num){
        return redisTemplate.opsForHash().increment(key,item,-num);
    }

    // set集合,无序不重复

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

    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键(set集合的名)
     * @param value 值(set集合的元素)
     * @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 键(set集合的名)
     * @param values 值(set集合的元素。不定参数)
     * @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 键(set集合的名)
     * @param time 过期时间
     * @param values 值(set集合的元素。不定参数)
     * @return 成功的个数
     */
    public long sSet(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 键(set集合的名)
     * @return set的长度
     */
    public long sGetSetSize(String key){
        try {
            return redisTemplate.opsForSet().size(key);
        }catch (Exception e){
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除set中值为value的元素
     * @param key 键(set集合的名)
     * @param values 值(set中的元素)
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values){
        try {
            return redisTemplate.opsForSet().remove(key,values);
        }catch (Exception e){
            e.printStackTrace();
            return 0;
        }
    }

    // list集合,有序可重复。一般当作栈、队列、阻塞队列使用

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

    /**
     * 通过索引 获取list中的值
     * @param key 键(list集合的名)
     * @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 键(list集合的名)
     * @param value 值
     * @return true - 成功;false - 失败
     */
    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 键(list集合的名)
     * @param value 值
     * @param time 过期时间
     * @return true - 成功;false - 失败
     */
    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 键(list集合的名)
     * @param value 值(集合)
     * @return true - 成功;false - 失败
     */
    public boolean lSet(String key, List<Object> value){
        try {
            redisTemplate.opsForList().rightPush(key,value);
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存,并设置过期时间
     * @param key 键(list集合的名)
     * @param value 值(集合)
     * @param time 过期时间
     * @return true - 成功;false - 失败
     */
    public boolean lSet(String key, List<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 键(list集合的名)
     * @param index 索引
     * @param value 值(替换掉旧元素)
     * @return true - 成功;false - 失败
     */
    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 键(list集合的名)
     * @param count 移除的数量
     * @param value 值
     * @return 实际移除的数量
     */
    public long lRemove(String key, long count, Object value){
        try {
            return redisTemplate.opsForList().remove(key,count,value);
        }catch (Exception e){
            e.printStackTrace();
            return 0;
        }
    }

}

使用&测试

@SpringBootTest
class MyTest {

    @Autowired
    private RedisUtil rUtil;

    @Test
    public void test() throws SQLException {
        rUtil.set("slogn","永远相信美好的事情即将发生!");
        System.out.println(rUtil.get("slogn"));
    }
}

//运行结果:
永远相信美好的事情即将发生!


Redis.conf详解

建议:启动redis时使用我们自定义的配置文件(多了解一下redis配置)

# 使用homebrew安装的redis,配置文件为/opt/homebrew/etc/redis.conf。
# 自定义配置文件myredis.conf
cd /opt/homebrew/etc
sudo cp redis.conf myredis.conf
# 编辑配置文件
sudo vim myredis.conf

# 使用自定义的配置文件启动redis服务
redis-server /opt/homebrew/etc/myredis.conf
# 打开redis客户端,连接redis服务器
redis-cli

配置后台启动、关闭protected模式,见前边的笔记

单位

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

# 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.

包含

通用的配置文件,可使用include包含

# include /path/to/local.conf
# include /path/to/other.conf

网络

# 绑定的ip(默认只允许本机连接)
bind 127.0.0.1 ::1
#端口设置
port 6379

通用

# 开启保护模式
protected-mode yes
# 守护线程方式运行
daemonize yes

# 如果以后台的方式(守护线程)运行,就需要指定一个pid文件
pidfile /var/run/redis_6379.pid

# 日志级别:默认notice,适度冗长,生产环境使用
# 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

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

#数据库数量
# Set the number of databases. The default database is DB 0, you can sele     ct
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16

#是否显示logo(启动时)
# ASCII art logo in startup logs by setting the following option to yes.
always-show-logo no

快照

redis是内存数据库,如果不进行持久化操作,那么数据断电就丢失。

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

# Save the DB to disk.
#
# save <seconds> <changes>
#
# Redis will save the DB if both the given number of seconds and the give     n
# number of write operations against the DB occurred.
#
# Snapshotting can be completely disabled with a single empty string argu     ment
# as in following example:
#
# save ""
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 key changed
#   * After 300 seconds (5 minutes) if at least 100 keys changed
#   * After 60 seconds if at least 10000 keys changed
# 上边为reids默认配置,可更改配置,例子如下:
# 如果900s内至少5个key发生了改变,那么就进行持久化操作
save 900 5

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

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

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


# The working directory.
#
# The DB will be written inside this directory, with the filename specifi     ed
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
 454 
 # rdb文件保存的目录
 dir /opt/homebrew/var/db/redis/

安全

可以设置密码,默认没有

# 启动redis客户端,连接redis服务器
qsdbl@macbook etc % redis-cli
127.0.0.1:6379> ping
PONG
# 查看密码
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
# 设置密码
127.0.0.1:6379> config set requirepass "123456"
OK
# 注意:config set requirepass "",即可去掉密码验证
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> exit
# 重新登录,需要完成验证才能访问redis
qsdbl@macbook etc % redis-cli
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
# 输入密码,完成验证
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> 

# 注意:重启失效

限制

限制客户端

# 最大连接数量
maxclients 10000

# redis配置最大的内存容量
maxmemory <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 : 永不过期,返回错误

aof

append only模式,aof

# 默认不开启,默认使用rdb方式持久化。
appendonly no

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

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


Redis持久化

RDB 做全量持久化,AOF 做增量持久化。Redis 4.0 之后推出 RDB-AOF 混合持久化模式作为默认配置来使用:

  • 由于 RDB 是间隔一段时间后才会进行持久化,在此期间内如果 Redis 服务出现问题,则会丢失这一段时间内的数据,因此需要 AOF 来配合使用
  • 在 Redis 重启时,会使用 BGSAVE 命令生成的 RDB 文件来重新构建内容,再使用 AOF 来重新执行近期的写指令,来实现数据的完整恢复

RDB操作

在指定时间间隔内将内存中的数据集快照写入磁盘,即snapshot快照。它恢复时是将快照文件直接读到内存中。

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

rdb保存的文件是dump.rdb,在配置文件中的“快照”部分进行配置:

# 快照文件名
dbfilename dump.rdb

触发机制

触发rdb规则,生成dump.rdb文件

  1. 执行sava命令或触发save规则(配置文件中的save配置,见上边conf详解中的快照
  2. 执行flushall命令
  3. 退出redis(shutdown命令)

如何恢复?

只需要将rdb文件放在redis启动目录下就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据

查看需要存放的位(rdb文件与aof文件等等):

# 通过下边的命令查看 或 查看配置文件(见上边conf详解中的快照)
127.0.0.1:6379> config get dir
1) "dir"
2) "/opt/homebrew/var/db/redis"

小结

优点:

  • 适合大规模的数据恢复(主从复制中,rdb就用在从机上备份数据)
  • 对数据的完整性要求不高

缺点:

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

AOF操作

AOF,append only file

将所有的命令都记录下来,恢复的时候就把这个文件全部再执行一遍。默认文件无限追加。

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


配置

配置文件:

#默认不开启,需要手动配置。只需更改为yes即可(重启生效。默认是RDB-AOF混合持久化模式)
appendonly no

#aof日志文件名
appendfilename "appendonly.aof"

#通过命令config get dir查看日志文件保存位置。homebrew安装的redis,保存该文件的地方应该是:/opt/homebrew/var/db/redis

#默认每秒记录一次日志文件
# appendfsync always
appendfsync everysec
# appendfsync no

#若日志文件受损,redis服务器启动时会报错并提示使用redis-check-aof命令来修复.aof文件
#把有语法错误的操作记录删掉,尽可能保留数据
redis-check-aof --fix appendonly.aof文件(注意路径)

#重写配置
#如果aof文件大于(配置文件中的)64M,就会fork一个新的进程来重写日志文件。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

#RDB-AOF 混合持久化模式,默认开启
#即AOF和RDB同时开启,先加载RDB再加载AOF
aof-use-rdb-preamble yes

重写规则:

  • 为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的。
  • 重复或者无效的操作记录不写入、过期的数据不再写入、多条命令合并

小结

优点:

  1. 使用always模式,每一次修改都同步,文件的完整性会更好
  2. 使用默认的everysec模式,每秒同步一次,可能会丢失一秒的数据
  3. 使用no模式,从不同步,可获得最好的效率

缺点:

  1. 相对于数据文件,aof远远大于rdb,修复的速度也比rdb慢。
  2. aof运行效率也要比rdb慢,所以redis默认的配置就是rddb持久化。

扩展

  1. RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储。
  2. AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
  3. 只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
  4. 性能建议
    • 因为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 Replication(主从复制)实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时挂掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个,微博就是这种架构。


Redis发布订阅

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

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

订阅/发布消息图:消息发送者、频道、消息订阅者

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

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

命令

命令描述
PSUBSCRIBE pattern [pattern …]订阅一个或多个符合给定模式的频道
PUBSUB subcommand [argument [argument …]]查看订阅与发布系统状态
PUBLISH channel message将信息发送到指定的频道
PUNSUBSCRIBE [pattern [pattern …]]退订所有给定模式的频道
SUBSCRIBE channel [channel …]订阅给定的一个或多个频道的信息
UNSUBSCRIBE [channel [channel …]]退订给定的频道

测试

订阅端:

qsdbl@macbook ~ % redis-cli
# 订阅一个频道 qsdbl
127.0.0.1:6379> SUBSCRIBE qsdbl
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "qsdbl"
3) (integer) 1

# 等待读取到推送的消息
1) "message" #消息
2) "qsdbl" #频道
3) "hello" # 发送的内容
1) "message"
2) "qsdbl"
3) "hello world"

发送端:

qsdbl@macbook ~ % redis-cli                                        
# 发布者发布消息到频道 qsdbl
127.0.0.1:6379> PUBLISH qsdbl hello
(integer) 1
127.0.0.1:6379> publish qsdbl "hello world"
(integer) 1

原理

Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。
Redis 通过 PUBLISH、 SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。
通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

使用场景

1、实时消息系统

2、实时聊天(频道当作聊天室,将消息回显给所有人)

3、订阅、关注系统都可以

稍微复杂的场景可以使用消息中间件来实现。


Redis主从复制

概念

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

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


作用

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

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

  • 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力过大
  • 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256GB,也不能将所有内存作为Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20GB。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是“多读少写”。对于这种场景,可以使用下边的架构:

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


配置

只配置从库,不配置主库。默认情况下,每台Redis服务器都是主节点,只需要配置从机即可。

# 查看当前库的信息(replication - 复制)
127.0.0.1:6379> info replication
# Replication
role:master #角色 master - 主机
connected_slaves:0 #没有从机
master_failover_state:no-failover

# 命令:
#(在从机中进行如下配置)指认主机
> slaveof 地址 端口号

使用命令的方式配置主从机,是临时的,重启失效(恢复为主机)。在配置文件中进行配置才是永久生效。

# 配置文件:
# replicaof <masterip> <masterport>
 
# replicaof 主机地址 端口,redis-server启动后就自动生效

案例

  • 本地模拟集群
  • 使用多个配置文件,启动多个redis-server(每个redis服务都是相互独立的)
  • 运行端口、pid文件名、日志文件名、rdb快照文件名等配置不同
从机

只需要配置从机,使用命令slaveof指认主机即可。

-- 配置文件为 myredis02.conf ,运行在6380端口
-- 使用以下命令启动:redis-server /opt/homebrew/etc/myredis02.conf

qsdbl@macbook homebrew % redis-cli -p 6380
127.0.0.1:6380> ping
PONG
# 查看本机信息
127.0.0.1:6380> info replication
# Replication
role:master #角色默认为“主机”
connected_slaves:0
master_failover_state:no-failover
master_replid:8a7d6685effb30244d55aed169f767689df7f61a
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

# 指认主机(将本机配置为从机)
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK

# 查看本机信息
127.0.0.1:6380> info replication
# Replication
role:slave #角色为“从机”,说明配置成功
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_read_repl_offset:14
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:0c8a6ae83e787193fbbc161150f92a2d47f126a3
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
主机

不需要配置。

-- 配置文件为 myredis.conf ,运行在6379端口
-- 使用以下命令启动:redis-server /opt/homebrew/etc/myredis.conf

qsdbl@macbook homebrew % redis-cli -p 6379
127.0.0.1:6379> ping
PONG
# 查看本机信息
127.0.0.1:6379> info replication
# Replication
role:master # 主机
connected_slaves:0 # 当前连接的从机数量
master_failover_state:no-failover
master_replid:8fac5ae804e75ba1fe75db2681e20279e689606b
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

# 查看本机信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 # 当前连接的从机数量
slave0:ip=127.0.0.1,port=6380,state=online,offset=294,lag=0
master_failover_state:no-failover
master_replid:0c8a6ae83e787193fbbc161150f92a2d47f126a3
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:294
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:294

注意

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

# 从机
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica.

主机断开连接(宕机、网络原因等),依然可以从“从机”读数据,但是没有写操作。与主机恢复连接后,依然可以获取到主机后边写入的数据。


复制原理
  • slave启动成功连接到master后,会发送一个sync同步命令。master接收到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完成同步。
  • 全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:master继续将新的所有收集到的修改命令依次传给slave,完成同步。
  • 只要是重新连接master,一次完成同步(全量复制)将被自动执行,保证数据一定能在从机中读取到。

扩展

也可以组成链式结构。主 - 从 - 从 - 从

配置命令也是使用前边的“slaveof”,只不过全部指向主机改成链式。

若主机宕机了,要在剩下的从机中选择一个作为主机。可使用命令“slaveof no one”进行配置。

原主机恢复后,不会主动恢复之前的关系,不过可以使用“slaveof”命令配置为从机。


哨兵模式

概念

前边,主从切换需要人工干预,当“主”服务器宕机后,需要手动把一台“从”服务器切换为“主”服务器,费时费力还会造成一段时间内服务不可用。哨兵模式,Redis自2.8开始正式提供Sentinel(哨兵)架构,能够在后台监控主机是否故障,如果发生故障则通过自动选举把其中一台“从”服务器转换为“主”服务器。

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

作用:

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

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。(除了监控各个服务器之外,哨兵之间也会互相监控)

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为“主观下线”。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为“客观下线”。


测试

我们目前的状态是一主二从。

1、配置哨兵配置文件sentinel.conf。vim sentinel.conf

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

# 如果之前是给redis加过密码的话一定要记得在配置文件里加上sentinel auth-pass <name> <password>

后边参数“1”,表示多少个主节点检测到主节点有问题就进行故障转移。其实就是有多少个哨兵(sentinel)认为这个主节点主观下线才算真正的下线,然后进行故障转移,让其客观下线

2、启动哨兵

redis-sentinel sentinel.conf

如果master节点断开了,就会从从机中随机选择一个服务器。

如果主机回来了,只能归并到新的主机下,当作从机。


小结

优点:

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

缺点:

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

全部配置

哨兵模式的全部配置:

# 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> <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缓存穿透和雪崩

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题就是数据的一致性问题,从严格意义上来讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。另外的一些典型问题就是缓存穿透、缓存雪崩、缓存击穿。目前,业界也都有比较流行的解决方案。


缓存穿透(查不到)

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


解决方案1-布隆过滤器

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

解决方案2-缓存空对象

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

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

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

缓存击穿(量太大,缓存过期)

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

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


解决方案1-设置永不过期

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


解决方案2-互斥锁

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


缓存雪崩

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

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

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


解决方案1-redis高可用

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


解决方案2-限流降级

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


解决方案3-数据预热

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



课程学习自:遇见狂神说

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值