这是本人根据黑马视频学习 Redis 的相关笔记,系列文章导航:《Redis设计与实现》笔记与汇总
单点 Redis 的问题
AOF
: Append Only File。Redis 处理的每一个写命令都会记录在 AOF 文件中,可以看作是命令日志文件。
目的是在同一台虚拟机中创建 3 个 redis 实例,模拟主从集群,如下:
IP | PORT | 角色 |
---|---|---|
192.168.137.112 | 7001 | master |
192.168.137.112 | 7002 | slave |
192.168.137.112 | 7003 | slave |
创建三个目录,分别存放一份配置文件,例如:
├── 7001 │ └── redis.conf ├── 7002 │ └── redis.conf └── 7003 └── redis.conf 3 directories, 3 files
修改每个配置文件的端口和工作目录,可以用 sed 快速完成:
sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/tmp\/7001\//g' 7001/redis.conf sed -i -e 's/6379/7002/g' -e 's/dir .\//dir \/tmp\/7002\//g' 7002/redis.conf sed -i -e 's/6379/7003/g' -e 's/dir .\//dir \/tmp\/7003\//g' 7003/redis.conf
修改每个实例的声明 IP:
虚拟机本身有多个 IP,为了避免将来混乱,我们需要在 redis.conf 文件中指定每一个实例的绑定 ip 信息,格式如下:
# redis实例的声明 IP replica-announce-ip 192.168.150.101
每个目录都要改,我们一键完成修改(在/tmp 目录执行下列命令):
# 逐一执行 sed -i '1a replica-announce-ip 192.168.137.112' 7001/redis.conf sed -i '1a replica-announce-ip 192.168.137.112' 7002/redis.conf sed -i '1a replica-announce-ip 192.168.137.112' 7003/redis.conf # 或者一键修改 printf '%s\n' 7001 7002 7003 | xargs -I{} -t sed -i '1a replica-announce-ip 192.168.137.112' {}/redis.conf
如果主节点设置了密码,则要在从节点的配置文件中添加如下配置:
masterauth [主节点密码]
当然练习的时候也可以把密码都删除了
启动:
# 第1个 redis-server 7001/redis.conf # 第2个 redis-server 7002/redis.conf # 第3个 redis-server 7003/redis.conf
设置主从关系:
在从节点上:
slaveof <master-ip> <master-port> # 或 5.0 之后 replicaof <master-ip> <master-port>
一键暂停:
printf '%s\n' 7001 7002 7003 | xargs -I{} -t redis-cli -p {} shutdown
可以看一下上图中的标记,日志记录了两者之间进行同步的信息
主从第一次同步为 全量同步:
Master 如何判断 Slave 是否是第一次来同步数据?
有几个概念,可以作为判断依据:
master 判断一个节点是否是第一次同步的依据,就是看 replid 是否一致。
哨兵的作用如下:
主观下线和客观下线:
Sentinel 基于心跳机制监测服务状态,每隔 1 秒向集群的每个实例发送 ping 命令:
•主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。
•客观下线:若超过指定数量(quorum)的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 Sentinel 实例数量的一半。
如何选举新的 master?
如何实现故障转移?
三个 sentinel 实例信息如下:
节点 | IP | PORT |
---|---|---|
s1 | 192.168.150.101 | 27001 |
s2 | 192.168.150.101 | 27002 |
s3 | 192.168.150.101 | 27003 |
创建目录
# 进入/tmp目录 cd /tmp # 创建目录 mkdir s1 s2 s3
在每个目录下创建配置文件,注意修改端口号(第一行)和文件夹路径(最后一行):
port 27001 sentinel announce-ip 192.168.150.101 sentinel monitor mymaster 192.168.150.101 7001 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 dir "/tmp/s1"
逐个启动
redis-sentinel sentinel.conf
sentinel
的日志:
redis-server
原来的从节点的日志:
7001 恢复后,自己变成从节点
这里我们会在同一台虚拟机中开启 6 个 redis 实例,模拟分片集群,信息如下:
IP | PORT | 角色 |
---|---|---|
192.168.150.101 | 7001 | master |
192.168.150.101 | 7002 | master |
192.168.150.101 | 7003 | master |
192.168.150.101 | 8001 | slave |
192.168.150.101 | 8002 | slave |
192.168.150.101 | 8003 | slave |
创建文件夹
# 进入/tmp目录 cd /tmp # 删除旧的,避免配置干扰 rm -rf 7001 7002 7003 # 创建目录 mkdir 7001 7002 7003 8001 8002 8003
准备一个配置文件
port 6379 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称,不需要我们创建,由redis自己维护 cluster-config-file /tmp/6379/nodes.conf # 节点心跳失败的超时时间 cluster-node-timeout 5000 # 持久化文件存放目录 dir /tmp/6379 # 绑定地址 bind 0.0.0.0 # 让redis后台运行 daemonize yes # 注册的实例ip replica-announce-ip 192.168.150.101 # 保护模式 protected-mode no # 数据库数量 databases 1 # 日志 logfile /tmp/6379/run.log
将这个文件拷贝到每个目录下
# 进入/tmp目录 cd /tmp # 执行拷贝 echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf
修改每个目录下的 redis.conf,将其中的 6379 修改为与所在目录一致
# 进入/tmp目录 cd /tmp # 修改配置文件 printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf
启动
# 进入/tmp目录 cd /tmp # 一键启动所有服务 printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf
创建集群
redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003
redis-cli --cluster
或者./redis-trib.rb
:代表集群操作命令create
:代表是创建集群--replicas 1
或者--cluster-replicas 1
:指定集群中每个 master 的副本个数为 1,此时节点总数 ÷ (replicas + 1)
得到的就是 master 的数量。因此节点列表中的前 n 个就是 master,其它节点都是 slave 节点,随机分配到不同 master查看集群状态
redis-cli -p 7001 cluster nodes
测试(集群测试时,要添加上 -c
参数)
redis-cli -c -p 7001
关闭
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown
原理方面可以参考 一致性哈希算法
Redis 会把每一个 master 节点映射到 0~16383 共 16384 个插槽(hash slot)上,查看集群信息时就能看到:
Redis 会根据 key 的有效部分计算插槽值,分两种情况:
案例:
>SET num 10那么就根据
num
计算,利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值,然后根据slot值确定要把数据插入到哪个 Redis 中
主要是增加新的节点,给它分配槽,或者删除节点的操作。
添加节点:
redis-cli --cluster add-node 192.168.150.101:7004 192.168.150.101:7001
通过命令查看集群状态:
redis-cli -p 7001 cluster nodes
默认是 master 节点,不分配插槽,因此没有任何数据可以存储到 7004 上
转移插槽:
[root@localhost] redis-cli --cluster reshard 192.168.137.112:7001
然后依次按照提示输入:
手动故障转移:
利用cluster failover
命令可以手动让集群中的某个 master 宕机,切换到执行 cluster failover 命令的这个 slave 节点,实现无感知的数据迁移。
这种 failover 命令可以指定三种模式:
例如,让 slave 变成 master,可以在 master 上执行:
RedisTemplate 底层同样基于 lettuce 实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
1)引入 redis 的 starter 依赖
2)配置分片集群地址
3)配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
spring: redis: cluster: nodes: - 192.168.150.101:7001 - 192.168.150.101:7002 - 192.168.150.101:7003 - 192.168.150.101:8001 - 192.168.150.101:8002 - 192.168.150.101:8003
导入数据库
创建 MySQL 数据库
docker run \ -p 3306:3306 \ --name mysql \ -v $PWD/conf:/etc/mysql/conf.d \ -v $PWD/logs:/logs \ -v $PWD/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=123 \ --privileged \ -d \ mysql
在/tmp/mysql/conf 目录添加一个 my.cnf 文件,作为 mysql 的配置文件:
# 创建文件 touch /tmp/mysql/conf/my.cnf
文件的内容如下:
[mysqld] skip-name-resolve character_set_server=utf8 datadir=/var/lib/mysql server-id=1000
重新启动一下 mysql
导入 SQL 数据
导入项目代码(略)
导入前端代码(略)
简单使用的案例:
public class CaffeineTest { @Test void testBasicOps() { Cache<String, String> cache = Caffeine.newBuilder().build(); cache.put("CAT", "TOM"); String gf = cache.getIfPresent("CAT"); System.out.println(gf); String dog = cache.get("DOG", key -> "WangWang"); System.out.println(dog); } }
三种缓存驱逐策略:
config/CaffeineConfig
:
@Configuration public class CaffeineConfig { @Bean public Cache<Long, Item> itemCache() { return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(10_000) .build(); } @Bean public Cache<Long, ItemStock> stockCache() { return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(10_000) .build(); } }
Controller:
:
@Autowired private Cache<Long, Item> itemCache; @Autowired private Cache<Long, ItemStock> stockCache; @GetMapping("/{id}") public Item findById(@PathVariable("id") Long id){ return itemCache.get(id, key -> itemService.query() .ne("status", 3).eq("id", key) .one()); } @GetMapping("/stock/{id}") public ItemStock findStockById(@PathVariable("id") Long id){ return stockCache.get(id, key -> stockService.getById(key)); }
此部分可以参考:Lua基础教程笔记
官网:OpenResty® - 中文官方站
OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
安装开发库
yum install -y pcre-devel openssl-devel gcc --skip-broken
安装 OpenResty 仓库
# 如果以前安装过 yum-utils 则跳过 # yum install -y yum-utils yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
安装
yum install -y openresty
安装 opm 工具
yum install -y openresty-opm
添加到环境变量
vi /etc/profile # 添加如下内容 export NGINX_HOME=/usr/local/openresty/nginx export PATH=${NGINX_HOME}/sbin:$PATH source /etc/profile
安装完成后:
其实是在 NGINX 的基础上做了增强,可以用之前学的 nginx 命令对其进行控制
这里有一份简化的配置文件:
#user nobody; worker_processes 1; error_log logs/error.log; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 8081; server_name localhost; location / { root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
让 OpenResty 可以处理请求:
nginx.conf
配置文件:
自定义脚本 nginx/lua/item.lua
:
ngx.say('{"name":"商品", "price":"100"}')
如果运行nginx失败,参考 解决 openresty Nginx 重启报错
这里拿第一种情况做一个测试:
nginx.conf
:
location ~ /api/item/(\d+) { default_type application/json; content_by_lua_file lua/item.lua; }
item.lua
:
-- 获取路径参数 local id = ngx.var[1] ngx.say('{"name":'.. id ..', "price":"100"}')
关于如何检查防火墙的状态,可以用
nmap
等工具关于如何关闭防火墙,可以参考 打开或关闭 Microsoft Defender 防火墙
首先修改一下 nginx 的配置:
location /item { proxy_pass http://192.168.137.1:8081; }
封装一个方法,用来进行通用的网络请求的发送:
openresty/lualib/common.js
:
-- 封装函数,发送http请求,并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息,返回404 ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http } return _M
然后编写具体的逻辑代码:
-- 导入 common 函数库 local common = require('common') local read_http = common.read_http -- 导入cjson模块 local cjson = require('cjson') -- 获取路径参数 local id = ngx.var[1] -- 查询商品信息 local itemJSON = read_http("/item/".. id, nil) -- 查询库存信息 local stockJSON = read_http("/item/stock/"..id, nil) -- JSON 转换 lua 的 table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- item 序列化为 json 返回结果 ngx.say(cjson.encode(item))
Tomcat 负载均衡:
实际应用场景中一般是用集群模式启动,所以我们模拟多台 Tomcat:
配置 nginx.conf
:
(部分)
upstream tomcat-cluster { hash $request_uri; server 192.168.137.1:8081; server 192.168.137.1:8082; } server { location /item { proxy_pass http://tomcat-cluster; } }
目的:让 OpenResty 优先查询 redis,然后再查询 tomcat
启动 redis (视频这里用了 docker 运行 redis)
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
在项目中引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置 redis 地址
spring: redis: host: 192.168.137.112 password: abc123
编写初始化类
实现了 InitializingBean
的类会在属性注入后执行,以实现预热的效果
@Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; @Autowired private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { List<Item> itemList = itemService.list(); List<ItemStock> stockLi = stockService.list(); for (Item item : itemList) { String s = MAPPER.writeValueAsString(item); redisTemplate.opsForValue().set("item:" + item.getId(), s); } for (ItemStock item : stockLi) { String s = MAPPER.writeValueAsString(item); redisTemplate.opsForValue().set("item:stock:" + item.getId(), s); } } }
首先需要能够连接上 redis:
openResty/lualib/common.lua
:添加如下功能
-- 导入 Redis local redis = require('resty.redis') -- 初始化 redis local red = redis:new() red:set_timeouts(1000, 1000, 1000) -- 关闭redis连接的工具方法,其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, "Put into redis connection Pool failed: ", err) end end -- 查询redis的方法 ip和port是redis地址,key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, "Connect redis Failed : ", err) return nil end red:auth('abc123') -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, "Query Redis Failed: ", err, ", key = " , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, "Query Redis Empty, key = ", key) end close_redis(red) return resp end -- 将方法导出 local _M = { read_redis = read_redis } return _M
- 关于如果redis没有密码,则无需
red:auth
那一行代码,参考 openresty 前端开发入门四之Redis篇- 关于日志的功能,参考 日志输出 - OpenResty 实践
openResty/nginx/lua/item.lua
:查询脚本的封装:
-- 封装查询函数 function read_data(key, path, param) local resp = read_redis("127.0.0.1", 6379, key) if not resp then -- 查询 http ngx.log(ngx.INFO, "redis 查询失败 , key : " , key) resp = read_http(path, param) end return resp end
具体的查询:
-- 查询商品信息 local itemJSON = read_data("item:"..id, "/item/".. id, nil) -- 查询库存信息 local stockJSON = read_data("item:stock:"..id, "/item/stock/"..id, nil)
-- 封装查询函数 function read_data(key, path, param, expire) -- 查询本地缓存 local resp = item_cache:get(key) if not resp then ngx.log(ngx.ERR, "Local Query Failed, key:", key) resp = read_redis("127.0.0.1", 6379, key) if not resp then -- 查询 http ngx.log(ngx.ERR, "redis Query Failed, key : " , key) resp = read_http(path, param) end -- 把数据写入缓存 item_cache:set(key, resp, expire) end return resp end
缓存数据同步的常见方式有三种:
暂略,可以参考网络文章
查看编码方式:
> object encoding XXX
查看内存占用:
> MEMORY USAGE name
一般用长度等来估计大小
危害:
网络阻塞
对 BigKey 执行读请求时,少量的 QPS 就可能导致带宽使用率被占满,导致 Redis 实例,乃至所在物理机变慢
数据倾斜
BigKey 所在的 Redis 实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis阻塞
对元素较多的 hash、list、zset 等做运算会耗时较旧,使主线程被阻塞
CPU压力
对 BigKey 的数据序列化和反序列化会导致 CPU 的使用率飙升,影响 Redis 实例和本机其它应用
公
寻找 BigKey:
Redis-Rdb-Tools
分析 RDB 快照文件,全面分析内存使用情况如何删除:
BigKey 内存占用较多,即便时删除这样的 key 也需要耗费很长时间,导致 Redis 主线程阻塞,引发一系列问题。
unlink
Redis 的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
用来做缓存的 Redis 实例尽量不要开启持久化功能
建议关闭 RDB 持久化功能,使用 AOF 持久化
利用脚本定期在 slave 节点做 RDB,实现数据备份
设置合理的 rewrite 阈值,避免频繁的 bgrewrite
配置 no-appendfsync-on-rewrite = yes,禁止在 rewrite 期间做 aof,避免因 AOF 引起的阻塞
部署有关建议:
参考这篇文章:Redis未授权访问配合SSH key文件利用分析
为了避免这样的漏洞,这里给出一些建议:
> client list
一个配置:
集群带宽问题:
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
建议:单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。