IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> Redis高级1 -> 正文阅读

[大数据]Redis高级1

个人博客

欢迎访问个人博客: https://www.crystalblog.xyz/

备用地址: https://wang-qz.gitee.io/crystal-blog/

简介

Redis前面的基础部分此处不做记录 , 本篇记录周阳老师讲解的Redis配置及高级应用的知识.

尚硅谷-周阳思维导图
链接: https://pan.baidu.com/s/1jY8bh8D0MN_4WN1hqwIQeQ
提取码: ezog
课件,别感谢我!

B站视频: 尚硅谷超经典Redis教程,redis实战,阳哥版从入门到精通

Redis官网

Redis中文网

https://redis.com.cn/

http://www.redis.cn/

https://www.redis.net.cn/

1. 解析Redis配置文件

解压后的Redis目录下有一个redis.conf文件, 里面是实现redis各种功能的配置项.

1.1 Units单位

配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit. 对大小写不敏感.

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

1.2 INCLUDES包含

可以通过includes包含,redis.conf可以作为总闸,包含其他。

################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

1.3 NETWORK网络

# 开启只能本机访问
bind 127.0.0.1 -::1
# 保护模式
protected-mode yes
# http端口
port 6379
# 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列+已经完成三次握手队列。
# 在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。
# 注意linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大somaxconn和tcp_max_syn_backing两个值来达到想要的效果。
tcp-backlog 511
# 超时关闭连接, 0不关闭
timeout 0
# 心跳机制!!! 单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
tcp-keepalive 300

1.4 GENERAL通用

# 守护线程启动
daemonize  no
# 进程管道文件
pidfile /var/run/redis_6379.pid
# 日志级别 debug verbose notice warning; 级别越高,日志越少
# 生产模式 warning
loglevel notice
# 日志文件名称
logfile ""
# 是否把日志输出到syslog中
syslog-enabled no
# 指定syslog里的日志标志
syslog-ident redis
# 指定syslog设备,值可以是user或local0-local7 , 默认0
syslog-facility local0
# 默认16个库
databases 16

1.5 SNAPSHOTTING快照

# save 秒钟  写操作次数
save <seconds> <changes>
# 默认是yes,如果配置成no,表示不在乎数据不一致或者有其他的手段发现和控制。
stop-writes-on-bgsave-error yes
# rdbcompression:对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。
# 如果不想消耗CPU来进行压缩的话,可以设置为no关闭此功能。默认是yes
rdbcompression yes
# rdbchecksum:在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗;
# 如果希望获取到最大的性能提升,可以关闭此功能。默认是yes
rdbchecksum yes
# 快照文件名, 默认dump.rdb, 建议以dump-端口.rdb命名
dbfilename dump.rdb
# 数据备份文件的目录; 日志文件的默认目录等
dir ./

1.6 REPLICATION复制

# 关闭保护模式,用于公网访问
protected-mode no
# 端口
port 6380
# 开启集群模式
cluster-enabled yes
cluster-config-file nodes-6380.conf
cluster-node-timeout 5000
# 后台启动
daemonize yes
pidfile /var/run/redis_6380.pid
logfile "6380.log"
#dir /redis/data
# 此处绑定ip可以是内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456

1.7 SECURITY安全

访问密码的查看, 设置和取消.

requirepass foobared

客户端操作密码

[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6379
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
127.0.0.1:6379> ping # 再次测试连通性
(error) NOAUTH Authentication required. # 没有授权, 需要密码授权
127.0.0.1:6379> config get requirepass 
(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> ping # 测试连通性
PONG
127.0.0.1:6379> config get dir # 查看redis启动目录
1) "dir"
2) "/usr/local/redis-6.x"

1.8 LIMITS限制

设置redis同时可以与多少个客户端进行连接。默认情况下为10000个客户端。当你无法设置进程文件句柄限制时,redis会设置为当前的文件句柄限制值减去32,因为redis会为自身内部处理逻辑留一些句柄出来。如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。

maxclients 10000

设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。

但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是master redis(说明你的redis有slave redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素.

maxmemory <bytes>

内存淘汰策略

  • volatile-lru:使用LRU算法移除key,从设置了过期时间的key中最近最少使用的数据淘汰.
  • allkeys-lru:使用LRU算法移除key, 从所有key中挑选最近最少使用的数据淘汰.
  • volatile-lfu : 使用LFU算法移除key,从设置了过期时间的key中挑选最近使用次数最少的数据淘汰
  • allkeys-lfu : 使用LFU算法移除key,从所有key中挑选最近使用次数最少的数据淘汰
  • volatile-random:从设置了过期时间的key中移除随机的key.
  • allkeys-random:从所有key中随机移除key.
  • volatile-ttl:移除那些TTL值最小的key,即那些即将要过期的key
  • noeviction:不进行移除。针对写操作,只是返回错误信息
maxmemory-policy noeviction

设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个.

maxmemory-samples 5

1.9 APPEND ONLY MODE追加

# yes开启aof配置, 默认no 
appendonly no
# aof备份文件名
appendfilename "appendonly.aof"
# appendfsync always|everysec|no  aof持久化策略
# always 同步持久化,每次发生数据变更会立即记录到磁盘,性能较差但数据完整性比较好
# everysec 出厂默认推荐,异步操作,每秒记录,如果一秒内宕机,有数据丢失
# no 不aof备份
appendfsync everysec
# 重写时是否可以运行Appendfsync,用默认no即可,保证数据安全性。
no-appendfsync-on-rewrite no
# 设置重写的基准值
auto-aof-rewrite-percentage 100
# 设置重写的基准值
auto-aof-rewrite-min-size 64mb

1.10 常见配置redis.conf介绍

工作中遇到不会的可以按照这份常用配置进行查阅.

redis.conf 配置项说明如下:

(1). Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程

daemonize no

(2). 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定

pidfile /var/run/redis.pid

(3). 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字

port 6379

(4). 绑定的主机地址

bind 127.0.0.1

(5).当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能

timeout 300

(6). 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose

loglevel verbose

(7). 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null

logfile stdout

(8). 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id

databases 16

(9). 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 save <seconds> <changes>
Redis默认配置文件中提供了三个条件:

save 900 1
save 300 10
save 60 10000
分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。

(10). 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大

rdbcompression yes

(11). 指定本地数据库文件名,默认值为dump.rdb

dbfilename dump.rdb

(12). 指定本地数据库存放目录

dir ./

(13). 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步

slaveof

(14). 当master服务设置了密码保护时,slav服务连接master的密码

masterauth

(15). 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH 命令提供密码,默认关闭

requirepass foobared

(16). 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息

maxclients 128

(17). 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区

maxmemory

(18). 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no

appendonly no

(19). 指定更新日志文件名,默认为appendonly.aof

appendfilename appendonly.aof

(20). 指定更新日志条件,共有3个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快)
always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
everysec:表示每秒同步一次(折衷,默认值)

appendfsync everysec

(21). 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)

vm-enabled no

(22). 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享

vm-swap-file /tmp/redis.swap

(23). 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0

vm-max-memory 0

(24). Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值

vm-page-size 32

(25). 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。

vm-pages 134217728

(26). 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4

vm-max-threads 4

(27). 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启

glueoutputbuf yes

(28). 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法

hash-max-zipmap-entries 64
hash-max-zipmap-value 512

(29). 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)

activerehashing yes

(30). 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件

include /path/to/local.conf

2. Redis持久化

2.1 RDB

2.1.1 RDB介绍

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

Redis会单独创建(Fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能. RDB保存的是dump.rdb文件.

如果要进行大规模数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量‘程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

优势: 适合大规模的数据恢复; 对数据完整性和一致性要求不高;

劣势: Fork的时候,内存中的数据被克隆了一份,大约2倍的膨胀性需要考虑。

关闭rdb: 动态停止RDB保存规则的方法:redis-cli config set save “”

2.1.2 如何触发RDB

开放配置文件中默认的快照配置, 也可以修改自定义数值. 命令save或者是bgsave, 高版本的redis优化为bgsave指令实现. 可以自行查阅save和bgsave指令备份的区别.

# save 秒钟  写操作次数
save <seconds> <changes>

save和bgsave

save : 只管保存,其他不管,全部zuse

bgsave :Redis会在后台异步进行快照操作,快照操作同时还可以响应客户端请求。可以通过lastsave命令获取最后一次成功执行快照的时间。

在客户端也可以手动执行save进行备份, 将数据写入dump.rdb文件, 相当于commit操作.

执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义。

如果恢复数据

将备份文件(dump.rdb)移动到redis安装目录并启动服务即可. 使用config get dir获取redis安装目录.

冷拷贝后重新使用, 可以cp dump.rdb dump_new.rdb, 等晚上业务停用时进行数据恢复.

2.1.3 客户端演示

不直接在原来配置上做修改, 防止配置错误无法恢复, 首先将redis.conf的复制一份到启动目录下, 修改下面的配置项, 假设为60s内写操作5次就触发rdb备份.

确保关闭了aof配置, 否则aof持久化数据也会生效, 并在重启redis时以aof方式恢复数据, 达不到rdb恢复数据的效果.

# 60s内写操作5次就触发rdb备份
save 60 5
# 关闭aof持久化
appendonly no

如果之前启动过redis, 先删除原来的dump.rdb文件, 再启动redis服务. 然后连接redis客户端, 写入数据达到上面的触发要求.

[root@centos7-01 redis-6.x]# vi redis.conf
[root@centos7-01 redis-6.x]#
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf 
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456 
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name cysw # 写次数1
OK
127.0.0.1:6379> set name sdfs # 写次数2
OK
127.0.0.1:6379> set name panda # 写次数3
OK
127.0.0.1:6379> set age 55 # 写次数4
OK
127.0.0.1:6379> set age 345 # 写次数5
OK
127.0.0.1:6379> set age 29 # 写次数6
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> SHUTDOWN # 模拟宕机, 关闭redis服务
not connected> exit
[root@centos7-01 redis-6.x]# ll
total 96
-rw-r--r--. 1 root root     0 Apr  3 17:18 appendonly.aof # 因为关闭了aof配置, 文件大小为0
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
-rw-r--r--. 1 root root   117 Apr  3 17:20 dump.rdb  # 触发rdb备份后, 数据备份到dump.rdb文件中
-rwxr-xr-x. 1 root root 93754 Apr  3 17:19 redis.conf
# 用redis-check-rdb命令检查dump.rdb
[root@centos7-01 redis-6.x]# ./bin/redis-check-rdb dump.rdb
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '6.2.6'
[offset 40] AUX FIELD redis-bits = '64'
[offset 52] AUX FIELD ctime = '1648977645'
[offset 67] AUX FIELD used-mem = '872144'
[offset 83] AUX FIELD aof-preamble = '0'
[offset 85] Selecting DB ID 0
[offset 117] Checksum OK
[offset 117] \o/ RDB looks OK! \o/
[info] 2 keys read  # 两个key
[info] 0 expires
[info] 0 already expired

现在重启redis服务, redis会以rdb方式恢复数据.

[root@centos7-01 redis-6.x]# ps -ef | grep  redis
root       3215   3035  0 15:50 pts/1    00:00:00 less redis.conf
root       3444   2977  0 17:21 pts/0    00:00:00 grep --color=auto redis
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> get name
"panda"
127.0.0.1:6379> get age
"29"

2.1.4 RDB总结

内存中的数据对象 ---rdbsave---> 磁盘中的RDB文件

内存中的数据对象 <---rdbLoad--- 磁盘中的RDB文件

优点:

  • RDB是一个非常紧凑的文件.

  • 在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所有RDB持久化方式可以最大化redis的性能。

  • 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些。

缺点:

  • 数据丢失风险大. 可能丢失最后一次备份时间之后写入的数据.

  • RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能导致Redis在一些毫秒级不能响应客户端请求。

2.2 AOF

2.2.1 AOF介绍

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

2.2.2 AOF启动/修复/恢复

正常恢复

redis.conf配置文件中配置appendonly yes开启aof持久化, 使用默认每秒备份策略:

appendfsync everysec

如果之前启动过, 先删除redis安装目录下的appendonly.aof文件, 再重启redis服务会重新加载数据.

[root@centos7-01 redis-6.x]# ll 
total 92
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
-rwxr-xr-x. 1 root root 93745 Apr  3 21:06 redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379>
127.0.0.1:6379> keys * # db空的
(empty array)
127.0.0.1:6379> config get dir # 查看redis安装目录
1) "dir"
2) "/usr/local/redis-6.x"
127.0.0.1:6379>
127.0.0.1:6379> set k1 v2
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
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> SHUTDOWN # 模拟redis服务宕机
not connected> exit
[root@centos7-01 redis-6.x]# ll
total 100
-rw-r--r--. 1 root root   185 Apr  3 21:07 appendonly.aof # 可以看到aof文件有大小
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
-rw-r--r--. 1 root root    92 Apr  3 21:07 dump.rdb # rdb文件也有大小,这个后面说
-rwxr-xr-x. 1 root root 93745 Apr  3 21:06 redis.conf

查看备份的命令日志文件appendonly.aof, 可以看到文件中记录了写操作的命令.

[root@centos7-01 redis-6.x]# cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v2
*3
$3
set
$2
k2
$2
v2
*3
$3
set
$2
k3
$2
v3
*3
$3
set
$2
k4
$2
v4
*3
$3
set
$2
k5
$2
v5
*1
$7
flushdb

因为appendonly.aof文件最后的指令是flushdb, 所以宕机重启服务后恢复的数据也是空的.

[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
(empty array)

现在我们重新写入数据, 再次模拟宕机, 然后重启看是否可以恢复数据.

[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379>
127.0.0.1:6379> keys * # 查看db中的key, 空的
(empty array)
127.0.0.1:6379>
127.0.0.1:6379> set k1 v1 # 写操作1
OK
127.0.0.1:6379> set k2 v2 # 写操作2
OK
127.0.0.1:6379> SHUTDOWN # 模拟宕机
not connected> exit
[root@centos7-01 redis-6.x]# ll 
total 100
-rw-r--r--. 1 root root   266 Apr  3 21:14 appendonly.aof # 可以看到aof文件大小266,变大了
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
-rw-r--r--. 1 root root   111 Apr  3 21:14 dump.rdb
-rwxr-xr-x. 1 root root 93745 Apr  3 21:06 redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys * # 上面重启redis服务后, 数据正常恢复成功
1) "k1"
2) "k2"

注意: 运维人员可以将有数据的aof文件复制一份保存到对应目录(config get dir), 让数据从aof文件中恢复.

异常恢复

在生产环境中, 可能会出现网络延迟等现象导致redis写操作没有完成, appendonly.aof文件中记录的写命令就是不完整的, 那么redis宕机后能从异常的aof文件中恢复数据吗? 下面我们通过实操验证, 先在appendonly.aof文件末尾手动添加一条错误的写命令模拟异常文件.

[root@centos7-01 redis-6.x]# vim appendonly.aof
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
# 无法连接到redis服务器, 说明6379的redis服务没有启动成功
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected>

从上面的验证结果可以看出, 如果appendonly.aof文件中出现错误命令, 是无法正常启动redis服务的. 可以通过官方提供的redis-check-aof命令对损坏的appendonly.aof文件进行修复, 也就是将错误的命令删除.

./bin/redis-check-aof --fix appendonly.aof

[root@centos7-01 redis-6.x]# ./bin/redis-check-aof --fix appendonly.aof
0x             11a: Expected to read 2 bytes, got 0 bytes
AOF analyzed: size=282, ok_up_to=266, ok_up_to_line=67, diff=16
This will shrink the AOF from 282 bytes, with 16 bytes, to 266 bytes
Continue? [y/N]: y
Successfully truncated AOF # 错误命令被成功删除
[root@centos7-01 redis-6.x]# less appendonly.aof
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys * # 修复aof文件后重启redis成功, 数据也恢复成功.
1) "k1"
2) "k2"

2.2.3 AOF重写

AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。

可以使用命令: bgrewriteaof

127.0.0.1:6379> BGREWRITEAOF # 手动执行aof命令重写
Background append only file rewriting started
127.0.0.1:6379> exit
[root@centos7-01 redis-6.x]# cat appendonly.aof # 重写被做了啥处理,看不懂
REDIS0009?      redis-ver6.2.6?
?edis-bits?@?ctime?)?Ibused-mem??N
aof-preamble???k1v1k2v2?R????[root@centos7-01 redis-6.x]# xterm-256colorxterm-256color
-bash: xterm-256colorxterm-256color: command not found
[root@centos7-01 redis-6.x]# ll
total 100
-rw-r--r--. 1 root root   111 Apr  3 21:42 appendonly.aof # aof重写后, 文件大小减少了.
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
-rw-r--r--. 1 root root   111 Apr  3 21:22 dump.rdb
-rwxr-xr-x. 1 root root 93745 Apr  3 21:06 redis.conf

重写原理

AOF文件持续膨胀而过大时,会fork出一条新进程将文件重写(也是先写临时文件最后在rename),遍历新进程的内存中数据,每条记录有一条Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

重写触发机制

Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。

# 设置重写的基准值, AOF文件大小达到上次rewrite后大小的一倍
auto-aof-rewrite-percentage 100
# 设置重写的基准值, AOF文件大小大于64MB时
auto-aof-rewrite-min-size 64mb

redis.conf文件中有这么一段话, 大概意思就是: RDB和AOF可以共存,但是恢复的时候找的是AOF,如果AOF文件异常,可以通过redis-check-aof进行AOF修复。这也就明白了上面aof备份时, 也同时产生了RDB文件.

# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.

?

2.2.4 AOF总结

在这里插入图片描述

优点:

  • appendfsync always 同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好.

  • appendfsync everysec 异步操作 ,每秒记录 如果一秒内宕机,有数据丢失。

  • appendfsync no 从不同步

  • AOF文件是一个只进行追加的日志文件

  • Redis可以在AOF文件体积变得过大时,自动地在后台对AOF进行重写

  • AOF文件有序地保存了对数据执行的所有写入操作,这些写入操作以Redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松。

缺点:

  • 相同数据集的数据而言, aof文件体积要远大于rdb文件,恢复速度慢于rdb
  • 根据所使用的fsync策略,AOF的速度可能会慢于RDB, 不同步时效率和RDB相同 。

3. Redis事务

可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不许加塞。 也就是一个队列中,一次性、顺序性、排他性地执行一系列命令.

3.1 Redis事务常用命令

DISCARD:取消事务,放弃执行事务块内的所有命令。

EXEC:执行所有事务块的命令。

MULTI:标记一个事务块的开始。

UNWATCH:取消WATCH命令对多有key的监视。

WATCH key [key…]:监视一个或多个key,如果在事务执行前该key被其他命令所改动,那么事务将打断。

3.2 Redis事务三阶段

  • 开启:以MULTI开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面。
  • 执行:由EXEC命令触发事务提交

3.4 Redis事务三特性

  • 单独的隔离操作:事务中的所有命令都会被序列化、按顺序地执行。事务在执行的构成中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题。
  • 不保证原子性:redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

3.5 Redis事务演示

3.5.1 正常放行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"
4) OK

3.5.2 取消事务

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 22
QUEUED
127.0.0.1:6379(TX)> set k3 33
QUEUED
127.0.0.1:6379(TX)> discard # 取消事务
OK
127.0.0.1:6379> get k2 # 事务被取消, k2还是原来的值
"v2"

3.5.3 全体连坐

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v4
QUEUED
127.0.0.1:6379(TX)> getset k3 # 编译期错误, 会导致整个队列的命令执行失败
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4 # 因为上面的命令错误, 导致整个队列的命令执行失败
(nil)

3.5.4 冤有头债有主

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1 # 运行时异常, k1是非数字字符串, 不能计算
QUEUED
127.0.0.1:6379(TX)> set k2 22
QUEUED
127.0.0.1:6379(TX)> set k3 33
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> get k4
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) OK
5) "v4"
127.0.0.1:6379> get k4 # 上面的异常, 命令没有语法错误, 队列中其他命令正常执行
"v4"

3.5.5 watch监控

在学习watch监控之前, 我们先来了解一下悲观锁/乐观锁相关知识.

悲观锁

顾名思义,每次去拿数据的时候都被认为别人会修改,所以每次在拿数据的时候都会被锁上,这样别人想拿这个数据就会block直到它拿到锁,传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先锁上。

乐观锁

每次去拿数据的时候都认为别人不会修改所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号, CAS等机制

CAS机制

CAS(Compare And Swap), 比较并替换. CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新, 直到成功。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为新值B。

CAS的缺点

(1). CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

(2). 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

扣减余额操作, 无加塞篡改,先监控再开启multi,保证两笔金额变动在同一个事务内.

# 客户端1
127.0.0.1:6379> set balance 100 # 余额
OK
127.0.0.1:6379> set debt 0 # 消费合计
OK
127.0.0.1:6379> WATCH balance
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY balance 20
QUEUED
127.0.0.1:6379(TX)> INCRBY debt 20
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> get balance
"80"
127.0.0.1:6379> get debt
"20"

有加塞篡改: 被监控的数据, 在事务执行过程中, 其他客户端修改了被监控的数据.

# 客户端1
127.0.0.1:6379> WATCH balance # 监控balance
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY balance 20 # 余额扣减20
QUEUED
127.0.0.1:6379(TX)> INCRBY debt 20 # 消费记账增加20
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务失败, 因为在提交事务之前, 客户端2修改了被监控的balance
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get balance # 查询balance已经被客户端2修改
"800"

客户端2, 在客户端1开启对balance的监控后, 在客户端1事务提交之前, 修改balance值为800.

# 客户端2
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> get balance
"80"
127.0.0.1:6379> set balance 800 # 修改balance值为800.
OK

一定要取消监控后或者没有其他客户端修改被监控的key, 事务才能提交成功.

一旦执行了exec,之前加的监控锁watch都会被取消掉.

# 客户端1
127.0.0.1:6379> WATCH balance # 监控balance
OK
127.0.0.1:6379> set balance 500 # 修改balance
OK
127.0.0.1:6379> get balance 
"500"
127.0.0.1:6379> UNWATCH # 取消监控balance
OK
127.0.0.1:6379> WATCH balance # 再次监控balance
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set balance 80
QUEUED
127.0.0.1:6379(TX)> set debt 20
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务
1) OK
2) OK
# 客户端2, 查询到balance是客户端1修改后的80
127.0.0.1:6379> get balance
"80"

总结

watch指令,类似乐观锁,事务提交时,如果Key的值已经被别的客户端改变,比如某个list已经被别的客户端push/pop过了,整个事务队列都不会被执行。

通过WATCH命令在事务执行之前监控了多个keys,倘若在WATCH之后有任何key的值的变化,EXEC命令执行的事务都将被放弃,同时返回Nullmulti-bulk应答以通知调用者事务执行失败。

4. Redis的发布订阅

4.1 介绍

进程间的一种消息通信模式:发送者(publish)发送消息,订阅者(subscribe)接受消息。

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

发布订阅

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

发布订阅

4.2 常用命令

# 订阅给定的一个或多个频道的信息
subscribe channel [channel2 ...]
# 订阅一个或多个符合给定模式的频道
psubscribe pattern [pattern2 ...]
# 将信息发送到指定的频道
publish channel message
# 退订给指定的频道
unsubscribe channel [channel2 ...]
# 退订所有给定模式的频道
punsubscribe pattern [pattern2 ...]

4.3 案例

先订阅后发布才能收到信息

可以一次性订阅多个,SUBSCRIBE c1 c2 c3

消息发布,PUBLISH c2 hello-redis

# 订阅消息的客户端
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> SUBSCRIBE c1 c2 c3
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
1) "subscribe"
2) "c2"
3) (integer) 2
1) "subscribe"
2) "c3"
3) (integer) 3
# 收到发布的消息
1) "message"
2) "c1"
3) "hello-redis"
1) "message"
2) "c2"
3) "hello2-redis"
1) "message"
2) "c3"
3) "hello3-redis"

发布消息

# 发布消息的客户端
[root@centos7-01 redis-6.x]# ./bin/redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> PUBLISH c1 hello-redis
(integer) 1
127.0.0.1:6379> PUBLISH c2 hello2-redis
(integer) 1
127.0.0.1:6379> PUBLISH c3 hello3-redis
(integer) 1

订阅多个,通配符*,PSUBSCRIBE new*

收到消息,PUBLISH new1 hello1

这里就不演示了, 和上面的操作一样, 我是使用的redis-6.x的版本, 没有起到通配符的效果, 把new*当作一个频道了.

5. Redis的主从复制(Master/Slave)

5.1 介绍

主从复制,主机数据更新后根据配置和策略,自动同步到备机的master/slver机制,Master以写为主,Slave以读为主。

可以进行读写分离, 容灾恢复等功能的实现.

5.2 主从复制原理

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

5.3 主从复制演示

5.3.1 一主二从

从库配置:slaveof master_IP master_port

slave每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件.

Info replication 命令可以查看当前服务的角色信息, 是主库还是从库.

现在先做一主二从的机器准备, 将redis.conf配置文件复制出来三份, 并修改相关的配置信息, 修改配置文件细节操作:

# 关闭保护模式,用于公网访问
protected-mode no
# 修改端口
port 6380
# 后台启动
daemonize yes
pidfile /var/run/redis_6380.pid
# 防止在其他目录启动,最好写绝对路径的文件名 /usr/local/redis-6.x/data/6380.log
logfile "./data/6380.log"
# 此处绑定ip可以是内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456
# 修改rdb文件名称
dbfilename dump_6380.rdb
# 数据备份文件的目录; 日志文件的默认目录等; 防止在其他目录启动,最好写绝对路径
dir /usr/local/redis-6.x/data

准备的配置文件

[root@centos7-01 redis-6.x]# pwd
/usr/local/redis-6.x
[root@centos7-01 redis-6.x]# ll
total 376
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
drwxr-xr-x. 2 root root   118 Apr  4 16:59 data
-rwxr-xr-x. 1 root root 93807 Apr  4 17:04 redis6379.conf # 6379配置
-rwxr-xr-x. 1 root root 93807 Apr  4 17:05 redis6380.conf # 6380配置
-rwxr-xr-x. 1 root root 93807 Apr  4 17:05 redis6381.conf # 6381配置
-rwxr-xr-x. 1 root root 93745 Apr  3 21:06 redis.conf

三台服务的配置修改完成后, 分别启动三台服务进行初始化操作, 并查看三台服务各自的角色和状态信息, 发现此时的三台服务都是master, 因为没有指定主从关系.
在这里插入图片描述

现在分配主从关系, 6379为master, 6380, 6381为slave. 通过客户端执行命令 slaveof 127.0.0.1 6379; 再次查看三台服务的主从关系, 以及数据同步情况.
在这里插入图片描述

查看master主机6379.log日志.

DB loaded from append only file: 0.001 seconds
5122:M 04 Apr 2022 18:02:31.255 * Ready to accept connections
5122:M 04 Apr 2022 18:03:20.478 * Replica 127.0.0.1:6380 asks for synchronization  # slave从机6380向master主机请求数据同步
5122:M 04 Apr 2022 18:03:20.478 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '39ab3c51e0304f13d9738e798332df54cc604a48', my replication IDs are '7b7c05d015af0b045eb1f22c505464ffb51bb3ea' and '0000000000000000000000000000000000000000')
5122:M 04 Apr 2022 18:03:20.478 * Replication backlog created, my new replication IDs are '47715abc4fa6f05dbfacd1befe65bbab07e62b95' and '0000000000000000000000000000000000000000'
5122:M 04 Apr 2022 18:03:20.478 * Starting BGSAVE for SYNC with target: disk
5122:M 04 Apr 2022 18:03:20.510 * Background saving started by pid 5143
5143:C 04 Apr 2022 18:03:20.513 * DB saved on disk
5143:C 04 Apr 2022 18:03:20.514 * RDB: 4 MB of memory used by copy-on-write
5122:M 04 Apr 2022 18:03:20.535 * Background saving terminated with success
5122:M 04 Apr 2022 18:03:20.535 * Synchronization with replica 127.0.0.1:6380 succeeded # master主机同步数据到6380从机成功
5122:M 04 Apr 2022 18:03:27.140 * Replica 127.0.0.1:6381 asks for synchronization  # slave从机6381向master主机请求数据同步
5122:M 04 Apr 2022 18:03:27.141 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '6e958e46e653bf8aae144d6f45faff64cd675e00', my replication IDs are '47715abc4fa6f05dbfacd1befe65bbab07e62b95' and '0000000000000000000000000000000000000000')
5122:M 04 Apr 2022 18:03:27.141 * Starting BGSAVE for SYNC with target: disk
5122:M 04 Apr 2022 18:03:27.144 * Background saving started by pid 5146
5146:C 04 Apr 2022 18:03:27.149 * DB saved on disk
5146:C 04 Apr 2022 18:03:27.150 * RDB: 4 MB of memory used by copy-on-write
5122:M 04 Apr 2022 18:03:27.189 * Background saving terminated with success 
5122:M 04 Apr 2022 18:03:27.189 * Synchronization with replica 127.0.0.1:6381 succeeded # master主机同步数据到6381从机成功

查看slave从机6380.log的日志

5130:S 04 Apr 2022 18:03:20.477 * Connecting to MASTER 127.0.0.1:6379 # 6380从机连接master主机(6379)
5130:S 04 Apr 2022 18:03:20.477 * MASTER <-> REPLICA sync started # 复制同步开始
5130:S 04 Apr 2022 18:03:20.477 * REPLICAOF 127.0.0.1:6379 enabled (user request from 'id=3 addr=127.0.0.1:36190 laddr=127.0.0.1:6380 fd=9 name= age=31 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=42 qbuf-free=40912 argv-mem=20 obl=0 oll=0 omem=0 tot-mem=61484 events=r cmd=slaveof user=default redir=-1')
5130:S 04 Apr 2022 18:03:20.477 * Non blocking connect for SYNC fired the event.
5130:S 04 Apr 2022 18:03:20.477 * Master replied to PING, replication can continue...
5130:S 04 Apr 2022 18:03:20.478 * Trying a partial resynchronization (request 39ab3c51e0304f13d9738e798332df54cc604a48:1).
5130:S 04 Apr 2022 18:03:20.511 * Full resync from master: 47715abc4fa6f05dbfacd1befe65bbab07e62b95:0
5130:S 04 Apr 2022 18:03:20.511 * Discarding previously cached master state.
5130:S 04 Apr 2022 18:03:20.535 * MASTER <-> REPLICA sync: receiving 175 bytes from master to disk # 从master主机同步数据中...
5130:S 04 Apr 2022 18:03:20.535 * MASTER <-> REPLICA sync: Flushing old data
5130:S 04 Apr 2022 18:03:20.536 * MASTER <-> REPLICA sync: Loading DB in memory
5130:S 04 Apr 2022 18:03:20.538 * Loading RDB produced by version 6.2.6
5130:S 04 Apr 2022 18:03:20.540 * RDB age 0 seconds
5130:S 04 Apr 2022 18:03:20.540 * RDB memory usage when created 1.85 Mb
5130:S 04 Apr 2022 18:03:20.541 # Done loading RDB, keys loaded: 0, keys expired: 0.
5130:S 04 Apr 2022 18:03:20.542 * MASTER <-> REPLICA sync: Finished with success  # 6380从master主机(6379)同步完成
5130:S 04 Apr 2022 18:03:20.544 * Background append only file rewriting started by pid 5145
5130:S 04 Apr 2022 18:03:20.618 * AOF rewrite child asks to stop sending diffs.
5145:C 04 Apr 2022 18:03:20.618 * Parent agreed to stop sending diffs. Finalizing AOF...
5145:C 04 Apr 2022 18:03:20.622 * Concatenating 0.00 MB of AOF diff received from parent.
5145:C 04 Apr 2022 18:03:20.622 * SYNC append only file rewrite performed
5145:C 04 Apr 2022 18:03:20.623 * AOF rewrite: 4 MB of memory used by copy-on-write
5130:S 04 Apr 2022 18:03:20.636 * Background AOF rewrite terminated with success
5130:S 04 Apr 2022 18:03:20.637 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)
5130:S 04 Apr 2022 18:03:20.637 * Background AOF rewrite finished successfully

主从问题演示

(1) 切入点问题?slave1、slave2是从头开始复制还是从切入点开始复制?比如从k4进来,那之前的123是否也可以复制

? 可以, 会先进行全量复制, 然后master主机的写命令会同步给slave从机更新数据.

(2) 从机是否可以写?set可否?
读写分离, master负责写, slave负责读, slave不能写.
在这里插入图片描述

(3) 主机shutdown后情况如何?从机是上位还是原地待命

? 主机shutdown后, 从机不会上位到master, 仍然在原地待命, 后面的集群-哨兵机制可以解决这个问题.
在这里插入图片描述

(4) 主机又回来了后,主机新增记录,从机还能否顺利复制?

? master主机重新启动后, 主机写数据, 从机会重新同步主机的数据, 可以顺利复制.
在这里插入图片描述

(5) 其中一台从机down后情况如何?依照原有它能跟上大部队吗?

? slave从机宕机后, 重新启动不会与master主机建立关系, slave重启后就是一个单例的独立master, 需要重新执行命令slaveof 127.0.0.1 6379与master主机6379建立主从关系.
在这里插入图片描述

5.3.2 薪火相传

上一个slave可以是下一个slave的Master,slave同样可以接受其他slaves的连接和同步请求,那么该slave作为链条中下一个的master,可以有效减轻master的写压力。

中途变更转向:会清除之前的数据,重新建立拷贝最新的。 slaveof 新主库IP 新主库端口.
在这里插入图片描述

可以看到6379写的数据会同步6380, 然后6380再同步到6381, 在这个链条上一直传下去. 但注意6380虽然是6381的主机, 但6380本身是6379的从机, 所以6380从机是不能写操作的, 从下面的报错信息也可以验证.
在这里插入图片描述

5.3.3 反客为主

SLAVEOF no one:使当前数据库通知与其他数据库的同步,转成主数据库。

SLAVEOF no one 实现的反客为主, 虽然能让当前slave服务转为master, 但是其他slave需要手动重新建立与新的master建立主从关系. 这个问题在下面的哨兵模式中可以解决, 哨兵模式就相当于将手动建立主从关系的操作改为自动选举和建立主从关系.
在这里插入图片描述

5.4 哨兵模式(sentinel)

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库, 并让其他从库与新的主库建立主从关系.

现在重新调整redis服务的主从结果, 6379为master, 6380, 6382为slave.
在这里插入图片描述

创建哨兵配置文件 , redis安装目录下新建sentinel.conf文件,名字决不能错。编辑sentinel.conf添加如下内容:

sentinel monitor 被监控数据库名字(自定义)127.0.0.1 6379 1

上面最后一个数字1,表示主机挂掉后slave投票数超过1后, 就让谁接替成为master主机,得票数多的成为主机.

# sentinel monitor 被监控数据库名字(自定义)127.0.0.1 6379 1
sentinel monitor redis6379 127.0.0.1 6379 1

启动哨兵, 这里采用的是非守护进程的方式启动, 如果窗口关闭, 哨兵服务也就关闭了. 后面介绍后台运行的配置方式.

[root@centos7-01 redis-6.x]# ./bin/redis-sentinel sentinel.conf
5691:X 04 Apr 2022 22:46:17.937 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
5691:X 04 Apr 2022 22:46:17.937 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=5691, just started
5691:X 04 Apr 2022 22:46:17.937 # Configuration loaded
5691:X 04 Apr 2022 22:46:17.938 * Increased maximum number of open files to 10032 (it was originally set to 1024).
5691:X 04 Apr 2022 22:46:17.938 * monotonic clock: POSIX clock_gettime
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 6.2.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 5691
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           https://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

5691:X 04 Apr 2022 22:46:17.951 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
5691:X 04 Apr 2022 22:46:17.959 # Sentinel ID is c34ca410b69366f5ec74d2e0b9e93e0213c94245
5691:X 04 Apr 2022 22:46:17.959 # +monitor master redis6379 127.0.0.1 6379 quorum 1

6379的master关闭后, 哨兵自动选举一个slave成为新的master.
在这里插入图片描述

好奇怪, 我自己操作的过程中, slave一直重复投票给已经挂掉的6379服务, 具体原因不清楚, 可能是配置问题吧, 日志如下:

# 第1次投票
5691:X 04 Apr 2022 22:46:47.969 # +new-epoch 1
5691:X 04 Apr 2022 22:46:47.969 # +try-failover master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:46:47.977 # +vote-for-leader c34ca410b69366f5ec74d2e0b9e93e0213c94245 1
5691:X 04 Apr 2022 22:46:47.977 # +elected-leader master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:46:47.977 # +failover-state-select-slave master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:46:48.069 # -failover-abort-no-good-slave master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:46:48.138 # Next failover delay: I will not start a failover before Mon Apr  4 22:52:48 2022
# 第2次投票
5691:X 04 Apr 2022 22:52:48.601 # +new-epoch 2
5691:X 04 Apr 2022 22:52:48.601 # +try-failover master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:52:48.611 # +vote-for-leader c34ca410b69366f5ec74d2e0b9e93e0213c94245 2
5691:X 04 Apr 2022 22:52:48.611 # +elected-leader master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:52:48.611 # +failover-state-select-slave master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:52:48.679 # -failover-abort-no-good-slave master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:52:48.740 # Next failover delay: I will not start a failover before Mon Apr  4 22:58:49 2022
# 第3次投票
5691:X 04 Apr 2022 22:58:49.580 # +new-epoch 3
5691:X 04 Apr 2022 22:58:49.580 # +try-failover master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:58:49.584 # +vote-for-leader c34ca410b69366f5ec74d2e0b9e93e0213c94245 3
5691:X 04 Apr 2022 22:58:49.584 # +elected-leader master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:58:49.584 # +failover-state-select-slave master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:58:49.687 # -failover-abort-no-good-slave master redis6379 127.0.0.1 6379
5691:X 04 Apr 2022 22:58:49.754 # Next failover delay: I will not start a failover before Mon Apr  4 23:04:49 2022

这里附上周阳老师操作的自动选举结果截图
在这里插入图片描述

选举成功后, 通过info replication查看状态, 6380被选举为新的master, 6381作为slave从机, 与6380建立了主从关系.
在这里插入图片描述

问题:如果之前的master(6379)重启回来,会不会双master冲突? 也就是6379会不会和6380冲突?

不会冲突,之前的master回来之后,哨兵会监测到,之前的master会变成slave

注意:

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

后台运行哨兵的配置

port 26379
daemonize no
pidfile /var/run/redis-sentinel.pid
logfile ""
dir /tmp
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
acllog-max-len 128
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
sentinel deny-scripts-reconfig yes
SENTINEL resolve-hostnames no
SENTINEL announce-hostnames no

6. Redis的Java客户端Jedis

6.1 测试连通性

创建SpringBoot项目, 导入Jedis相关的依赖.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.crys</groupId>
    <artifactId>boot-jedis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--jedis, springboot默认了jedis版本, jedis需要依赖commons-pool,已经自动依赖上了-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml添加Jedis连接的配置, 下面列出单机的配置, 也支持集群配置.

server:
  port: 8081
spring:
  redis:
    host: 192.168.65.129
    password: 123456
    port: 6379
    jedis:
      pool:
        max-idle: 50 # 连接池中最大空闲数
        max-active: 100 #  连接池中最大连接数
        min-idle: 10 # 连接池中最小空闲数
        max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
    timeout: 2000 # 连接超时时间

创建测试类, 测试Jedis操作Redis的连通性.

@RunWith(SpringRunner.class)
@SpringBootTest
public class JedisTest {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    @Test
    public void testJedis() {
        // 1.连接redis
        Jedis jedis = new Jedis(host, port);
        System.out.println(jedis);
        // 授权
        jedis.auth(password);
        // 2. 操作redis
        jedis.set("name", " crysw");
        System.out.println("name=" + jedis.get("name"));
        System.out.println("ping=" + jedis.ping());
        Set<String> keys = jedis.keys("*");
        System.out.println("keys * : " + keys);
        // 关闭连接
        jedis.close();
    }
}   

测试结果

redis.clients.jedis.Jedis@7e87ef9e
name= crysw
ping=PONG
keys * : [k3, name, k4, balance, debt, k1, k2]

6.2 Jedis常用API

Jedis的API使用, 和Redis的原生命令一模一样, 按照Redis命令使用就可以了.

@RunWith(SpringRunner.class)
@SpringBootTest
public class JedisApiTest {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    private Jedis jedis;

    @Before
    public void before() {
        jedis = new Jedis(host, port);
        jedis.auth(password);
    }

    @Test
    public void testKey() {
        Set<String> keys = jedis.keys("*");

        System.out.println("keys *: " + keys);

        System.out.println("jedis.exists====>" + jedis.exists("k2"));
        System.out.println("jedis.ttl: " + jedis.ttl("k1"));
    }

    @Test
    public void testString() {
        jedis.append("k1", ", myredis");
        System.out.println("jedis.get(k1): " + jedis.get("k1"));

        jedis.set("k4", "k4_redis");
        System.out.println("k4: " + jedis.get("k4"));

        jedis.mset("str1", "v1", "str2", "v2", "str3", "v3");
        List<String> mget = jedis.mget("str1", "str2", "str3");
        System.out.println("jedis.mget: " + mget);
    }

    @Test
    public void testList() {
        jedis.lpush("mylist", "v1", "v2", "v3", "v4", "v5");
        List<String> mylist = jedis.lrange("mylist", 0, -1);
        System.out.println("mylist: " + mylist);
    }

    @Test
    public void testSet() {
        jedis.sadd("orders", "jd001");
        jedis.sadd("orders", "jd002");
        jedis.sadd("orders", "jd003");
        Set<String> orders = jedis.smembers("orders");
        System.out.println("orders: " + orders);

        jedis.srem("orders", "jd002");
        Set<String> orders1 = jedis.smembers("orders");
        System.out.println("orders1: " + orders1);
    }

    @Test
    public void testHash() {
        jedis.hset("hash1", "userName", "lisi");
        System.out.println("username: " + jedis.hget("hash1", "userName"));

        Map<String, String> map = new HashMap<String, String>();
        map.put("telphone", "13811814763");
        map.put("address", "atguigu");
        map.put("email", "abc@163.com");
        jedis.hmset("hash2", map);
        List<String> hmget = jedis.hmget("hash2", "telphone", "email");
        System.out.println("hmget: " + hmget);
    }

    @Test
    public void testZset() {
        jedis.zadd("zset01", 60d, "v1");
        jedis.zadd("zset01", 70d, "v2");
        jedis.zadd("zset01", 80d, "v3");
        jedis.zadd("zset01", 90d, "v4");

        Set<String> zset01 = jedis.zrange("zset01", 0, -1);
        System.out.println("zset01: " + zset01);

        Set<String> zset011 = jedis.zrangeByScore("zset01", 60, 70);
        System.out.println("zset011: " + zset011);

    }
}

6.3 Jedis操作Redis事务

Jedis如何实现Redis事务的操作呢?

/**
 * 描述:测试Redis事务
 * @author crysw
 * @date 2022/4/5 17:45
 * @version 1.0
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestTx {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    private Jedis jedis;

    @Before
    public void before() {
        jedis = new Jedis(host, port);
        jedis.auth(password);
    }

    /**
     * watch命令就是标记一个键
     * 如果标记了一个键, 在提交事务前如果该键被别人修改过, 那事务就会失败, 这种情况通常可以在程序中重新再尝试一次.
     * 首先标记键balance, 然后检查余额是否足够:
     *                     不足就取消标记, 并不做余额的扣减;
     *                     足够, 就启动事务进行更新操作.
     * 如果在此期间键balance被其他人修改, 那在提交事务(执行exec)时就会报错, 程序中可以捕获这类错误再重新执行一次, 直到成功.
     *
     */
    @Test
    public void testTx() {

        boolean retValue = this.transMethod();
        System.out.println("retValue: " + retValue);
    }

    private boolean transMethod() {
        // 可用余额
        int balance;
        // 欠款
        int debt;
        // 实刷额度
        int amtToSubtract = 10;

        System.out.println("before modify, balance : " + jedis.get("balance"));
        System.out.println("before modify, debt : " + jedis.get("debt"));

        // 对可用余额添加监控
        jedis.watch("balance");
        // 该操作不应该出现, 为了方便, 模拟其他客户端修改了被监控的可用余额
        // jedis.set("balance", "5");
        // jedis.set("balance", "50");
        balance = Integer.parseInt(jedis.get("balance"));
        if (balance < amtToSubtract) {
            jedis.unwatch();
            System.out.println("the balance is not enough");
            return false;
        } else {
            System.out.println(">>>>>>transaction>>>>");
            // multi 开启事务
            Transaction transaction = jedis.multi();
            transaction.decrBy("balance", amtToSubtract);
            transaction.incrBy("debt", amtToSubtract);
            // exec 提交事务
            List<Object> exec = transaction.exec();
            System.out.println("exec: " + exec);
           	// 执行失败, 被监控的balance被其他程序修改了
            if (exec == null) {
                System.out.println("modified by others");
                return false;
            }
            balance = Integer.parseInt(jedis.get("balance"));
            debt = Integer.parseInt(jedis.get("debt"));
            System.out.println("balance: " + balance);
            System.out.println("debt: " + debt);
            jedis.unwatch();
            return true;
        }
    }
}

测试场景1, 可用余额不足. 上面的程序放开 jedis.set("balance", "5");

before modify, balance : 50
before modify, debt : 40
the balance is not enough # 余额不足, 取消监控
retValue: false

测试场景2, 被监控的balance对象被其他程序修改了, 事务执行失败. 上面的程序放开 jedis.set("balance", "50");

before modify, balance : 5
before modify, debt : 40
>>>>>>transaction>>>>
exec: null # 事务执行失败,返回null
modified by others
retValue: false

测试场景3, 事务执行成功, 余额扣减, 债务增加成功. 先将balace重置到50.

before modify, balance : 50
before modify, debt : 40
>>>>>>transaction>>>>
exec: [40, 50] # 事务执行成功, 返回执行结果; 并取消监控
balance: 40
debt: 50
retValue: true

6.4 Jedis操作主从复制

测试Jedis操作Redis的主从复制, 6379设置为master主机, 6380为其slave从机, 同步6379的数据.

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestMS {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    @Test
    public void testMS() {

        Jedis jedis_master = new Jedis(host, 6379);
        Jedis jedis_slave = new Jedis(host, 6380);
        jedis_master.auth(password);
        jedis_slave.auth(password);
		  // 6380建立与master63789的主从关系
        jedis_slave.slaveof(host, 6379);
  		  // 主机写数据
        jedis_master.set("class", "123456");
        // 从机读数据
        String result = jedis_slave.get("class");
        System.out.println("result: " + result);
    }
}

第一次执行测试用例, 发现slave读取的数据为null, 但是在CentOS的服务器上连接slave查询是已经同步了数据的, 这是因为程序在JVM内存中运行太快导致, Redis主从还没有同步完成, Java程序就执行完成了. 只要第二次执行slave就可以正常读取到master的数据了.

注意: 开始连接6380怎么操作都连接失败, 后面经过排查,发现是因为测试程序在Windows本机上, 而Redis服务在CentOS上, 需要开放对应的端口访问权限.

# 开放端口
firewall-cmd --permanent --zone=public --add-port=6380/tcp
# 查看端口开放情况
firewall-cmd --permanent --list-ports 
# 查看单个端口开放情况
firewall-cmd --permanent --query-port=6380/tcp
# 重启防火墙
firewall-cmd --reload

开放6380端口访问权限

[root@centos7-01 redis-6.x]# firewall-cmd --permanent --query-port=6380/tcp
no
[root@centos7-01 redis-6.x]# firewall-cmd --permanent --zone=public --add-port=6381/tcp
success
[root@centos7-01 redis-6.x]# firewall-cmd --permanent --zone=public --add-port=6380/tcp
success
[root@centos7-01 redis-6.x]# firewall-cmd --reload
success
[root@centos7-01 redis-6.x]# firewall-cmd --permanent --query-port=6380/tcp
yes

6.5 Jedis工具封装

JedisConfig配置类, 实例化JedisPool池.

@Configuration
@Slf4j
public class JedisConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.jedis.pool.max-wait}")
    private int maxWaitMillSeconds;

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }

    @Bean
    public JedisPool jedisPool(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillSeconds);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
        log.info("JedisPool连接成功: {}:{}",host,port);
        return jedisPool;
    }
}

获取Jedis的简易工具封装

@Component
public class JedisUtils {
    @Autowired
    private JedisPool jedisPool;

    @Value("${spring.redis.password}")
    private String password;

    /**
     * 获取Jedis资源
     * @return
     */
    public Jedis getJedis() {
        Jedis jedis = jedisPool.getResource();
        jedis.auth(password);
        return jedis;
    }

    /**
     * 关闭jedis连接
     * @param jedis
     */
    public void close(Jedis jedis) {
        if (jedis != null) jedis.close();
    }

    // 为什么要封装工具类? Redis有很多指令, Jedis操作它需要针对业务场景做方法封装
    public int calcTimeHour(int hours) {
        int seconds = hours * 60 * 60;
        return seconds;
    }
}
  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 23:15:00  更:2022-04-06 23:15:43 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:43:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码