Redis一篇从入门到实战

Redis 入门

1、NoSQL概述

什么是NoSQL,NoSQL = Not Only SQL(不仅仅是 SQL)泛指非关系型的数据库

NoSQL 的特点

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

传统 RDBMS 和 NoSQL的比较

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

了解:3V+3高

  • 大数据时代的3V:主要是描述问题的:海量Volume、多样Variety、实时Velocity
  • 大数据时代的3高:主要是对程序的要求:高并发、高可扩、高性能
  • 实际项目:NoSQL+RDBMS 配合使用

NoSQL 的四大分类

1、键值对(key-value)存储数据库:

  • 相关产品:Redis、Tokyo Cabinet/Tyrant、Voldemort、Berkeley Db 等
  • 典型应用:内存缓存,主要用于处理大量数据的高访问负载
  • 数据模型:一系列键值对
  • 优势:快速查询
  • 劣势:存储的数据缺少结构化

2、文档型数据库(bson 和 json 一样):

  • 相关产品:MongoDB、ConthDB
  • 典型应用:web应用(与key-value类似,value是结构化的)
  • 数据模型:一系列键值对
  • 优势:数据结构要求不严格
  • 劣势

MongoDB:

  1. MongoDB 是一个基于分布式文件储存的数据库,使用 C++编写,主要用来处理大量的文档。
  2. MongoDB 是一个介于关系型数据库和非关系型数据中间的产品!MongoDB 是非关系型数据中功能最丰富,最像关系型数据库

3、列存储数据库:

  • 相关产品:Hbase、Riak、Cassandra
  • 典型应用:分布式的文件系统
  • 数据模型:以列簇式存储,将同一列数据存在一起
  • 优势:查找速度快,可扩展性强,更容易进行分布式扩展
  • 劣势:功能相对局限

4、图关系数据库:

  • 相关数据库:Neo4J、InfoGrid、Infinite、Graph

  • 典型应用:社交网络

  • 数据模型:图结构

  • 优势:利用图结构先关算法

  • 劣势:需要对整个图做计算才能得出结果,不容易做分布式的集群方案。

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

2、Redis 概述

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

是一个开源的使用 ANSI C语言编写、支持网络、可基于内存亦可持久刷的日志型、key-value 数据库,并提供多种语言的 API。免费和开源!是当下最热门的NoSQL 技术之一,也被人们称之为机构化数据库。

Redis 的作用

  • 内存存储、持久化(RDBAOF
  • 效率高,可以用于高速缓存
  • 发布订阅系统
  • 地图信息分析
  • 计数器、计时器(比如浏览量)

Redis 的特征:多样化的数据类型、持久化、集群、事务

Redis 应用场景:

  • 内存数据库(登录信息、购物车信息、用户浏览记录等)
  • 缓存服务器(商品数据、广告数据等等)(最多使用)
  • 解决分布式集群架构中的Session分离问题(Session共享)
  • 任务队列(秒杀、抢购、12306等等)
  • 支持发布订阅的消息模式
  • 应用排行榜
  • 网站访问统计
  • 数据过期处理(可以精确到毫秒)

官网:

3、Windows安装

1、下载安装包:https://github.com/tporadowski/redis/releases

2、下载完毕得到压缩包解压:Redis-x64-5.0.9.zip

3、开启Redis:双击redis-server.exe运行服务,

[24304] 29 Aug 11:32:40.572 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
[24304] 29 Aug 11:32:40.572 # Redis version=5.0.9, bits=64, commit=9414ab9b, modified=0, pid=24304, just started
[24304] 29 Aug 11:32:40.575 # Warning: no config file specified, using the default config. In order to specify a config file use d:environment
edis
edis-x64-5.0.9
edis-server.exe /path/to/redis.conf
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 5.0.9 (9414ab9b/0) 64 bit
  .-`` .-```.  ```/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 24304
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

[24304] 29 Aug 11:32:40.581 # Server initialized
[24304] 29 Aug 11:32:40.581 * Ready to accept connections

4、使用Redis客户端连接:点击redis-cli.exe,输入ping,出现如图PONG结果即连接成功

127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

5、常用的redis服务命令

  • 安装服务:redis-server --service-install redis.windows-service.conf --loglevel verbose

  • 卸载服务:redis-server --service-uninstall

  • 开启服务:redis-server --service-start

  • 停止服务:redis-server --service-stop

  • 重命名服务:redis-server --service-name rename

# 安装服务:
# redis.windows-service.conf 为启动的配置文件
# --loglevel verbose表示记录日志等级
redis-server --service-install redis.windows-service.conf --loglevel verbose
redis-server --service-install --service-name Redis6 redis.windows-service.conf --loglevel verbose

# 开启服务:
redis-server --service-start
redis-server --service-start --service-name Redis6
# 停止服务:
redis-server --service-stop
redis-server --service-stop --service-name Redis6
# 卸载服务:
redis-server --service-uninstall
redis-server --service-uninstall --service-name Redis6

# 以下将会安装并启动三个不同的Redis实例作服务:
redis-server –service-install --service-name redisService1 --port 10001
redis-server –service-install --service-name redisService2 --port 10002
redis-server –service-install --service-name redisService3 --port 10003

redis-server –service-start --service-name redisService1
redis-server –service-start --service-name redisService2
redis-server –service-start --service-name redisService3

4、Linux安装

1、下载安装包并将压缩包上传到服务器:redis 6.0.6.tar.gz

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

# 下载redis-6.0.6.tar.gz安装包
wget http://download.redis.io/releases/redis-6.0.6.tar.gz

# 移动到/opt目录下
mv redis-6.0.6.tar.gz /opt
# 解压Redis压缩包
tar -zxvf /opt/redis-6.0.6.tar.gz

###### 或者执行下面命令 #######
tar -zxvf redis-6.0.6.tar.gz -C /opt/

3、进入Redis目录:查看解压后的文件,可以看到Redis的配置文件redis.conf

[root@liusx redis-6.0.6]# ls
00-RELEASENOTES  COPYING  Makefile   redis.conf       runtest-moduleapi  src     utils
BUGS             deps     MANIFESTO  runtest          runtest-sentinel   tests
CONTRIBUTING     INSTALL  README.md  runtest-cluster  sentinel.conf      TLS.md

4、基本的环境安装

# 增加环境支持
[root@liusx redis-6.0.6]# yum install gcc-c++ 
# 安装
[root@liusx redis-6.0.6]# make && make install

5、Redis默认安装路径为:/usr/local/bin

[root@liusx bin]# ll /usr/local/bin
-rwxr-xr-x 1 root root  4739840 Aug 29 12:15 redis-benchmark
-rwxr-xr-x 1 root root  9653568 Aug 29 12:15 redis-check-aof
-rwxr-xr-x 1 root root  9653568 Aug 29 12:15 redis-check-rdb
-rwxr-xr-x 1 root root  5059032 Aug 29 12:15 redis-cli
lrwxrwxrwx 1 root root       12 Aug 29 12:15 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root  9653568 Aug 29 12:15 redis-server

6、进入到该目录然后将Redis的配置文件拷贝到此位置,首先新建一个放配置文件的目录,然后拷贝至该目录下

[root@liusx bin]# cd /usr/local/bin
[root@liusx bin]# mkdir rconfig
[root@liusx bin]# cp /opt/redis-6.0.6/redis.conf rconfig
[root@liusx bin]# cd rconfig/
[root@liusx rconfig]# ls
redis.conf

7、修改Redis配置文件后台启动:

[root@liusx rconfig]# vim /usr/local/bin/rconfig/redis.conf

修改配置项:

  • daemonizeyes
  • 注释掉 bind 127.0.0.1
  • 修改protected-mode no
    • 关闭protected-mode保护模式,此时外部网络可以直接访问
    • 开启protected-mode保护模式,需配置bind ip或者设置访问密码
No. 配置项 描述
1 port 6379 配置Redis运行端口
2 daemonize yes 配置Redis是否为后台运行
3 pidfile /usr/data/redis/run/redis_6379.pid 设置进程保存路径(父目录必须存在)
4 logfile "/usr/data/redis/logs/redis.log" 设置日志保存目录(父目录必须存在)
5 databases16 该Redis支持的数据库个数(0~15)
6 dir /usr/data/redis/dbcache 保存缓存数据文件目录

8、启动Redis服务:在/usr/local/bin目录下,输入

[root@liusx bin]# redis-server rconfig/redis.conf 
17909:C 29 Aug 2020 13:37:00.822 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
17909:C 29 Aug 2020 13:37:00.822 # Redis version=6.0.6, bits=64, commit=00000000, modified=0, pid=17909, just started
17909:C 29 Aug 2020 13:37:00.822 # Configuration loaded

9、使用Redis客户端连接测试

[root@liusx bin]# redis-cli -p 6379 
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set name test
OK
127.0.0.1:6379> get name
"test"
127.0.0.1:6379> keys *
1) "name"

10、查看 Redis 进程:再开一个窗口

[root@liusx ~]# ps -ef|grep redis
root      8179     1  0 08:43 ?        00:00:00 redis-server 127.0.0.1:6379
root      8210  3437  0 08:44 pts/1    00:00:00 redis-cli -p 6379
root      8306  8281  0 08:48 pts/2    00:00:00 grep --color=auto redis

11、关闭和退出Redis服务:

#################### 方式一:################
127.0.0.1:6379> shutdown		# 关闭Redis 
not connected> exit				# 退出
[root@liusx bin]# ps -ef|grep redis
root      8330  3437  0 08:49 pts/1    00:00:00 grep --color=auto redis

#################### 方式二:################
[root@liusx bin]# ./redis-cli shutdown

#################### 方式三:################
[root@liusx bin]# ps -ef|grep redis
[root@liusx bin]# kill -9 PID

备注:

启动Redis服务进程: /usr/local/bin/redis-server /usr/local/bin/rconf/redis.conf
查询Redis进程是否启动,进行端口查看: netstat -nptl
连接本机Redis数据库: /usr/local/bin/redis-cli
连接远程Redis数据库: /usr/local/bin/redis-cli-h 127.0.0.1 -p 6379
干掉所有的Redis服务 killall redis-server
Redis性能测试工具: /usr/local/bin/redis-benchmark -n 10000-d 50 -c 2000

其他命令说明:

redis-server :启动redis服务
redis-cli :进入redis命令客户端
redis-benchmark: 性能测试的工具
redis-check-aof : aof文件进行检查的工具
redis-check-dump :  rdb文件进行检查的工具
redis-sentinel :  启动哨兵监控服务

5、Redis 性能测试

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 -k 1=keep alive 0=reconnect 1
8 -r SET/GET/INCR 使用随机 key, SADD 使用随机值
9 -P 通过管道传输 请求 1
10 -q 强制退出 redis。仅显示 query/sec 值
11 --csv 以 CSV 格式输出
12 -l 生成循环,永久执行测试
13 -t 仅运行以逗号分隔的测试命令列表。
14 -I Idle 模式。仅打开 N 个 idle 连接并等待。

使用方式:

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

分析举例:

====== SET ======
  100000 requests completed in 1.97 seconds		  # 对 10万个请求进行写入测试
  100 parallel clients							# 100 个并发客户端 
  3 bytes payload								# 每次写入 3 个字节
  keep alive: 1									# 只有一台服务器来处理这些请求,即单机性能

19.56% <= 1 milliseconds
98.70% <= 2 milliseconds
99.82% <= 3 milliseconds
99.85% <= 9 milliseconds
99.94% <= 10 milliseconds
99.95% <= 126 milliseconds
99.96% <= 127 milliseconds
100.00% <= 127 milliseconds					# 所有请求在127 毫秒处理完成
50761.42 requests per second				# 每秒处理 50761.42 个请求

6、基本操作

Redis 默认有 16 个数据库,这个可以在配置文件中查看。默认使用的是第 0 个。

[root@liusx bin]# vim rconfig/redis.conf 

使用 select 命令切换数据库,使用 dbsize查看数据库大小:

[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> select 3		# 切换数据库
OK
127.0.0.1:6379[3]> dbsize		# 查看数据库大小
(integer) 0
127.0.0.1:6379[3]> set name liusx
OK
127.0.0.1:6379[3]> dbsize
(integer) 1
127.0.0.1:6379[3]> 
  • 查看数据库中所有的keykeys *

  • 清除当前数据库:flushdb

  • 清除所有数据库:flushall

127.0.0.1:6379[3]> keys *
1) "name"

127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty list or set)

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)

7、Redis单线程快(面试)

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

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

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

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

  • 误区2:多线程一定比单线程的效率高?

要了解在执行:效率上 CPU > 内存 > 硬盘

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

Redis 采用网络IO多路复用技术R来保证在多连接的时候, 系统的高吞吐量。 参考文章

Redis 8种数据类型

官网可查看命令http://www.redis.cn/commands.html

Redis 五大数据类型

0、通用命令

  • 查询所有的 key:keys *
  • 判断key 是否存在:exists name
  • 设置key的过期时间,单位是秒:expire name 10
  • 查看当前key的剩余过期时间:ttl name
  • 查看当前key的类型:type age
127.0.0.1:6379> keys *  	# 查询所有的key
(empty list or set)
127.0.0.1:6379> set name xxx
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) "age"
2) "name"
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
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name yyy
OK
127.0.0.1:6379> expire name 10  # 设置key的过期时间,单位是秒
(integer) 1
127.0.0.1:6379> ttl name		# 查看当前key的剩余过期时间
(integer) 7
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> type age		# 查看当前key的类型
string
127.0.0.1:6379> 

Redis 有以下 5 种基本的数据类型

1、String(字符串)

127.0.0.1:6379> set key1 v1			# 设置值
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> append key1 "hello"		# 追加值,如果不存在,相当于 set key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1		# 获取字符串长度
(integer) 7
127.0.0.1:6379> 

自增、自减

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views		# 自增 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> decr views       # 自减 1
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> incrby views 10		# 设置步长、自增 10 
(integer) 9
127.0.0.1:6379> decrby views 5      # 设置步长、自减 5
(integer) 4

字符串范围

127.0.0.1:6379> set key1 "hello,world!"
OK
127.0.0.1:6379> get key1
"hello,world!"
127.0.0.1:6379> getrange key1 0 3		# 截取字符串[0, 3]
"hell"
127.0.0.1:6379> getrange key1 0 -1		# 获取全部的字符串,和 get key一样
"hello,world!"
127.0.0.1:6379>

替换字符串范围

127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> setrange key2 1 xx		# 替换字符,从下标1开始替换xx 2个字符
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"
127.0.0.1:6379> 

setnx(set if not exist):不存在再设置(在分布式锁中会经常使用)

setex(set with expire):设置过期时间

ttl key :查看当前剩余有效时间

  • 如果该内容已经消失则返回 -2,
  • 如果没有消失,则返回剩余的时间

persist key:让该key取消有效时间(在有效期内),这个时候如果使用 ttl 命令查看则返回的结果是“ -1 ”;

127.0.0.1:6379> setex key3 30 "hello"		# 设置 30 秒后过期
OK
127.0.0.1:6379> ttl key3					# 剩余过期时间
(integer) 25
127.0.0.1:6379> setnx mykey "redis"			# mykey 不存在时设置成功
(integer) 1
127.0.0.1:6379> keys *
1) "key2"
2) "key1"
3) "views"
4) "mykey"
127.0.0.1:6379> setnx mykey "mongoDB"		# mykey 存在时设置失败
(integer) 0
127.0.0.1:6379> get mykey					# mykey 值不变
"redis"
127.0.0.1:6379> 

msetmget:设置多个key。

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3		# 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
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 k4
(nil)
127.0.0.1:6379>

对象

# 设置一个 user:1 对象 值为 json  字符来保存一个对象
set user:1 {name:zhangsan, age:3}     

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"
127.0.0.1:6379>

getset:先 get 再 set

127.0.0.1:6379> getset db redis		# 如果不存在值,则返回 nil
(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 除了是字符串以外还可以是数字

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存存储
名称 命令 范例: 程序结果:
设置操作数据: set key value set msg lee-hello OK
查询数据: get key get msg "lee-hello"
不覆盖设置: setnx key value setnx msg lee-hello (integer)0--false
(integer)1--true
设置数据有效期: setex key Time value **setex code 10 adc **
setpx code 10 abc
OK
设置多个key: mset key1 value1 key2 value2 … mset k1xiaol k2 xiaoz OK
不覆盖设置多个key: msetnx key1 value1 key2 value2 … msetnx k1 xiaol k2 xiaoz (integer)0--false
(integer)1--true
追加内容: append key 追加内容 append msg -appendMsg (integer)19--字符长度
取得数据长度: strlen key strlen (integer)19--字符长度
删除指定数据: del key key key del name1 name2 (integer)2
查询所有key keys * keys * 或 keys info-* 1)msg
2)code
清除当前库全数据 flushdb flushdb OK
清除所有库全数据 flushall flushall OK
切换索引数据库 select 0-16 select 0 或 select 1 OK

数字操作:

描述 操作语法 返回结果
设置一个基本类型 set mldn-pid 100 OK
自增1处理,随后会返回当前增长后的数据内容 incr mldn-pid (integer)101
默认增长为1,现在要求增长为200: incrby mldn-pid 200 (integer)301
自减1处理 decr mldn-pid (integer)300
自减指定的数据: decrby mldn-pid 100 (integer)200
设置一个Hash类型 hset mldn pid 100 (integer)1
进行Hash数据类型的数字操作(Hash制有hincrby一种操作) hincrby mldn pid -1 (integer)99

2、Hash(哈希)

也是 key - value 形式的,但是value 是一个map。

127.0.0.1:6379> hset myhash field xxx		# set 一个 key-value
(integer) 1
127.0.0.1:6379> hget myhash field			# 获取一个字段值
"xxx"
127.0.0.1:6379> hmset myhash field1 hello field2 world		# set 多个 key-value
OK
127.0.0.1:6379> hmget myhash field field1 field2			# 获取多个字段值
1) "xxx"
2) "hello"
3) "world"
127.0.0.1:6379> hgetall myhash				# 获取全部的数据
1) "field"
2) "xxx"
3) "field1"
4) "hello"
5) "field2"
6) "world"

删除key

127.0.0.1:6379> hdel myhash field1		# 删除指定的key,对应的value也就没有了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field"
2) "xxx"
3) "field2"
4) "world"
127.0.0.1:6379> 

获取value,map相关信息

127.0.0.1:6379> hlen myhash		# 获取长度
(integer) 2
127.0.0.1:6379> hexists myhash field1   # 判断指定key是否存在
(integer) 0
127.0.0.1:6379> hexists myhash field2
(integer) 1
127.0.0.1:6379> hkeys myhash		# 获取所有的key
1) "field"
2) "field2"
127.0.0.1:6379> hvals myhash		# 获取所有的value
1) "xxx"
2) "world"
127.0.0.1:6379> 

hash数字自增操作

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>

Hash 适合存储经常变动的对象信息,String 更适合于存储字符串。

名称 命令 范例: 程序结果:
存放hash数据: hset 对象key 属性key 内容 hset cat name bc OK
取得hash数据: hget 对象key key hget cat name "bc"
不覆盖设置: hsetnx 对象key key value hsetnx cat name bc (integer)0--false****(integer)1--true
批量设置: hmset 对象key key1 value1 key2 value2 … hmset cat name bc age 18 OK
获取对象的多个key的值: hmget 对象key key1 key2 hmget cat name age 1)"bc"****2)"18"
判断某个数据是否存在: hexists 对象key key hexists cat name (integer)1
取得全部内容数量: hlen 对象key hlen cat (integer)2
删除指定key信息: hdel 对象key key1 kye2 hdel cat name age (integer)2
取得所有key: hkeys 对象key hkeys cat 1)"name"****2)"age"
取得hash中所有内容: hvals 对象key hvals cat 1)"bc"****2)"18"
获得全部的key与vaue: hgetall 对象key hgetall cat 1)"name"2)"bc"3)"age"****4)"18"

3、List(列表)

基本的数据类型,列表。

在 Redis 中可以把 list 用作栈、队列、阻塞队列。

list 命令多数以 l开头。

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			# 查看全部元素
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"
127.0.0.1:6379> 

弹出 pop

127.0.0.1:6379> lrange list 0 -1
1) "!"
2) "world"
3) "world"
4) "hello"
127.0.0.1:6379> lpop list		# 移除list的第一个元素
"!"
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "world"
3) "hello"
127.0.0.1:6379> rpop list			# 移除list的第一个元素
"hello"
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "world"
127.0.0.1:6379> 

索引 Lindex

127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "world"
3) "world"
127.0.0.1:6379> lindex list 1		# 通过下标获取list中的某一个值
"world"
127.0.0.1:6379> lindex list 0
"hjk"
127.0.0.1:6379> 

Llen 长度:

127.0.0.1:6379> llen list
(integer) 3
127.0.0.1:6379>

移除指定的值:

127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "world"
3) "world"
127.0.0.1:6379> lrem list 1 world		# 移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "world"
127.0.0.1:6379> lpush list hjk
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "hjk"
3) "world"
127.0.0.1:6379> lrem list 2 hjk
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "world"
127.0.0.1:6379> 

trim 截断:

127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> ltrim mylist 1 2 # 通过下标截取指定长度,这个list已经被破坏了,截断之后只剩下截断后的元素
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello3"
127.0.0.1:6379> 

rpoplpush :移除列表的最后一个元素,将他移动到新的列表中。

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

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> lset list 1 other		# 如果不存在的话,更新会报错
(error) ERR index out of range
127.0.0.1:6379> 

linsert:将某个具体的value插入到列表中某个元素的前面或者后面:

127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
127.0.0.1:6379> linsert mylist before "hello2" hello
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello"
3) "hello2"
127.0.0.1:6379> linsert mylist after "hello2" hello
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello"
3) "hello2"
4) "hello"
127.0.0.1:6379> 

小结

  • list 实际上是一个链表,前后都可以插入
  • 如果key不存在,创建新的链表
  • 如果移除了所有的值,空链表,也代表不存在
  • 在两边插入或者改动值,效率最高。
名称 命令 案例 结果
向队列左边存放数据(头部): lpush 集合key 内容 ... lpush mldn-list a b (integer)2
向队列右边存在数据(尾部): rpush 集合key 内容 ... rpush mldn-list 1 2 (integer)4
取得指定索引位置的内容: lrange 集合key start stop lrange mldn-list 0 -1****(start =0,stop=-1为全部数据) 1)”b“2)”a“3)"1“****4)"2"
在指定元素前追加内容: linsert 集合key before 内容1 增加内容 linsert mldn-list before 1 0 (integer)5
在指定元素后追加内容: linsert 集合key after 内容1 增加内容 linsert mldn-list after 1 0 (integer)6
修改指定索引的内容: lset 集合key 索引 内容 lset mldn-list 0 bb OK
删除数据(从左边删起): lrem 集合key 重复个数 删除内容 lrem mldn-list 2 0 (integer)2
保留指定key的值范围内的数据: ltrim 集合Key 开始索引 结束索引 trim mldn-list 1 3 OK
从指定集合的头部删除元素,并返回删除元素: lpop 集合key lpop mldn-list b
从指定集合的尾部删除元素,并返回删除元素: rpop 集合key rpop mldn-list 2
将移除的元素添加到指定的集合中,返回参照rpop: rpoplpush 移除集合Key 接收集合Key rpoplpush mldn-list new-list 2
取得元素指定索引的内容: lindex 集合key 索引 lindex mldn-list 0 b
返回集合中的元素个数: llen 集合Key llen mldn-list (integer)4

4、Set (集合)

127.0.0.1:6379> sadd myset "hello"		# set 集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset "world"		# set 集合中添加元素
(integer) 1
127.0.0.1:6379> smembers myset		    # 查看指定Set的所有值
1) "world"
2) "hello"
127.0.0.1:6379> sismember myset hello	# 判断某一个值是不是在set中
(integer) 1
127.0.0.1:6379> sismember myset hello1	# 判断某一个值是不是在set中
(integer) 0
127.0.0.1:6379>
127.0.0.1:6379> scard myset				# 获取集合中的个数
(integer) 2
127.0.0.1:6379> sadd myset "hello2"		# set 集合中添加元素
(integer) 1
127.0.0.1:6379> smembers myset   		# 查看指定Set的所有值
1) "world"
2) "hello2"
3) "hello"
127.0.0.1:6379> srem myset hello   		# 移除元素
(integer) 1
127.0.0.1:6379> smembers myset			# 查看指定Set的所有值
1) "world"
2) "hello2"
127.0.0.1:6379> 
127.0.0.1:6379> smembers myset			# 查看指定Set的所有值
1) "kkk"
2) "world"
3) "hjk"
4) "hello2"
127.0.0.1:6379> srandmember myset		# 随机抽取一个元素
"hjk"
127.0.0.1:6379> srandmember myset		# 随机抽取一个元素
"hello2"
127.0.0.1:6379> srandmember myset 2		# 随机抽取指定个数的元素
1) "world"
2) "hello2"
127.0.0.1:6379> srandmember myset 2		# 随机抽取指定个数的元素
1) "hello2"
2) "hjk"
127.0.0.1:6379> 
127.0.0.1:6379> smembers myset		# 查看指定Set的所有值
1) "kkk"
2) "world"
3) "hjk"
4) "hello2"
127.0.0.1:6379> spop myset			# 随机删除元素
"hjk"
127.0.0.1:6379> smembers myset		# 查看指定Set的所有值
1) "kkk"
2) "world"
3) "hello2"
127.0.0.1:6379> spop myset			# 随机删除元素
"hello2"
127.0.0.1:6379> smembers myset		# 查看指定Set的所有值
1) "kkk"
2) "world"
127.0.0.1:6379> 
127.0.0.1:6379> smembers myset			# 查看指定Set的所有值
1) "kkk"
2) "world"
127.0.0.1:6379> sadd myset2 set2
(integer) 1
127.0.0.1:6379> smove myset myset2 "kkk"	# 将一个特定的值,移动到另一个set集合中
(integer) 1
127.0.0.1:6379> smembers myset			# 查看指定Set的所有值
1) "world"
127.0.0.1:6379> smembers myset2			# 查看指定Set的所有值
1) "kkk"
2) "set2"
127.0.0.1:6379> 
127.0.0.1:6379> smembers key1			# 查看key1的所有值
1) "b"
2) "a"
3) "c"
127.0.0.1:6379> smembers key2			# 查看key2的所有值
1) "e"
2) "d"
3) "c"
127.0.0.1:6379> sdiff key1 key2			# 差集
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2        # 交集
1) "c"
127.0.0.1:6379> sunion key1 key2		# 并集
1) "e"
2) "a"
3) "c"
4) "d"
5) "b"
名称 命令 案例 结果
向集合添加元素 sadd 集合key 内容 ... sadd mldn-set 1 2 3 4 1 2 (integer)6
查询set集合(无序排列,无重复) smembers 集合key smembers mldn-set 1)22)3....
删除集合元素 srem 集合key 内容 srem mldn-set (integer)1
从集合中随机弹出一个元素,返回删除元素 spop 集合key spop mldn-set 1
从集合中随机弹出一个元素,但是不删除元素 srandmember 集合key srandmember mldn-set 1
返回两个集合的差集 sdiff 集合key1 集合key2 sdiff mldn-set mldn-set2
返回两个集合的交集 sinter 集合key1 集合key2 sinter mldn-set mldn-set2
返回两个集合的并集 sunion 集合key1 集合key2 sunion mldn-set mldn-set2
将差集保存到新的集合之中 sdiffstore 存储集合 集合key1 集合key2 sdiffstore new-set mldn-set mldn-set2
将交集保存到新的集合之中 sinterstore 存储集合 集合Key1 集合Key2 sinterstore new-set mldn-set mldn-set2
将并集保存到新的集合之中 sunionstore 存储集合 集合key1 集合key2 sunionstore new-set mldn-set mldn-set2
从第一个key对应的set中移除并添加到外一个集合之中 smove 集合key1 集合key2 第一个集合的内容 smove mldn-set mldn-set2 6 (integer)1
返回名称为key的集合个数 scard 集合key scard mldn-set (integer)4
测试member是否是名称为key的set的元素 sismember 集合key 内容 sismember mldn-set 9 (integer)0

5、Zset (有序集合)

追加和获取有序集合数据:

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> 

实现排序:

127.0.0.1:6379> zadd salary 2500 xiaohong		# 添加一个值
(integer) 1
127.0.0.1:6379> zadd salary 5000 xiaoming		# 添加一个值
(integer) 1
127.0.0.1:6379> zadd salary 500 xaiozhang		# 添加一个值
(integer) 1
127.0.0.1:6379> zrange salary 0 -1				# 获取集合数据
1) "xaiozhang"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrangebyscore salary -inf +inf	# 从小到大显示全部的用户
1) "xaiozhang"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrevrange salary 0 -1			# 从大到小进行排序
1) "xiaoming"
2) "xiaohong"
3) "xaiozhang"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores   # 附带成绩的显示所有用户
1) "xaiozhang"
2) "500"
3) "xiaohong"
4) "2500"
5) "xiaoming"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores			# 显示工资小于 2500 的用户
1) "xaiozhang"
2) "500"
3) "xiaohong"
4) "2500"
127.0.0.1:6379> zrange salary 0 -1		# 获取集合数据
1) "xaiozhang"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrem salary xiaohong  	# 移除特定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xaiozhang"
2) "xiaoming"
127.0.0.1:6379> zcard salary			# 获取有序集合的个数
(integer) 2
127.0.0.1:6379> 
127.0.0.1:6379> zadd myset 1 hello		# 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 x	# 添加两个值
(integer) 2
127.0.0.1:6379> zcount myset 1 3		# 获取指定区间的人员数量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
名称 命令 案例
追加有序集合数据: zadd 集合Key 分数 内容 zadd user-mldn 1 pid-1
zadd user-mldn 1 pid-2
zadd user-mldn 1 pid-3....到pid-7
取出有序集合内容: zrange 集合Key 0 -1 withscores 显示所有的数据的元素:grange user-mldn 0 -1
显示每个元素和其对应的分数:grange user-mldn 0 -1 withscores
也可以输出指定范围的数据:grange user-mldn 0 2 withscores
删除集合数据: zrem 集合Key 内容 zrem user-mldn pid-1
数据增长(针对于顺序增加): zincrby 集合Key 分数内容 zincrby user-mldn 5 pid-1
返回集合中指定元素的索引数值: zrank 集合Key 内容 zrank user-mldn pid-1
反转数据: zrevrank 集合Key 内容 zrevrank user-mldn pid-1
反转后取得数据: zrevrange 集合 开始分数 结束分数 withscores zrevrange user-mldn 0 -1 withscores
根据索引取得指定范围的数据: zrangebyscore 集合 开始分数 猜束分数 withscores 闭区间处理:zrangebyscore user-mldn 3 6 withscores (包含了3和6)
开区间处理:zrangebyscore user-mldn (3 (6 withscores
设置一个范围:zrangebyscore user-mldn 1 10 withscores limi 0 2
取得集合中指定分数范围的数量: zcount 集合Key 开始分数 结束分数 zcount user-mldn 2 8
取得指定集合中的元素个数: zcard 集合Key zcard user-mldn
根据下标排序,删除指定范围中的数据 zremrangebyrank 集合Key 索引开始 结束索引 zremrangebyrank user-mldn 0 5

Redis 三种特殊数据类型

6、geospatial

Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离。

文档: https://www.redis.net.cn/order/3687.html

借助网站模拟一些数据: http://www.jsons.cn/lngcode/

geoadd 添加地理位置

规则:两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。

有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。

127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin 
(error) ERR invalid longitude,latitude pair 39.900000,116.400000

添加一些模拟数据:

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
127.0.0.1:6379> 

geopos 获得当前定位坐标值

127.0.0.1:6379> geopos china:city beijing		# 获得指定城市的经纬度
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
127.0.0.1:6379> 

geodist 获取两个位置之间的距离

单位:

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

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

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"
127.0.0.1:6379> 

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

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

获得指定数量的人,200。所有数据应该都录入:china:city ,才会让结果更加请求!

# 以110, 30 这个点为中心,寻找方圆 1000km 的城市
127.0.0.1:6379> georadius china:city 110 30 1000 km 	
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 500 km 
1) "chongqing"
2) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord	#  显示他人的定位信息
1) 1) "chongqing"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) 1) "108.96000176668167114"
      2) "34.25999964418929977"
127.0.0.1:6379> 
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 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"
127.0.0.1:6379> 

GEORADIUSBYMEMBER 找出位于指定元素周围的其他元素

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

geo 底层实现原理其实就是 zset ,可以使用 zset 命令操作 geo

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"

7、hyperloglog

基数:数学上集合的元素个数,是不能重复的。

UV(Unique visitor):是指通过互联网访问、浏览这个网页的自然人。访问的一个电脑客户端为一个访客,一天内同一个访客仅被计算一次。

Redis 2.8.9 版本更新了 hyperloglog 数据结构,是基于基数统计的算法。

hyperloglog 的优点是占用内存小,并且是固定的。存储 2^64 个不同元素的基数,只需要 12 KB 的空间。但是也可能有 0.81% 的错误率。

这个数据结构常用于统计网站的 UV。传统的方式是使用 set 保存用户的ID,然后统计 set 中元素的数量作为判断标准。但是这种方式保存了大量的用户 ID,ID 一般比较长,占空间,还很麻烦。我们的目的是计数,不是保存数据,所以这样做有弊端。但是如果使用 hyperloglog 就比较合适了。

127.0.0.1:6379> pfadd mykey a b c d e f g h i j	# 创建第一组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey					# 统计 mykey 基数
(integer) 10
127.0.0.1:6379> PFADD mykey2 i j z x c v b n m  # 创建第二组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2					# 统计 mykey2 基数
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2		# 合并两组 mykey mykey2 => mykey3
OK
127.0.0.1:6379> PFCOUNT mykey3
(integer) 15
127.0.0.1:6379> 

8、bitmap 位图

bitmap就是通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。一个bit的值,或者是0,或者是1;也就是说一个bit能存储的最多信息是2。

bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等。

这里使用一周打卡的案例说明其用法:

127.0.0.1:6379> setbit sign 0 1		# 周一打卡了
(integer) 0
127.0.0.1:6379> setbit sign 1 0		# 周二未打卡
(integer) 0
127.0.0.1:6379> setbit sign 2 0		# 周三未打卡
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
127.0.0.1:6379> 

查看某一天是否打卡:

127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 0
127.0.0.1:6379> 

统计:统计打卡的天数

127.0.0.1:6379> BITCOUNT sign
(integer) 4
127.0.0.1:6379> 

Redis 事务

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

一次性、顺序性、排他性的执行一组命令。

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

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

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

Redis 事务的命令:

  • 开启事务:multi
  • 命令入队:正常执行命令
  • 执行事务:exec
  • 撤销事务:discard

1、正常执行事务

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
127.0.0.1:6379> 

2、放弃事务

127.0.0.1:6379> multi		# 开启事务
OK
127.0.0.1:6379> set m1 n1
QUEUED
127.0.0.1:6379> set m2 n2
QUEUED
127.0.0.1:6379> DISCARD		# 放弃事务
OK
127.0.0.1:6379> get m1		# 事务队列中命令都不会被执行
(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> setget k3 v3	# 错误的命令
(error) ERR unknown command `setget`, with args beginning with: `k3`, `v3`, 
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec			# 执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4			# 所有的命令都不会被执行
(nil)
127.0.0.1:6379> 

运行时异常:如果事务中某条命令执行结果报错,其他命令是可以正常执行的,错误命令抛出异常

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> 

3、监视 Watch (面试常问)

悲观锁:很悲观,认为什么时候都会出问题,无论什么都会加锁。影响效率,实际情况一般会使用乐观锁。

乐观锁:很乐观,认为什么时候都不会出现问题,所以不上锁。更新数据的时候回判断一下,在此期间是否修改过监视的数据,也就是获取 version。

首先要了解redis事务中watch的作用,watch命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到exec命令(事务中的命令是在exec之后才执行的,所以在multi命令后可以修改watch监控的键值)。假设我们通过watch命令在事务执行之前监控了多个Keys,倘若在watch之后有任何Key的值发生了变化,exec命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。

所以,需要注意的是watch监控键之后,再去操作这些键,否则watch可能会起不到效果。

Redis 监视测试

正常测试:

127.0.0.1:6379> set money 100		
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money		# 监视 money 对象
OK
127.0.0.1:6379> multi		# 事务正常结束,执行期间,money 没有变动,这个时候就能执行成功了
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
127.0.0.1:6379> 

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

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 10
OK
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> DECRBY out 10
QUEUED
127.0.0.1:6379> exec		# 执行之前,在另外一个线程 B 中修改 money 的值,下面就是执行失败。
(nil)
127.0.0.1:6379> 

B 线程:

[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> set money 30
OK

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

127.0.0.1:6379> UNWATCH		# 事务执行失败,先解锁
OK
127.0.0.1:6379> WATCH money		# 获取最新的值,再次监视。相当于 MySQL 中的 select version
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 1
QUEUED
127.0.0.1:6379> INCRBY out 1
QUEUED
127.0.0.1:6379> exec		# 执行的时候会对比监视的值,如果发生变化会执行失败。
1) (integer) 29
2) (integer) 11
127.0.0.1:6379> 

Redis的Java客户端

Redis的Java客户端很多,官方推荐的有三种:Jedis、Redisson和lettuce。

Jedis:

  • 轻量,简洁,便于集成和改造
  • 支持连接池
  • 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持读写分离,需要自己实现
  • 文档差(真的很差,几乎没有……)

Redisson:

  • 基于Netty实现,采用非阻塞IO,性能高
  • 支持异步请求
  • 支持连接池
  • 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持事务,官方建议以LUA Scripting代替事务
  • 支持在Redis Cluster架构下使用pipelining
  • 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
  • 内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
  • 可以与Spring Session集成,实现基于Redis的会话共享
  • 文档较丰富,有中文文档

Lettuce:

  • 基于Netty框架的事件驱动的通信层
  • 其方法调用是异步的
  • Lettuce的API是线程安全的,
  • 所以可以操作单个Lettuce连接来完成各种操作

参考:

https://www.cnblogs.com/dw3306/p/9520741.html

1、Jedis

Jedis托管在github上,地址:https://github.com/xetorthio/jedis

使用 Java 操作 Redis 。Jedis 是 Redis 官方推荐的Java 链接开发工具,是 Java 操作 Redis 的中间件。

1、导入依赖

<dependencies>
    <!-- jeids -->
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.2.0</version>
    </dependency>

    <!-- fastjson -->
    <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.68</version>
    </dependency>
</dependencies>

2、测试:启动本地 Windows 版本的 Redis

public class TestPing {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379, 10);
        System.out.println(jedis.ping()); // PONG
    }
}

事务

/**
 * 测试事务
 */
public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.flushDB();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "xxx");

        // 开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();

        try {
            multi.set("user1", result);
            multi.set("user2", result);
            int i = 1 / 0; // 模拟异常
            multi.exec(); // 执行事务
        } catch (Exception e) {
            multi.discard(); // 放弃事务
            e.printStackTrace();
        } finally {
            // 正常执行时{"name":"xxx","hello":"world"}   
            System.out.println(jedis.get("user1")); // null
            System.out.println(jedis.get("user1"));
            jedis.close(); // 关闭链接
        }
    }
}

2、lettuce

lettuce 官网地址:https://lettuce.io/

lettuce GitHub项目地址:https://github.com/lettuce-io/lettuce-core

3、redisson

redisson 官网地址:https://redisson.org/

redisson GitHub项目地址:https://github.com/redisson/redisson/wiki/目录

SpinrgBoot整合Redis

1、基本使用

Spring Data 也是和 Spring Boot 齐名的项目。

说明:在 Spring Boot 2.x 之后,原来的 Jedis 被替换为了 lettuce。

Jedis:采用的直连,多线程操作的话是不安全的,如果想要避免不安全的话,使用 Jedis Pool ,更像 BIO 模式。

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

# Spring Boot 所有的配置类,都有一个自动配置类  RedisTemplate
# 自动配置类都会绑定一个 properties 配置文件。  RedisProperties

阅读源码:

20200830123401

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

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

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

3、测试

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class Redis02SpringbootApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void contextLoads() {
        // 除了基本的操作,我们常用的方法都可以直接通过 redisTemplate 操作,比如事务和 CRUD
        redisTemplate.opsForValue().set("name", "xiaoming");
        System.out.println(redisTemplate.opsForValue().get("name")); // xiaoming
    }
}

看一下源码:RedisTemplate.class

// 序列化配置
@Nullable
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
public void afterPropertiesSet() {
    super.afterPropertiesSet();
    boolean defaultUsed = false;
    if (this.defaultSerializer == null) {
        // 默认使用了 JDK 的序列化,会使得字符串转义
        this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
    }

    // ...
}

我们使用 Json 序列化,所以需要自定义配置类

2、序列化

编写一个实体类 User,测试序列化。

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

测试序列化:

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

抛出异常:

Caused by: java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [cn.itzhouq.pojo.User]
	at org.springframework.core.serializer.DefaultSerializer.serialize(DefaultSerializer.java:43)
	at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:63)
	... 35 more

DefaultSerializer requires a Serializable默认的序列化需要实体类实现序列化接口。所以修改 User:

public class User implements Serializable {
    private String name;
    private int age;
}

结果:

User(name=xiaoming, age=3)

结果显示正常,但是控制台还是转义的。

127.0.0.1:6379> keys *
1) "xacxedx00x05tx00x04user"
127.0.0.1:6379>

使用 jackson 的序列化:

@Test
public void test() throws JsonProcessingException {
    // 一般开发中都会使用 json 来传递对象
    User user = new User("xiaoming", 3);
    String jsonUser = new ObjectMapper().writeValueAsString(user);
    redisTemplate.opsForValue().set("user", jsonUser);
    System.out.println(redisTemplate.opsForValue().get("user")); // {"name":"xiaoming","age":3}
}

无论 User 是否实现了 Serializable 接口,控制台结果显示正常,但是客户端中查看还是被转义了。

如果不想使用 JDK 的序列化,可以自己编写 RedisTemplate。

3、自定义RedisTemplate

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;

/**
 * 编写的自己的 RedisTemplate
 */
@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 为了开发方便,一般使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        // 序列化配置
        Jackson2JsonRedisSerializer<Object> 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.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

注入和测试:

@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;

@Test
public void test() throws JsonProcessingException {
    // 一般开发中都会使用 json 来传递对象
    User user = new User("xiaoming", 3);
    String jsonUser = new ObjectMapper().writeValueAsString(user);
    redisTemplate.opsForValue().set("user", jsonUser);
    System.out.println(redisTemplate.opsForValue().get("user")); // {"name":"xiaoming","age":3}
}

客户端中查看:

127.0.0.1:6379> keys *
1) "user"
127.0.0.1:6379>

这个时候的对象就没有被转义。

4、Redis实现Session共享

1、Cookie与Session

  • Cookie是什么? Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息,我们可以看到在服务器写的cookie,会通过响应头Set-Cookie的方式写入到浏览器

  • HTTP协议是无状态的,并非TCP一样进行三次握手,对于一个浏览器发出的多次请求,WEB服务器无法区分是不是来源于同一个浏览器。所以服务器为了区分这个过程会通过一个sessionid来区分请求,而这个sessionid是怎么发送给服务端的呢。cookie相对用户是不可见的,用来保存这个sessionid是最好不过了

2、Redis实现分布式集群配置过程:

  • 引入依赖:
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
@EnableRedisHttpSession:开启redis session缓存
maxInactiveIntervalInSeconds:指定缓存的时间
key为:spring:session:sessions:expires:+‘sessionId’的过期时间  
  • 开启redis-session共享
/**
 * @EnableRedisHttpSession:开启redis session缓存
 * maxInactiveIntervalInSeconds:指定缓存的时间
 * key 为 spring:session:sessions:expires:+‘sessionId’的过期时间
 **/
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 50)
public class RedisConfig {
}
  • 测试
@RestController
public class SessionController {

    @RequestMapping(value = "/setSession", method = RequestMethod.GET)
    public Map<String, Object> setSession (HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        request.getSession().setAttribute("request Url", request.getRequestURL());
        map.put("request Url", request.getRequestURL());
        return map;
    }

    @RequestMapping(value = "/getSession", method = RequestMethod.GET)
    public Object getSession (HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        map.put("sessionIdUrl",request.getSession().getAttribute("request Url"));
        map.put("sessionId", request.getSession().getId());
        return map;
    }
}

Redis 配置文件详解

Redis.conf 详解

# 找到启动时指定的配置文件:
[root@liusx ~]# cd /usr/local/bin
[root@liusx bin]# ll
drwxr-xr-x 2 root root     4096 Aug 29 13:35 rconfig
-rwxr-xr-x 1 root root  4739840 Aug 29 12:15 redis-benchmark
-rwxr-xr-x 1 root root  9653568 Aug 29 12:15 redis-check-aof
-rwxr-xr-x 1 root root  9653568 Aug 29 12:15 redis-check-rdb
-rwxr-xr-x 1 root root  5059032 Aug 29 12:15 redis-cli
lrwxrwxrwx 1 root root       12 Aug 29 12:15 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root  9653568 Aug 29 12:15 redis-server
[root@liusx bin]# cd rconfig/
[root@liusx rconfig]# vim redis.conf 

1、单位

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

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

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.
#
# Notice 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.
#
# include /path/to/local.conf
# include /path/to/other.conf

配置文件可以将多个配置文件合起来使用。好比Spring、Improt, include

3、NETWORK 网络

bind 127.0.0.1		# 绑定的 IP
protected-mode no   # 保护模式
port 6379			# 端口设置

4、GENERAL 通用

daemonize yes		# 以守护进程的方式运行,默认是 no ,我们需要自己开启为 yes
pidfile /var/run/redis_6379.pid		#  如果是后台启动,我们需要指定一个pid 文件

# 日志级别
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice
logfile ""		# 日志文件的位置
databases 16	# 数据库的数量,默认是 16
always-show-logo yes  # 是否总是显示 LOGO

5、快照 SNAPSHOTTING

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

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

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""
# 如果 900s 内,至少有 1 个 key 进行了修改,进行持久化操作
save 900 1

# 如果 300s 内,至少有 10 个 key 进行了修改,进行持久化操作
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes  # 如果持久化出错,是否还要继续工作
rdbcompression yes    # 是否压缩 rdb 文件,需要消耗一些 cpu 资源
rdbchecksum yes # 保存 rdb 文件的时候,进行错误的检查校验
dir ./  # rdb 文件保存的目录

6、SECURITY 安全

可以设置 Redis 的密码,默认是没有密码的。

[root@liusx bin]# redis-cli -p 6379
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> 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"
127.0.0.1:6379> 

7、CLIENTS 限制

################################### CLIENTS ####################################

# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000		# 设置能链接上 redis 的最大客户端数量
# maxmemory <bytes>		# redis 设置最大的内存容量
 maxmemory-policy noeviction  # 内存达到上限之后的处理策略
     - noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
    - allkeys-lru:在所有键中采用lru算法删除键,直到腾出足够内存为止。
    - volatile-lru:在设置了过期时间的键中采用lru算法删除键,直到腾出足够内存为止。
    - allkeys-random:在所有键中采用随机删除键,直到腾出足够内存为止。
    - volatile-random:在设置了过期时间的键中随机删除键,直到腾出足够内存为止。
    - volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。

8、AOF 模式配置

APPEND ONLY 模式 AOF 配置

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

appendfilename "appendonly.aof"		# 持久化的文件的名字
# appendfsync always  				# 每次修改都会 sync 消耗性能
appendfsync everysec  				# 每秒执行一次 sync 可能会丢失这 1s 的数据。
# appendfsync no      				# 不执行 sync 这个时候操作系统自己同步数据,速度最快。

Redis 持久化(面试)

Redis RDB 持久化详解

面试和工作,持久化都是重点。

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

  • RDB(Redis DataBase):(属于全量数据备份,备份的是数据)
  • AOF(Append Only File):(增量持久化备份,备份的是指令)

1、RDB(Redis DataBase)

什么是 RDB(属于全量数据备份,备份的是数据)

20200830123402

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

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

生产环境下,需要对这个文件记性

默认持久化方式是 RDB,一般不需要修改。

rdb 保存的文件是 dump.rdb :

# The filename where to dump the DB
dbfilename dump.rdb

测试1:

首先修改配置文件保存快照的策略

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   save ""

# save 900 1
# save 300 10
# save 60 10000
save 60 5  # 只要 60s 内修改了 5 次 key 就会触发 rdb 操作。

保存配置文件:

127.0.0.1:6379> save
OK
127.0.0.1:6379> 

删除原始的 dump.rdb 文件:

[root@liusx bin]# ls
dump.rdb         jemalloc.sh  rconfig           luajit        mcrypt    redis-benchmark  redis-check-rdb  redis-sentinel
jemalloc-config  jeprof       libmcrypt-config  luajit-2.0.4  mdecrypt  redis-check-aof  redis-cli        redis-server
[root@liusx bin]# rm -rf dump.rdb 
[root@liusx bin]# ls
jemalloc-config  jeprof   libmcrypt-config  luajit-2.0.4  mdecrypt         redis-check-aof  redis-cli       redis-server
jemalloc.sh      rconfig  luajit            mcrypt        redis-benchmark  redis-check-rdb  redis-sentinel
[root@liusx bin]# 

60s 内修改 5 次 key :

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> set k4 v4
OK
127.0.0.1:6379> set k5 v5
OK

dump.rdb 文件再次出现。

[root@liusx bin]# ls
dump.rdb         jemalloc.sh  rconfig           luajit        mcrypt    redis-benchmark  redis-check-rdb  redis-sentinel
jemalloc-config  jeprof       libmcrypt-config  luajit-2.0.4  mdecrypt  redis-check-aof  redis-cli        redis-server
[root@liusx bin]# 

恢复数据:

关闭 Redis 服务和客户端,再次进入时数据被自动恢复:

127.0.0.1:6379> shutdown		# 关闭 Redis 服务
not connected> exit
[root@liusx bin]# ps -ef|grep redis		# redis 已经关闭了
root     25989 23576  0 14:27 pts/1    00:00:00 grep --color=auto redis
[root@liusx bin]# redis-server rconfig/redis.conf 		# 再次开启服务
25994:C 02 May 2020 14:28:01.003 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
25994:C 02 May 2020 14:28:01.003 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=25994, just started
25994:C 02 May 2020 14:28:01.003 # Configuration loaded
[root@liusx bin]# redis-cli -p 6379		# 客户端连接
127.0.0.1:6379> get k2						# 可以直接获取数据,说明k2 被持久化了。
"v2"
127.0.0.1:6379> 

测试2:

删除 dump.rdb 文件

root@liusx bin]# rm -rf dump.rdb 
[root@liusx bin]# ls
jemalloc-config  jeprof      libmcrypt-config  luajit-2.0.4  mdecrypt      
redis-check-aof  redis-cli   redis-server      jemalloc.sh   rconfig    luajit 

在客户端清除所有数据:

127.0.0.1:6379> flushall
OK

再次检验 dump.rdb 文件:

[root@liusx bin]# ls
dump.rdb         jemalloc.sh      rconfig          luajit     mcrypt            redis-benchmark  
redis-check-rdb  redis-sentinel   jemalloc-config  jeprof     libmcrypt-config  luajit-2.0.4  
mdecrypt         redis-check-aof  redis-cli        redis-server 

dump.rdb 文件再次出现。

触发机制

1、save 的规则满足的情况下,会自动触发 rdb 规则

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

3、退出 redis 也会产生 rdb 文件

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

如何恢复 rdb 文件

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

2、查看存放 rdb 文件的位置,在客户端中使用如下命令。

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

RDB 的优缺点

优点:

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

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

缺点:

1、需要一定的时间间隔进行操作,如果 Redis 意外宕机,最后一次修改的数据就没有了

2、fork 进程的时候,会占用一定的空间。

2、AOF(Append Only File)

AOF( append only file )持久化以独立日志的方式记录每次写命令,并在 Redis 重启时在重新执行 AOF 文件中的命令以达到恢复数据的目的。AOF 的主要作用是解决数据持久化的实时性。

Redis AOF持久化详解

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

AOF 保存的是 appendonly.aof 文件。

开启 AOF 模式

将配置文件中默认为 no 的 appendonly 修改为 yes ,重启服务。

appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"

重启后可以看到 AOF 文件(appendonly.aof):

[root@liusx bin]# ls
appendonly.aof    redis-benchmark   redis-cli        redis-server    backup.db        
rconfig           redis-check-aof  redis-sentinel    dump.rdb        redis-check-rdb   

但是文件是空的。使用客户端添加一些数据再次查看:

[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> 

[root@liusx bin]# cat appendonly.aof 
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
*3
$3
set
$2
k3
$2
v3

自动修复 AOF文件

如果手动修改AOF 文件,可能导致 Redis 服务不能启动。比如这里我手动在 AOF 文件的最后一行随便添加一些命令:

set
$2
k3
$2
v3
gjjjjjjjjj

删除 dump.rdb 文件,重启服务:

[root@liusx bin]# rm -rf dump.rdb 
[root@liusx bin]# ls
appendonly.aof    redis-benchmark   redis-cli        redis-server    backup.db        
rconfig           redis-check-aof  redis-sentinel    dump.rdb        redis-check-rdb   
[root@liusx bin]# redis-server rconfig/redis.conf 
13746:C 02 May 2020 16:22:43.345 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
13746:C 02 May 2020 16:22:43.346 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=13746, just started
13746:C 02 May 2020 16:22:43.346 # Configuration loaded
[root@liusx bin]# redis-cli -p 6379
Could not connect to Redis at 127.0.0.1:6379: Connection refused		# 连接失败
not connected>

如果这个 AOF 文件有错位,客户端就不能链接了,需要修复 AOF 文件。Redis 提供了工具 redis-check-aof --fix

[root@liusx bin]# redis-check-aof --fix appendonly.aof 
0x              6e: Expected prefix '*', got: 'g'
AOF analyzed: size=122, ok_up_to=110, diff=12
This will shrink the AOF from 122 bytes, with 12 bytes, to 110 bytes
Continue? [y/N]: y
Successfully truncated AOF
[root@liusx bin]# 

重启服务,再次尝试链接成功。

重写规则说明

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

rdb-del-sync-files no

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

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

AOF 的优缺点

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

优点:

1、每一次修改都同步,文件的完整性更加好

2、每秒同步一次,可能会丢失一秒的数据

3、从不同步,效率最高的

缺点:

1、相对于数据文件来说, AOF 远远大于 RDB ,修复的速度也比 RDB 慢

2、AOF 的运行效率也比 RDB 慢,所以 Redis 默认的配置就是 RDB 持久化。

RDB 和 AOF 对比

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

3、扩展

1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储

2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以Redis 协议追加保存每次写的操作到文件尾部,Redis 还能对 AOF 文件记性后台重写,使得AOF 文件的体积不至于过大

3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化

4、同时开启两种持久化方式

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

5、性能建议

  • 因为 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 客户端可以订阅任意数量的频道。

订阅 / 发布消息图:第一个:消息发送者, 第二个:频道 第三个:消息订阅者!

20200830123403

下图展示了频道 channel1,已经订阅这个频道的三个客户端。

20200830123404

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

命令

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

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

测试

以下实例演示了发布订阅是如何工作的。在我们实例中我们创建了订阅频道名为 redisChat:

redis 127.0.0.1:6379> SUBSCRIBE redisChat		# 订阅一个频道:redisChat

Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
# 等待读取推送的信息

现在,我们先重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。

redis 127.0.0.1:6379> PUBLISH redisChat "Redis is a great caching technique"

(integer) 1

redis 127.0.0.1:6379> PUBLISH redisChat "Learn redis by runoob.com"

(integer) 1

# 订阅者的客户端会显示如下消息
1) "message"
2) "redisChat"
3) "Redis is a great caching technique"
1) "message"
2) "redisChat"
3) "Learn redis by runoob.com"

原理

Redis 是使用 C语言 实现的,通过分析 Redis 源码里的 public.c 文件,了解发布和订阅机制的底层实现,借此加深对 Redis 的理解。Redis 通过 public 、subscribe 和 psubscribe 等命令实现发布和订阅功能。

微信:

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

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

使用场景:

  1. 实时消息系统
  2. 实时聊天
  3. 订阅、关注系统都可以

稍微复杂的场景更多的使用消息中间件 MQ。

主从复制

1、概念

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

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

2、主从复制的作用

1、数据冗余:主从复制实现了数据的热备份,是持久化的之外的一种数据冗余方式。

2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。实际也是一种服务的冗余。

3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 的时候应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个节点分担读负载,可以大大提高 Redis 服务器的并发量。

4、高可用(集群)的基石:除了上述作用以外,主从复制还是哨兵模式和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。

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

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

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

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

对于这种场景,我们可以使用如下这种架构:

20200830123405

主从复制,读写分离!80% 的情况下,都是在进行读操作。这种架构可以减少服务器压力,经常使用实际生产环境中,最少是“一主二从”的配置。真实环境中不可能使用单机 Redis。

3、环境配置

只配置从库,不用配置主库。

[root@liusx bin]# 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_replid:2467dd9bd1c252ce80df280c925187b3417055ad
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> 

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

1、port 端口

2、pid 名称

3、log 文件名称

4、dump.rdb 名称

port 6380
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dbfilename dump6380.rdb
port 6381
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump6381.rdb

修改完毕后,启动我们的 3 个 redis 服务器,可以通过进程信息查询。

[root@liusx ~]# ps -ef|grep redis
root       426     1  0 16:53 ?        00:00:00 redis-server *:6379
root       446     1  0 16:54 ?        00:00:00 redis-server *:6380
root       457     1  0 16:54 ?        00:00:00 redis-server *:6381
root       464   304  0 16:54 pts/3    00:00:00 grep --color=auto redis

4、一主二从

默认情况下,每台 Redis 服务器都是主节点,我们一般情况下,只用配置从机就好了。

主机:6379, 从机:6380 和 6381

配置的方式有两种:

  • 一种是直接使用命令配置,这种方式当 Redis 重启后配置会失效。
  • 另一种方式是使用配置文件。这里使用命令演示一下。

下面将80 和 81 两个配置为在从机。

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379		# SLAVEOF host port 找谁当自己的老大!
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:6
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:907bcdf00c69d361ede43f4f6181004e2148efb7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
127.0.0.1:6380> 

配置好了之后,看主机:

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2		# 主节点下有两个从节点
slave0:ip=127.0.0.1,port=6380,state=online,offset=420,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=420,lag=1
master_replid:907bcdf00c69d361ede43f4f6181004e2148efb7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:420
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:420
127.0.0.1:6379> 

真实的主从配置应该是在配置文件中配置,这样才是永久的。这里使用命令是暂时的。

配置文件 redis.conf

################################# REPLICATION #################################

# Master-Replica replication. Use replicaof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
#   +------------------+      +---------------+
#   |      Master      | ---> |    Replica    |
#   | (receive writes) |      |  (exact copy) |
#   +------------------+      +---------------+
#
# 1) Redis replication is asynchronous, but you can configure a master to
#    stop accepting writes if it appears to be not connected with at least
#    a given number of replicas.
# 2) Redis replicas are able to perform a partial resynchronization with the
#    master if the replication link is lost for a relatively small amount of
#    time. You may want to configure the replication backlog size (see the next
#    sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
#    network partition replicas automatically try to reconnect to masters
#    and resynchronize with them.
#
# replicaof <masterip> <masterport>			# 这里配置
slaveof localhost 6379						# 这里配置

# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the replica to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the replica request.
#
# masterauth <master-password>

配置方式也是一样的。

5、几个问题

1、主机可以写,从机不能写只能读。主机中的所有信息和数据都会保存在从机中。如果从机尝试进行写操作就会报错。

127.0.0.1:6381> get k1			# k1的值是在主机中写入的,从机中可以读取到。
"v1"
127.0.0.1:6381> set k2 v2			# 从机尝试写操作,报错了
(error) READONLY You can't write against a read only replica.
127.0.0.1:6381> 

2、如果主机断开了,从机依然链接到主机,可以进行读操作,但是还是没有写操作。这个时候,主机如果恢复了,从机依然可以直接从主机同步信息。

3、使用命令行配置的主从机,如果从机重启了,就会变回主机。如果再通过命令变回从机的话,立马就可以从主机中获取值。这是复制原理决定的。

6、复制原理

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

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

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

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

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

这种模式的原理图:

第一种模式

20200830123406

第二种模式

20200830123407

这种模式的话,将 6381 的主节点配置为 6380 。主节点 6379 只有一个从机。

如果现在 6379 节点宕机了, 6380 和 6381 节点都是从节点,只能进行读操作,都不会自动变为主节点。需要手动将其中一个变为主节点,使用如下命令:

SLAVEOF no one

哨兵模式

(自动选举老大的模式)

1、概述

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

哨兵模式能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

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

20200830123408

这里的哨兵有两个作用

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

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

20200830123409

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

2、哨兵模式(1主2从)

1、配置哨兵模式配置文件,新建文件 /usr/local/bin/rconfig/sentinel.conf

# sentinel monitor 被监控的名字(随便写) host 1
sentinel monitor myredis 127.0.0.1 1

后面的数字1代表主机宕机后,slave投票决定谁成为新的主机,票数最多成为主机。

2、启动哨兵

[root@liusx bin]# ls
6379.log  6381.log      dump6380.rdb  dump.rdb  redis-benchmark  redis-check-rdb  redis-sentinel
6380.log  dump6379.rdb  dump6381.rdb  rconfig   redis-check-aof  redis-cli        redis-server
[root@liusx bin]# redis-sentinel rconfig/sentinel.conf 
2421:X 15 May 2020 20:24:06.847 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2421:X 15 May 2020 20:24:06.847 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=2421, just started
2421:X 15 May 2020 20:24:06.847 # Configuration loaded
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 2421
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

2421:X 15 May 2020 20:24:06.848 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2421:X 15 May 2020 20:24:06.851 # Sentinel ID is 100430af0018d23bd1ae2fe57e71e0d45f64d9a5
2421:X 15 May 2020 20:24:06.851 # +monitor master myredis 127.0.0.1 6379 quorum 1
2421:X 15 May 2020 20:24:06.852 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379

启动成功~!

如果现在 Master 节点宕机了,这个时候会从从机中根据投票算法选择一个作为主机。

如果原来的主机恢复运行了,只能归到新的主机下,作为从机, 这就是哨兵模式的规则。

哨兵模式的优点

1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有

2、主从可以切换,故障可以转移,系统的可用性就会更好

3、哨兵模式就是主从模式的升级,手动到自动,更加健壮。

哨兵模式的缺点

1、Redis 不方便在线扩容,集群达到一定的上限,在线扩容就会十分麻烦;

2、实现哨兵模式的配置其实也很麻烦,里面有甚多的配置项。

哨兵模式的全部配置(完整的哨兵模式配置文件 sentinel.conf)

# Example sentinel.conf
 
# 哨兵sentinel实例运行的端口 默认26379
port 26379
 
# 哨兵sentinel的工作目录
dir /tmp
 
# 哨兵sentinel监控的redis主节点的 ip port 
# master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在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无法正常启动成功。
#通知脚本
# 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 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带了一些问题。其中,最要害的是问题,就是数据一致性的问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么久不能使用缓存。

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

1、缓存穿透(查不到数据)

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

20200830123410

解决方案:

1、布隆过滤器

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

20200830123411

2、缓存空对象

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

20200830123412

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

1、如果控制能够被缓存起来,这就意味着缓存需要更多的空间存储,因为这当中可能会有很多的空值的键;

2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

2、缓存击穿

缓存击穿(请求量过多、缓存过期)

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

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

解决方案:

1、设置热点数据永不过期

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

2、加互斥锁

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

20200830123413

3、缓存雪崩

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

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

20200830123414

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

解决方案

1、Redis 高可用

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

2、限流降级

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

3、数据预热

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

Redis 面试题

1、什么是 Redis?有什么特点?

Redis 是一款开源,高性能的 key-value 的非关系型数据库。
特点
1)支持持久化,可以将内存中的数据持久化到磁盘,重启可以再次从磁盘中加载使用;
2)支持多种数据结构;
3)支持数据的备份:主从模式的备份;
4)高性能,读速度达 11 万次/秒,写速度达到 8.1 万次/秒
5)支持事务。

2、说说 Redis 的数据类型

一共 8 种:
5 种基本数据类型:String、Hash、List、Set、Zset
3 种特殊类型:geospatial、hyperloglog、bitmap

3、Redis 和 Memcache 的区别?

1)Memcache 数据都存储在内存中,断电即失,数据不能超过内存大小;而 Redis 的数据可以持久化到硬盘。
2) Memcache 只支持简单的字符串,Redis 有丰富的数据结构;
3)底层实现方式不一样,Redis 自行构建了 VM 机制,速度更快。

4、Redis 是单进程单线程的?

Redis 将数据放在内存中,单线程执行最高,多线程执行反而需要进行 CPU 上下文切换,这是个耗时操作,单线程效率最高。

5、说说 Redis的持久化

Redis 提供了两种持久化机制:RDB 和 AOF

RDB 持久化机制指的是,用数据集快照的方式记录 Redis 数据库的所有键值对,在某个时间点写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复的目的。
优点
1)只有一个文件 dump.rdb 方便持久化;
2)容灾性好,一个文件可以保存到安全的磁盘;
3)性能最大化,Redis 会单独创建(fork)一个子进程进行持久化,主进程不进行任何 IO 操作,保证了性能;
4)在数据较多时,比 AOF 的启动效率高。
缺点
最后一次持久化的数据可能会丢失。

AOF 持久化,是以独立日志的方式记录每次写命令,并在 Redis 重启时重新执行 AOF 文件中的命令以达到恢复数据的目的。AOF 主要解决数据持久化的实时性。
优点
1)数据安全,配置 appendfsync 属性,可以选择不同的同步策略;
2)自动修复功能, redis-check-aof工具可以解决数据一致性问题;
缺点
1)AOF 文件比 RDB 文件大,且恢复速度慢;
2)数据多时,效率低于 RDB。

6、Redis 的主从复制

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

主从复制的作用
1)数据冗余:主从复制实现了数据的热备份;
2)故障修复:当主节点出现故障后,从节点还可以提供服务,实现快速的故障修复。
3)负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写操作,从节点提供读操作,实现负载均衡,提高并发量;
4)高可用的基石:主从复制是哨兵模式的基础。

复制原理
从节点启动成功连接到主节点后,会发送一个 sync 的同步命令。主节点接收到命令之后,启动后台的存盘进程,收集所有修改数据库的命令,在后台执行完毕后将整个数据文件传送到从节点,完成一次完全同步。
全量复制:从节点在接收到了数据文件后,将其存盘文件加载都内存中;
增量复制:主节点继续将新收集到修改命令传递给从节点,完成同步。

7、说说哨兵模式

哨兵模式是为了解决手动切换主节点的问题。Redis 提供了哨兵的命令,哨兵是一个独立的进程。哨兵能够后台监控主节点是否故障,如果故障需要将从节点选举为主节点。

其原理是哨兵通过发送命令,等待 Redis 服务器的响应,从而监控多个 Redis 节点。

当只有一个哨兵时,还是可能会出现问题的,比如哨兵自己挂掉。为此,可以使用多哨兵模式,多个哨兵之间相互监控。当主节点宕机了,哨兵1先检测到这个结果,系统并不会马上进行 failover 【故障转移】的过程。仅仅是哨兵1认为主节点不可用的现象称之为主观下线。当其余的哨兵也检测到主节点不可用之后,哨兵之间会进行一次投票选举从节点中的一个作为新的主节点,这个过程称之为客观下线

哨兵模式的优点
1)基于主从复制,高可用;
2)主从可以切换,进行故障转移,系统可用性好;
3)哨兵模式是主从模式的升级版,手动到自动,更加健壮。

哨兵模式的缺点
1)不方便在线扩容;
2)实现哨兵模式需要很多的配置。

8、缓存穿透、击穿、雪崩

缓存穿透:

概念:用户需要查询一个数据,缓存中没有,也就是没有命中,于是向数据库中发起请求,发现也没有。当用户很多的时候,缓存都没有命中,于是都去请求数据库,这给数据库造成很大的压力。

解决方案

  • 布隆过滤器:是一种数据结构,对所有可能查询的参数以 hash 方式存储,先在控制层进行校验,不符合则丢弃,避免了过多访问数据库。
  • 缓存空对象:当存储层没有命中时,即使返回空对象也将其缓存起来。(意味着更多的空间存储,即使设置了过期时间,缓存和数据库还是有段时间数据不一致。)

缓存击穿:

概念:当一个 key 非常热点时,在不断扛高并发,集中对这个热点数据进行访问,当这个 key 失效的瞬间,请求直接到达数据库,给数据库瞬间的高压力。

解决方案

  • 设置热点数据永不过期
  • 加分布式锁:保证每个 key 同时只有一个线程去查询后端服务。

缓存雪崩:

概念:某个时间段,缓存集中失效

解决方案

  • 增加 Redis 集群的数量
  • 限流降级:在缓存失效后,通过加锁和队列来控制数据库写缓存的线程数量
  • 数据预热:正式部署之前,将数据预先访问一遍,让缓存失效的时间尽量均匀

9、Redis 的使用场景

1)会话缓存:如 单点登录,使用 Redis 模拟 session,SSO 系统生成一个 token,将用户信息存到 Redis 中,并设置过期时间;
2)全页缓存
3)作为消息队列平台
4)排行榜和计数器
5)发布/订阅:比如聊天系统
6)热点数据:比如ES中搜索的热词

10、Redis 缓存如何保持一致性

读数据的时候首先去 Redis 中读取,没有读到再去 MySQL 中读取,读取都数据更新到 Redis 中作为下一次的缓存。

写数据的时候会产生数据不一致的问题。无论是先写入 Redis 再写入 MySQL 中,还是先写入 MySQL 再写入 Redis 中,这两步操作都不能保证原子性,所以会出现 Redis 和 MySQL 中数据不一致的问题。

无论采取何种方式都不能保证强一致性,如果对 Redis 中的数据设置了过期时间,能够保证最终一致性,对架构的优化只能降低发生的概率,不能从根本上避免不一致性。

更新缓存的两种方式:删除失效缓存、更新缓存
更新缓存和数据库有两种顺序:先数据库后缓存、先缓存后数据库
两两组合,分为四种更新策略。

原文地址:https://www.cnblogs.com/liusuixing/p/14198140.html