第五阶段 大型分布式系统缓存架构进阶
文章目录
- 第五阶段 大型分布式系统缓存架构进阶
第一部分 Redis 快速实战
第一节 缓存原理与设计
1.1 缓存基本思想
1.11 缓存的使用场景
DB 缓存,减轻 DB 服务器压力
一般情况下数据存在数据库中,应用程序直接操作数据库。
当访问量上万,数据库压力增大,可以采取的方案有: 读写分离,分库分表
当访问量达到 10 万、百万,需要引入缓存。 将已经访问过的内容或数据存储起来,当再次访问时先找缓存,缓存命中返回数据。 不命中再找数据库,并回填缓存。
提高系统响应
数据库的数据是存在文件里,也就是硬盘,与内存做交换(swap) 在大量瞬间访问时(高并发)MySQL 单机会因为频繁 IO 而造成无法响应。
MySQL 的 InnoDB 是有行锁 将数据缓存在 Redis 中,也就是存在了内存中。 内存天然支持高并发访问。可以瞬间处理大量请求。 qps 到达 11 万/S 读请求 8 万写/S
做 Session 分离
传统的 session 是由 tomcat 自己进行维护和管理。
集群或分布式环境,不同的 tomcat 管理各自的 session。 只能在各个 tomcat 之间,通过网络和 Io 进行 session 的复制,极大的影响了系统的性能。
1、各个 Tomcat 间复制 session,性能损耗
2、不能保证各个 Tomcat 的 Session 数据同步
将登录成功后的 Session 信息,存放在 Redis 中,这样多个服务器(Tomcat)可以共享 Session 信息。
Redis 的作用是数据的临时存储
做分布式锁(Redis)
一般讲锁是多线程的锁,是在一个进程中的 多个进程(JVM)在并发时也会产生问题,也要控制时序性 可以采用分布式锁。使用 Redis 实现 setNX
做乐观锁(Redis)
同步锁和数据库中的行锁、表锁都是悲观锁
悲观锁的性能是比较低的,响应性比较差
高性能、高响应(秒杀)采用乐观锁
Redis 可以实现乐观锁 watch + incr
1.12 什么是缓存?
缓存原指 CPU 上的一种高速存储器,它先于内存与 CPU 交换数据,速度很快 现在泛指存储在计算机上的原始数据的复制集,便于快速访问。 在互联网技术中,缓存是系统快速响应的关键技术之一 以空间换时间的一种技术(艺术)
1.13 大型网站中缓存的使用
单机架构 LAMP(Linux+apache+MySQL+PHP)、JavaEE(SSM)
访问量越大,响应力越差,用户体验越差
引入缓存、示意图如下:
在大型网站中从浏览器到网络,再到应用服务器,再到数据库,通过在各个层面应用缓存技术,大大提 升了系统性能和用户体验。
1.2 常见缓存的分类
1.21 客户端缓存
传统互联网:页面缓存和浏览器缓存
移动互联网:APP 缓存
页面缓存
页面缓存:页面自身对某些元素或全部元素进行存储,并保存成文件。
html5:Cookie、WebStorage(SessionStorage 和 LocalStorage)、WebSql、indexDB、Application Cache 等
开启步骤:
1、设置 manifest 描述文件
CACHE MANIFEST
#comment
js/index.js
img/bg.png
2、html 关联 manifest 属性
<html lang="en" manifest="demo.appcache">
使用 LocalStorage 进行本地的数据存储,示例代码:
localStorage.setItem("Name","张飞")
localStorage.getItem("Name")
localStorage.removeItem("Name")
localStorage.clear()
浏览器缓存
当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接 从浏览器缓存中提取而不是从原始服务器中提取这个资源。
浏览器缓存可分为强制缓存和协商缓存。
强制缓存:直接使用浏览器的缓存数据
条件:Cache-Control 的 max-age 没有过期或者 Expires 的缓存时间没有过期
<meta http-equiv="Cache-Control" content="max-age=7200" />
<meta http-equiv="Expires" content="Mon, 20 Aug 2010 23:00:00 GMT" />
协商缓存:服务器资源未修改,使用浏览器的缓存(304);反之,使用服务器资源(200)。
<meta http-equiv="cache-control" content="no-cache">
APP 缓存
原生 APP 中把数据缓存在内存、文件或本地数据库(SQLite)中。比如图片文件。
1.22 网络端缓存
通过代理的方式响应客户端请求,对重复的请求返回缓存中的数据资源。
Web 代理缓存
可以缓存原生服务器的静态资源,比如样式、图片等。
常见的反向代理服务器比如大名鼎鼎的 Nginx。
边缘缓存
边缘缓存中典型的商业化服务就是 CDN 了。
CDN 的全称是 Content Delivery Network,即内容分发网络。
CDN 通过部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度 和命中率。
CDN 的关键技术主要有内容存储和分发技术。现在一般的公有云服务商都提供 CDN 服务。
1.23 服务端缓存
服务器端缓存是整个缓存体系的核心。包括数据库级缓存、平台级缓存和应用级缓存。
数据库级缓存
数据库是用来存储和管理数据的。
MySQL 在 Server 层使用查询缓存机制。将查询后的数据缓存起来。
K-V 结构,Key:select 语句的 hash 值,Value:查询结果
InnoDB 存储引擎中的 buffer-pool 用于缓存 InnoDB 索引及数据块
平台级缓存
平台级缓存指的是带有缓存特性的应用框架。 比如:GuavaCache 、EhCache(二级缓存,硬盘)、OSCache(页面缓存)等。 部署在应用服务器上,也称为服务器本地缓存
应用级缓存(重点)
具有缓存功能的中间件:Redis、Memcached、EVCache(AWS)、Tair(阿里 、美团)等。 采用 K-V 形式存储。 利用集群支持高可用、高性能、高并发、高扩展。 分布式缓存
1.3 缓存的优势与代价
1.31 使用缓存的优势
提升用户体验
用户体验(User Experience):用户在使用产品过程中建立起来的一种纯主观感受。
缓存的使用可以提升系统的响应能力,大大提升了用户体验。
减轻服务器压力
客户端缓存、网络端缓存减轻应用服务器压力。
服务端缓存减轻数据库服务器的压力。
提升系统性能
系统性能指标:响应时间、延迟时间、吞吐量、并发用户数和资源利用率等。
缓存技术可以:
- 缩短系统的响应时间
- 减少网络传输时间和应用延迟时间
- 提高系统的吞吐量
- 增加系统的并发用户数
- 提高了数据库资源的利用率
1.32 使用缓存的代价
额外的硬件支出
缓存是一种软件系统中以空间换时间的技术
需要额外的磁盘空间和内存空间来存储数据
搭建缓存服务器集群需要额外的服务器
采用云服务器的缓存服务就不用额外的服务器了
阿里云(Tair、Redis),百度云(Redis),提供缓存服务
AWS 亚马逊云服务:EVCache
高并发缓存失效
在高并发场景下会出现缓存失效(缓存穿透、缓存雪崩、缓存击穿)
造成瞬间数据库访问量增大,甚至崩溃
缓存与数据库数据同步
缓存与数据库无法做到数据的实时同步
Redis 无法做到主从时时数据同步
缓存并发竞争
多个 redis 的客户端同时对一个 key 进行 set 值得时候由于执行顺序引起的并发问题
1.4 缓存的读写模式
缓存有三种读写模式
1.41 Cache Aside Pattern(常用)
Cache Aside Pattern(旁路缓存),是最经典的缓存 + 数据库读写模式。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存呢?
1、缓存的值是一个结构:hash、list,更新数据需要遍历 先遍历(耗时)后修改
2、懒加载,使用的时候才更新缓存,使用的时候才从 DB 中加载
也可以采用异步的方式填充缓存 开启一个线程 定时将 DB 的数据刷到缓存中
高并发脏读的三种情况
1、先更新数据库,再更新缓存
update 与 commit 之间,更新缓存,commit 失败 则 DB 与缓存数据不一致
2、先删除缓存,再更新数据库
update 与 commit 之间,有新的读,缓存空,读 DB 数据到缓存
数据是旧的数据 commit 后 DB 为新数据 则 DB 与缓存数据不一致
3、先更新数据库,再删除缓存(推荐)
update 与 commit 之间,有新的读,缓存空,读 DB 数据到缓存
数据是旧的数据 commit 后 DB 为新数据 则 DB 与缓存数据不一致
采用延时双删策略
1.42 Read/Write Through Pattern
应用程序只操作缓存,缓存操作数据库。
Read-Through(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入 缓存。(guavacache)
Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。 该种模式需要提供数据库的 handler,开发较为复杂。
1.43 Write Behind Caching Pattern
应用程序只更新缓存。 缓存通过异步的方式将数据批量或合并后更新到 DB 中 不能时时同步,甚至会丢数据
1.5 缓存架构的设计思路
缓存的整体设计思路包括:
1、多层次
分布式缓存宕机,本地缓存还可以使用
2、数据类型
简单数据类型
Value 是字符串或整数或二进制 Value 的值比较大(大于 100K) 只进行 setter 和 getter 可采用 Memcached Memcached 纯内存缓存,多线程 K-V
复杂数据类型
Value 是 hash、set、list、zset 需要存储关系,聚合,计算 可采用 Redis
3、要做集群
分布式缓存集群方案(Redis)
codis 哨兵 + 主从 RedisCluster
4、缓存的数据结构设计
1、与数据库表一致
数据库表和缓存是一一对应的
缓存的字段会比数据库表少一些
缓存的数据是经常访问的 用户表,商品表
2、与数据库表不一致
需要存储关系,聚合,计算等 比如某个用户的帖子、用户的评论。 以用户评论为例,DB 结构如下
ID | UID | PostTime | Content |
---|---|---|---|
1 | 1000 | 1547342000 | xxxxxxxxxx |
2 | 1000 | 1547342000 | xxxxxxxxxx |
3 | 1001 | 1547341030 | xxxxxxxxxx |
如果要取出 UID 为 1000 的用户的评论,原始的表的数据结构显然是不行的。
我们应做如下设计:
key:UID+ 时间戳(精确到天) 评论一般以天为计算单位 (uid+posttime)
value:Redis 的 Hash 类型。field 为 id 和 content (id+content)
expire:设置为一天
1.6 案例:设计拉勾首页缓存职位列表、热门职位
拉勾网(www.lagou.com),是国内的招聘门户网站,亿万级 PV,单机响应性能 QPS 万级。
首页分析: 职位时时变化,不能使用静态 html 采用模板技术,数据在服务端拿出,不能为空
数据不一定时时 架构图如下:
1、静态文件
在 nginx 中,放置静态文件,比如 css,js, 图片等
listen 80 default_server;
server_name localhost;
root /mnt/blog/;
location / {
}
#要缓存文件的后缀,可以在以下设置。
location ~ .*\.(gif|jpg|png|css|js)(.*) {
proxy_pass http://ip 地址:90;
proxy_redirect off;
proxy_set_header Host $host;
proxy_cache cache_one;
proxy_cache_valid 200 302 24h;
proxy_cache_valid 301 30d;
proxy_cache_valid any 5m;
expires 90d;
add_header wall "hello lagou.";
}
}
2、职位列表
数据特点: 固定数据,一次性读取
方案: 在服务器开启时一次性初始化(从 xml)到服务器本地缓存 采用 Guava Cache,Guava Cache 用于存储频繁使用的少量数据,支持高并发访问 也可以使用 JDK 的 CurrentHashMap,需要自行实现
3、热门职位
数据特点: 频繁变化,不必时时同步 但一定要有数据,不能为空
方案: 数据从服务层读取(dubbo),然后放到本地缓存中(Guava),如果出现超时或读取为空,则返回原 来本地缓存的数据。
注意:不同的客户端看到的数据有可能不一样。
4、数据回填
从 Dubbo 中读取数据时,先读取 Redis 集群的缓存,如果缓存命中则直接返回。 如果缓存不命中则返回本地缓存,不能直接读取数据库。 采用异步的形式从数据库刷入到缓存中。
5、热点策略
对于热点数据我们采用本地缓存策略,而不采用服务熔断策略,因为首页数据可以不准确,但不能不响 应。
第二节 Redis 简介和安装
2.1 Redis 简介
什么是 Redis
Redis(Remote Dictionary Server)远程字典服务器,是用 C 语言开发的一个开源的高性能键值对(key-value)内存数据库。
它提供了五种数据类型来存储值:字符串类型、散列类型、列表类型、集合类型、有序集合类型
它是一种 NoSQL 数据存储。
Redis 发展历史
2008 年,意大利的一家创业公司 Merzia 推出了一款基于 MySQL 的网站实时统计系统 LLOOGG ,然而没过多久该公司的创始人 Salvatore Sanfilippo ( antirez)便 对 MySQL 的性能感到失望,于是他决定亲自为 LLOOGG 量身定做一个数据库,并于 2009 年开发完成,这个数据库就是 Redis。
Redis2.6
Redis2.6 在 2012 年正式发布,主要特性如下:
服务端支持 Lua 脚本、去掉虚拟内存相关功能、键的过期时间支持毫秒、从节点提供只读功能、两个新的位图命令:bitcount 和 bitop、重构了大量的核心代码、优化了大量的命令。
Redis2.8
Redis2.8 在 2013 年 11 月 22 日正式发布,主要特性如下:
添加部分主从复制(增量复制)的功能、可以用 bind 命令绑定多个 IP 地址、Redis 设置了明显的进程名、发布订阅添加了 pubsub 命令、Redis Sentinel 生产可用
Redis3.0
Redis3.0 在 2015 年 4 月 1 日正式发布,相比于 Redis2.8 主要特性如下:Redis Cluster:Redis 的官方分布式实现(Ruby)、全新的对象编码结果、lru 算法大幅提升、部分命令的性能提升
Redis3.2
Redis3.2 在 2016 年 5 月 6 日正式发布,相比于 Redis3.0 主要特征如下:
添加 GEO 相关功能、SDS 在速度和节省空间上都做了优化、新的 List 编码类型:quicklist、从节点读取过期数据保证一致性、Lua 脚本功能增强等
Redis4.0
Redis4.0 在 2017 年 7 月发布,主要特性如下:
提供了模块系统,方便第三方开发者拓展 Redis 的功能、PSYNC2.0:优化了之前版本中,主从节点切换必然引起全量复制的问题、提供了新的缓存剔除算法:LFU(Last Frequently Used),并对已有算法进行了优化、提供了 RDB-AOF 混合持久化格式等
Redis 应用场景
缓存使用,减轻 DB 压力
DB 使用,用于临时存储数据(字典表,购买记录)
解决分布式场景下 Session 分离问题(登录信息)
任务队列(秒杀、抢红包等等) 乐观锁
应用排行榜 zset
签到 bitmap
分布式锁
冷热数据交换
2.2 Redis 单机版安装和使用
Redis 下载
官网地址:http://redis.io/
中文官网地址:http://www.redis.cn/
下载地址:http://download.redis.io/releases/
Redis 安装环境
Redis 没有官方的 Windows 版本,所以建议在 Linux 系统上安装运行。
我们使用 CentOS 7 作为安装环境。
具体操作如下
#第一步:安装 C 语言需要的 GCC 环境
yum install -y gcc-c++
yum install -y wget
#第二步:下载并解压缩 Redis 源码压缩包
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
tar -zxf redis-5.0.5.tar.gz
#第三步:编译 Redis 源码,进入 redis-5.0.5 目录,执行编译命令
cd redis-5.0.5/src
make
#第四步:安装 Redis,需要通过 PREFIX 指定安装路径
mkdir /usr/redis -p
make install PREFIX=/usr/redis
#复制conf
cp redis.conf /usr/redis/bin/
#修改redis.conf
vim redis.conf
#注释掉则不限于本机器访问
#bind 127.0.0.1
#no 允许外界访问
protected-mode no
#yes 守护进程后台启动
daemonize yes
#占用窗口启动 --不可以被远程访问
./redis-server
#关闭
control + c
#不占用窗口启动 --不可以被远程访问
./redis-server &
#守护进程后台启动 --可以被远程访问
./redis-server redis.conf
#守护进程后台启动关闭
./redis-cli shutdown
#查看进程
ps -ef |grep redis
root 7891 1977 0 17:14 pts/1 00:00:04 ./redis-server *:6379
#关闭进程
kill -9 7891
#客户端启动
./redis-cli -h 127.0.0.1 -p 6379
# 关闭centos的防火墙
#systemctl stop firewalld
# 设置centos防火墙不开机自启动
#systemctl disable firewalld.service
Redis 启动
前端启动
启动命令: redis-server ,直接运行 bin/redis-server 将以前端模式启动
#启动
./redis-server
#关闭
control + c
启动缺点:客户端窗口关闭则 redis-server 程序结束,不推荐使用此方法
后端启动(守护进程启动)
第一步:拷贝 redis-5.0.5/redis.conf 配置文件到 Redis 安装目录的 bin 目录
#复制conf
cp redis.conf /usr/redis/bin/
#修改redis.conf
vim redis.conf
修改内容
# 将`daemonize`由`no`改为`yes`
daemonize yes
# 默认绑定的是回环地址,默认不能被其他机器访问
# bind 127.0.0.1
# 是否开启保护模式,由yes该为no
protected-mode no
第三步:启动服务
./redis-server redis.conf
后端启动的关闭方式
./redis-cli shutdown
命令说明
redis-server :启动 redis 服务
redis-cli :进入 redis 命令客户端
redis-benchmark : 性能测试的工具
redis-check-aof : aof 文件进行检查的工具
redis-check-dump : rdb 文件进行检查的工具
redis-sentinel : 启动哨兵监控服务
Redis 命令行客户端
命令格式
./redis-cli -h 127.0.0.1 -p 6379
参数说明
-h:redis服务器的ip地址
-p:redis实例的端口号
默认方式
#默认主机地址是 127.0.0.1
#默认端口是 6379
./redis-cli
2.3 Redis 客户端访问
2.3.1 Java 程序访问 Redis
采用 jedis API 进行访问即可
1、关闭 RedisServer 端的防火墙
systemctl stop firewalld(默认)
systemctl disable firewalld.service(设置开启不启动)
2、新建 maven 项目后导入 Jedis 包
pom.xml
<?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.galaxy</groupId>
<artifactId>jedis-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<target>11</target>
<source>11</source>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
3、写程序
package test;
import redis.clients.jedis.Jedis;
/**
* @author lane
* @date 2021年07月28日 下午6:13
*/
public class JedisTest {
public static void main(String[] args) {
//与Redis建立连接 IP+port
Jedis jedis = new Jedis("172.16.94.13", 6379);
//在Redis中写字符串 key value
jedis.set("name", "zhangfei");
//获得Redis中字符串的值
System.out.println(jedis.get("name"));
//在Redis中写list
jedis.lpush("list1", "1", "2", "3");
//获得list的长度
System.out.println(jedis.llen("list1"));
}
}
4、输出结果
zhangfei
2.3.2 Spring 访问 Redis
1、新建一个 maven 项目
2、导入 Spring 的核心依赖
<?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.galaxy</groupId>
<artifactId>spring-redis-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<spring.version>5.2.5.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
</dependencies>
</project>
3、添加 Spring 配置文件
redis.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
<!-- <context:property-placeholder location="classpath:redis.properties"/>-->
<!-- redis config -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxActive" value="${redis.pool.maxActive}" />
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<property name="maxWait" value="${redis.pool.maxWait}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.server}"/>
<property name="port" value="${redis.port}"/>
<property name="timeout" value="${redis.timeout}" /><property name="poolConfig" ref="jedisPoolConfig" />
</bean>
<bean id="redisTemplate"
class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<property name="KeySerializer">
<bean
class="org.springframework.data.redis.serializer.StringRedisSerializer">
</bean>
</property>
<property name="ValueSerializer">
<bean
class="org.springframework.data.redis.serializer.StringRedisSerializer">
</bean>
</property>
</bean>
</beans>
4、添加 properties 文件
redis.properties
redis.pool.maxActive=100
redis.pool.maxIdle=50
redis.pool.maxWait=1000
redis.pool.testOnBorrow=true
redis.timeout=50000
redis.server=172.16.94.13
redis.port=6379
5、编写测试用例
package test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.Serializable;
/**
* @author lane
* @date 2021年07月28日 下午6:53
*/
//@RunWith(SpringJUnit4ClassRunner.class)
@RunWith(SpringRunner.class)
@ContextConfiguration("classpath:redis.xml")
public class RedisTest {
@Autowired
private RedisTemplate<Serializable, Serializable> rt;
@Test
public void testRedis(){
rt.opsForValue().set("spring","spring-redis");
System.out.println(rt.opsForValue().get("spring"));
}
}
测试结果
spring-redis
2.3.3 Spring boot 访问 Redis
新建 spring boot 项目
添加 redis 依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
完整版依赖 pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.galaxy</groupId>
<artifactId>springboot-redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-redis-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加配置文件 application.yml
spring:
redis:
host: 172.16.94.13
port: 6379
jedis:
pool:
max-active: 80
max-idle: 8
max-wait: 30000
min-idle: 0
timeout: 3000
添加配置类 RedisConfig
package com.galaxy.redisDemo.cache;
import org.springframework.beans.factory.annotation.Autowired;
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.StringRedisSerializer;
/**
* @author lane
* @date 2021年07月29日 上午10:46
*/
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String,Object> redisTemplate(){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
添加 controller
package com.galaxy.redisDemo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @author lane
* @date 2021年07月29日 上午10:50
*/
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@GetMapping("/put")
public String putKey(@RequestParam(required = true) String key,
@RequestParam(required = true) String value){
redisTemplate.opsForValue().set(key,value,20, TimeUnit.SECONDS);
return "success";
}
@GetMapping("/get")
public String getValue(@RequestParam(required = true) String key){
Object o = redisTemplate.opsForValue().get(key);
return o==null?"empty":o.toString();
}
}
修改 Application 并运行
package com.galaxy.redisDemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SpringbootRedisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRedisDemoApplication.class, args);
}
}
测试
输入 http://localhost:8080/redis/put?key=name2&value=google
127.0.0.1:6379> get name2
"google"
127.0.0.1:6379> ttl name2
(integer) 11
2.3.4 Spring boot 访问 redis 集群
这个是后面添加集群之后才写的
Redis 集群机器
172.16.94.13:7010,172.16.94.13:7011,172.16.94.13:7012,172.16.94.13:7020,
172.16.94.13:7021,172.16.94.13:7022,172.16.94.13:7030,172.16.94.13:7031,
172.16.94.13:7032,172.16.94.13:7101,172.16.94.13:7102
123
添加依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.galaxy</groupId>
<artifactId>lane-jediscluster-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>lane-jediscluster-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置信息
spring:
redis:
cluster:
nodes: 172.16.94.13:7010,172.16.94.13:7011,172.16.94.13:7012,172.16.94.13:7020,172.16.94.13:7021,172.16.94.13:7022,172.16.94.13:7030,172.16.94.13:7031,172.16.94.13:7032,172.16.94.13:7101,172.16.94.13:7102
jedis:
pool:
max-active: 80
max-idle: 8
max-wait: 30000
min-idle: 0
time-between-eviction-runs:
timeout: 3000
测试类
package com.galaxy.lanejedisclusterdemo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.util.HashSet;
import java.util.Set;
@SpringBootTest
class LaneJedisclusterDemoApplicationTests {
@Test
public void testJedisCluster()throws Exception{
//创建jedisCluster对象,有一个参数 nodes是Set类型,Set包含若干个HostAndPort对象
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("172.16.94.13",7010));
nodes.add(new HostAndPort("172.16.94.13",7020));
nodes.add(new HostAndPort("172.16.94.13",7030));
nodes.add(new HostAndPort("172.16.94.13",7011));
nodes.add(new HostAndPort("172.16.94.13",7012));
nodes.add(new HostAndPort("172.16.94.13",7021));
nodes.add(new HostAndPort("172.16.94.13",7022));
nodes.add(new HostAndPort("172.16.94.13",7031));
nodes.add(new HostAndPort("172.16.94.13",7032));
nodes.add(new HostAndPort("172.16.94.13",7101));
nodes.add(new HostAndPort("172.16.94.13",7102));
JedisCluster jedisCluster = new JedisCluster(nodes);
//使用jedisCluster操作redis
jedisCluster.set("hello", "jediscluster");
String str = jedisCluster.get("hello");
System.out.println(str);
//关闭连接池
jedisCluster.close();
}
}
测试效果
172.16.94.13:7030> get hello
-> Redirected to slot [866] located at 172.16.94.13:7011
"jediscluster"
2.4 Redis 数据类型选择和应用场景
Redis 是一个 Key-Value 的存储系统,使用 ANSI C 语言编写。
key 的类型是字符串。
value 的数据类型有:
常用的:string 字符串类型、list 列表类型、set 集合类型、sortedset(zset)有序集合类型、hash 类型。
不常见的:bitmap 位图类型、geo 地理位置类型。
Redis5.0 新增一种:stream 类型
注意:Redis 中命令是忽略大小写,(set SET),key 是不忽略大小写的 (NAME name)
2.4.1 Redis 的 Key 的设计
- 用:分割
- 把表名转换为 key 前缀, 比如 user:
- 第二段放置主键值
- 第三段放置列名
比如:用户表 user, 转换为 redis 的 key-value 存储
userid | username | password | |
---|---|---|---|
9 | zhangfei | 123 | 1@qq.com |
username 的 key: user:9:username
json 类型存储 {userid:9,username:zhangfei}
email 的 key user:9:email
表示明确:看 key 知道意思 不易被覆盖
2.4.2 String 字符串类型
Redis 的 String 能表达 3 种值的类型:字符串、整数、浮点数 100.01 是个六位的串
常见操作命令如下表:
命令名称 | 命令描述 | |
---|---|---|
set | set key value | 赋值 |
get | get key | 取值 |
getset | getset key value | 取值并赋值 |
setnx | setnx key value | 当 key 不存在时才用赋值 set key value NX PX 3000 原子操作,px 设置毫秒数 |
append | append key value | 向尾部追加值 |
strlen | strlen key | 获取字符串长度 |
incr | incr key | 递增数字 |
incrby | incrby key increment | 增加指定的整数 |
decr | decr key | 递减数字 |
decrby | decrby key decrement | 减少指定的整数 |
应用场景
1、key 和命令是字符串
2、普通的赋值
3、incr 用于乐观锁 incr:递增数字,可用于实现乐观锁 watch(事务)
4、setnx 用于分布式锁 当 value 不存在时采用赋值,可用于实现分布式锁
命令操作演示
127.0.0.1:6379> set weather cloudy
OK
127.0.0.1:6379> get weather
"cloudy"
127.0.0.1:6379> getset weather sunny
"cloudy"
127.0.0.1:6379> setnx weather rainy #没有值则成功,有则失败
(integer) 0
127.0.0.1:6379> get weather
"sunny"
127.0.0.1:6379> setnx sky blue
(integer) 1
127.0.0.1:6379> get sky
"blue"
127.0.0.1:6379> set sky white nx px 20000 #没有值则成功,有则失败,过期时间20s
(nil)
127.0.0.1:6379> set cloud white nx px 20000
OK
127.0.0.1:6379> set cloud white1 nx px 20000
(nil)
127.0.0.1:6379> set cloud white1 nx px 20000
OK
127.0.0.1:6379> get cloud
"white1"
127.0.0.1:6379> append tree green
(integer) 5
127.0.0.1:6379> get tree
"green"
127.0.0.1:6379>
127.0.0.1:6379> strlen tree
(integer) 5
127.0.0.1:6379> incr wind
(integer) 1
127.0.0.1:6379> incr wind
(integer) 2
127.0.0.1:6379> incrby wind 10
(integer) 12
127.0.0.1:6379> decr wind
(integer) 11
127.0.0.1:6379> decrby wind 5
(integer) 6
127.0.0.1:6379>
2.4.3 List 列表类型
list 列表类型可以存储有序、可重复的元素
获取头部或尾部附近的记录是极快的,采用双端列表
list 的元素个数最多为 2^32-1 个(40 亿)
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
lpush | lpush key v1 v2 v3 … | 从左侧插入列表 |
lpop | lpop key | 从列表左侧取出 |
rpush | rpush key v1 v2 v3 … | 从右侧插入列表 |
rpop | rpop key | 从列表右侧取出 |
lpushx | lpushx key value | 将值插入到列表头部 |
rpushx | rpushx key value | 将值插入到列表尾部 |
blpop | blpop key timeout | 从列表左侧取出,当列表为空时阻塞,可以设置最大阻塞时 间,单位为秒 |
brpop | blpop key timeout | 从列表右侧取出,当列表为空时阻塞,可以设置最大阻塞时 间,单位为秒 |
llen | llen key | 获得列表中元素个数 |
lindex | lindex key index | 获得列表中下标为 index 的元素 index 从 0 开始 |
lrange | lrange key start end | 返回列表中指定区间的元素,区间通过 start 和 end 指定 |
lrem | lrem key count value | 删除列表中与 value 相等的元素 当 count>0 时, lrem 会从列表左边开始删除;当 count<0 时, lrem 会从列表后边开始删除;当 count=0 时, lrem 删除所有值 为 value 的元素 |
lset | lset key index value | 将列表 index 位置的元素设置成 value 的值 |
ltrim | ltrim key start end | 对列表进行修剪,只保留 start 到 end 区间 |
rpoplpush | rpoplpush key1 key2 | 从 key1 列表右侧弹出并插入到 key2 列表左侧 |
brpoplpush | brpoplpush key1 key2 | 从 key1 列表右侧弹出并插入到 key2 列表左侧,会阻塞 |
linsert | linsert key BEFORE/AFTER pivot value | 将 value 插入到列表,且位于值 pivot 之前或之后 |
应用场景:
1、作为栈或队列使用
列表有序可以作为栈和队列使用
2、可用于各种列表,比如用户列表、商品列表、评论列表等。
举例:
127.0.0.1:6379> lpush list:my 1 2 3 4 5
(integer) 5
127.0.0.1:6379> lpop list:my
"5"
127.0.0.1:6379> lpop list:my
"4"
127.0.0.1:6379> rpush list:my 11 13 15
(integer) 6
127.0.0.1:6379> rpop list:my
"15"
127.0.0.1:6379> lrange list:my 0 3
1) "3"
2) "2"
3) "1"
4) "11"
127.0.0.1:6379> lpushx list:my f
(integer) 6
127.0.0.1:6379> lrange list:my 0 -1
1) "f"
2) "3"
3) "2"
4) "1"
5) "11"
6) "13"
127.0.0.1:6379> rpushx list:my l
(integer) 7
127.0.0.1:6379> lrange list:my 0 -1
1) "f"
2) "3"
3) "2"
4) "1"
5) "11"
6) "13"
7) "l"
127.0.0.1:6379> blpop list:my 20
1) "list:my"
2) "f"
127.0.0.1:6379> llen list:my
(integer) 6
127.0.0.1:6379> lindex list:my 3
"11"
127.0.0.1:6379> lrem list:my -1 11
(integer) 1
127.0.0.1:6379> lrange list:my 0 -1
1) "3"
2) "2"
3) "1"
4) "13"
5) "l"
127.0.0.1:6379> lset list:my 3 5
OK
127.0.0.1:6379> lrange list:my 0 -1
1) "3"
2) "2"
3) "1"
4) "5"
5) "l"
127.0.0.1:6379> ltrim list:my 1 3
OK
127.0.0.1:6379> lrange list:my 0 -1
1) "2"
2) "1"
3) "5"
127.0.0.1:6379> linsert list:my before 5 6
(integer) 4
127.0.0.1:6379> lrange list:my 0 -1
1) "2"
2) "1"
3) "6"
4) "5"
127.0.0.1:6379>
2.4.4 Set 集合类型
Set:无序、唯一元素
集合中最大的成员数为 2^32 - 1
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
sadd | sadd key mem1 mem2 … | 为集合添加新成员 |
srem | srem key mem1 mem2 … | 删除集合中指定成员 |
smembers | smembers key | 获得集合中所有元素 |
spop | spop key | 返回集合中一个随机元素,并将该元素删除 |
srandmember | srandmember key | 返回集合中一个随机元素,不会删除该元素 |
scard | scard key | 获得集合中元素的数量 |
sismember | sismember key member | 判断元素是否在集合内 |
sinter | sinter key1 key2 key3 | 求多集合的交集 |
sdiff | sdiff key1 key2 key3 | 求多集合的差集 |
sunion | sunion key1 key2 key3 | 求多集合的并集 |
应用场景:
适用于不能重复的且不需要顺序的数据结构
比如:关注的用户,还可以通过 spop 进行随机抽奖
操作演示
127.0.0.1:6379> sadd myset a b c
(integer) 3
127.0.0.1:6379> smembers myset
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> srem myset a
(integer) 1
127.0.0.1:6379> spop myset
"c"
127.0.0.1:6379> srandmember myset
"b"
127.0.0.1:6379> scard myset
(integer) 1
127.0.0.1:6379> sismember b
(error) ERR wrong number of arguments for 'sismember' command
127.0.0.1:6379> sismember myset b
(integer) 1
127.0.0.1:6379> smembers myset
1) "b"
127.0.0.1:6379> sadd myset2 b c d
(integer) 3
127.0.0.1:6379> sinter myset myset2
1) "b"
127.0.0.1:6379> sdiff myset myset2
(empty list or set)
127.0.0.1:6379> sunion myset myset2
1) "b"
2) "c"
3) "d"
127.0.0.1:6379>
2.4.5 ZSet 有序集合类型
SortedSet(ZSet) 有序集合: 元素本身是无序不重复的
每个元素关联一个分数(score)
可按分数排序,分数可重复
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
zadd | zadd key score1 member1 score2 member2 … | 为有序集合添加新成员 |
zrem | zrem key mem1 mem2 … | 删除有序集合中指定成员 |
zcard | zcard key | 获得有序集合中的元素数量 |
zcount | zcount key min max | 返回集合中 score 值在[min,max]区间 的元素数量 |
zincrby | zincrby key increment member | 在集合的 member 分值上加 increment |
zscore | zscore key member | 获得集合中 member 的分值 |
zrank | zrank key member | 获得集合中 member 的排名(按分值从 小到大) |
zrevrank | zrevrank key member | 获得集合中 member 的排名(按分值从 大到小) |
zrange | zrange key start end | 获得集合中指定区间成员,按分数递增 排序 |
zrevrange | zrevrange key start end | 获得集合中指定区间成员,按分数递减 排序 |
应用场景:
由于可以按照分值排序,所以适用于各种排行榜。比如:点击排行榜、销量排行榜、关注排行榜等。
操作演示
127.0.0.1:6379> zadd myscore 90 english 80 chemistry 70 geography
(integer) 3
127.0.0.1:6379> zadd yourscore 91 english 81 chemistry 71 geography
(integer) 3
127.0.0.1:6379> zadd hisscore 92 english 82 chemistry 72 geography
(integer) 3
127.0.0.1:6379> zrank myscore english
(integer) 2
127.0.0.1:6379> zrevrank myscore english
(integer) 0
127.0.0.1:6379> zrange yourscore 0 -1
1) "geography"
2) "chemistry"
3) "english"
127.0.0.1:6379> zrevrange yourscore 0 -1
1) "english"
2) "chemistry"
3) "geography"
127.0.0.1:6379> zscore myscore english
"90"
127.0.0.1:6379> zincrby myscore 10 english
"100"
127.0.0.1:6379> zcount myscore 80 100
(integer) 2
127.0.0.1:6379> zcard myscore
(integer) 3
127.0.0.1:6379> zrem myscore english
(integer) 1
127.0.0.1:6379> zrange myscore 0 -1
1) "geography"
2) "chemistry"
127.0.0.1:6379>
2.4.6 Hash 类型(散列表)
Redis hash 是一个 string 类型的 field 和 value 的映射表,它提供了字段和字段值的映射。
每个 hash 可以存储 2^32 - 1 键值对(40 多亿)。
常见操作命令
如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
hset | hset key field value | 赋值,不区别新增或修改 |
hmset | hmset key field1 value1 field2 value2 | 批量赋值 |
hsetnx | hsetnx key field value | 赋值,如果 filed 存在则不操作 |
hexists | hexists key filed | 查看某个 field 是否存在 |
hget | hget key field | 获取一个字段值 |
hmget | hmget key field1 field2 … | 获取多个字段值 |
hgetall | hgetall key | |
hdel | hdel key field1 field2… | 删除指定字段 |
hincrby | hincrby key field increment | 指定字段自增 increment |
hlen | hlen key | 获得字段数量 |
应用场景
对象的存储 ,表数据的映射
127.0.0.1:6379> hmset user:001 username xiaozhi password 1234 age 10 sex m
OK
127.0.0.1:6379> hset user:002 usename xiaoxia
(integer) 1
127.0.0.1:6379> hsetnx user:002 username xiaoxia2
(integer) 1
127.0.0.1:6379> hexists user:001 username
(integer) 1
127.0.0.1:6379> hget user:001 username
"xiaozhi"
127.0.0.1:6379> hmget user:001 username
1) "xiaozhi"
127.0.0.1:6379> hmget user:001 username password age
1) "xiaozhi"
2) "1234"
3) "10"
127.0.0.1:6379> hgetall user:001
1) "username"
2) "xiaozhi"
3) "password"
4) "1234"
5) "age"
6) "10"
7) "sex"
8) "m"
127.0.0.1:6379> hdel user:001 sex
(integer) 1
127.0.0.1:6379> hincrby user:001 password 10
(integer) 1244
127.0.0.1:6379> hlen user:001
(integer) 3
127.0.0.1:6379>
2.4.7 Bitmap 位图类型
bitmap 是进行位操作的
通过一个 bit 位来表示某个元素对应的值或者状态,其中的 key 就是对应元素本身。
bitmap 本身会极大的节省储存空间。
应用场景:
1、用户每月签到,用户 id 为 key , 日期作为偏移量 1 表示签到
2、统计活跃用户, 日期为 key,用户 id 为偏移量 1 表示活跃
3、查询用户在线状态, 日期为 key,用户 id 为偏移量 1 表示在线
常见操作命令如下表:
命令名 称 | 命令格式 | 描述 |
---|---|---|
setbit | setbit key offset value | 设置 key 在 offset 处的 bit 值(只能是 0 或者 1)。 |
getbit | getbit key offset | 获得 key 在 offset 处的 bit 值 |
bitcount | bitcount key | 获得 key 的 bit 位为 1 的个数 |
bitpos | bitpos key value | 返回第一个被设置为 bit 值的索引值 |
bitop | bitop and[or/xor/not] destkey key [key …] | 对多个 key 进行逻辑运算后存入 destkey 中 |
操作演示
127.0.0.1:6379> setbit user:sign:1000 20200101 1 #id为1000的用户20200101签到
(integer) 0
127.0.0.1:6379> setbit user:sign:1000 20200103 1 #id为1000的用户20200103签到
(integer) 0
127.0.0.1:6379> getbit user:sign:1000 20200101 #获得id为1000的用户20200101签到状态 1 表示签到
(integer) 1
127.0.0.1:6379> getbit user:sign:1000 20200102 #获得id为1000的用户20200102签到状态 0表示未签到
(integer) 0
127.0.0.1:6379> bitcount user:sign:1000 # 获得id为1000的用户签到次数
(integer) 2
127.0.0.1:6379> bitpos user:sign:1000 1 #id为1000的用户第一次签到的日期
(integer) 20200101
127.0.0.1:6379> setbit 20200201 1000 1 #20200201的1000号用户上线
(integer) 0
127.0.0.1:6379> setbit 20200202 1001 1 #20200202的1000号用户上线
(integer) 0
127.0.0.1:6379> setbit 20200201 1002 1 #20200201的1002号用户上线
(integer) 0
127.0.0.1:6379> bitcount 20200201 #20200201的上线用户有2个
(integer) 2
127.0.0.1:6379> bitop or desk1 20200201 20200202 #合并20200201的用户和20200202上线 了的用户
(integer) 126
127.0.0.1:6379> bitcount desk1 #统计20200201和20200202都上线的用 户个数
(integer) 3
2.4.8 Geo 地理位置类型
geo 是 Redis 用来处理位置信息的。在 Redis3.2 中正式使用。主要是利用了 Z 阶曲线、Base32 编码和 geohash 算法
Z 阶曲线
在 x 轴和 y 轴上将十进制数转化为二进制数,采用 x 轴和 y 轴对应的二进制数依次交叉后得到一个六位数编 码。
把数字从小到大依次连起来的曲线称为 Z 阶曲线,Z 阶曲线是把多维转换成一维的一种方法。
Base32 编码
Base32 这种数据编码机制,主要用来把二进制数据编码成可见的字符串,
其编码规则是:任意给定一 个二进制数据,以 5 个位(bit)为一组进行切分(base64 以 6 个位(bit)为一组),对切分而成的每个组进行编 码得到 1 个可见字符。
Base32 编码表字符集中的字符总数为 32 个(0-9、b-z 去掉 a、i、l、o),这也是 Base32 名字的由来。
geohash 算法 Gustavo 在 2008 年 2 月上线了 geohash.org 网站。**Geohash 是一种地理位置信息编码方法。 经过 geohash 映射后,地球上任意位置的经纬度坐标可以表示成一个较短的字符串。可以方便的存储在数据 库中,附在邮件上,以及方便的使用在其他服务中。**以北京的坐标举例,[39.928167,116.389550]可以 转换成 wx4g0s8q3jf9 。
Redis 中经纬度使用 52 位的整数进行编码,放进 zset 中,zset 的 value 元素是 key,score 是 GeoHash 的 52 位整数值。在使用 Redis 进行 Geo 查询时,其内部对应的操作其实只是 zset(skiplist)的操作。通过 zset 的 score 进行排序就可以得到坐标附近的其它元素,通过将 score 还原成坐标值就可以得到元素的原始坐 标。
应用场景:
1、记录地理位置
2、计算距离
3、查找"附近的人"
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
geoadd | geoadd key 经度 纬度 成员名称 1 经度 1 纬度 1 成员名称 2 经度 2 纬度 2 … | 添加地理坐标 |
geohash | geohash key 成员名称 1 成员名称 2… | 返回标准的 geohash 串 |
geopos | geopos key 成员名称 1 成员名称 2… | 返回成员经纬度 |
geodist | geodist key 成员 1 成员 2 单位 | 计算成员间距离 |
georadiusbymember | georadiusbymember key 成员 值单位 count 数 asc[desc] | 根据成员查找附近 的成员 |
操作演示
127.0.0.1:6379> geoadd myloc 116.31 40.05 beijing 116.38 40.08 tianjin
(integer) 2
127.0.0.1:6379> geohash myloc beijing tianjin
1) "wx4eydyk5m0"
2) "wx4u0236ft0"
127.0.0.1:6379> geopos myloc beijing tianjin
1) 1) "116.31000012159347534"
2) "40.04999982043828055"
2) 1) "116.38000041246414185"
2) "40.08000078008021916"
127.0.0.1:6379> geodist myloc beijing tianjin km
"6.8294"
127.0.0.1:6379> georadiusbymember myloc beijing 20 km withcoord withdist count 3 asc
# 获得距离beijing 20km以内的按由近到远的顺序排出前三名的成员名称、距离及经纬度
#withcoord : 获得经纬度 withdist:获得距离 withhash:获得geohash码
1) 1) "beijing"
2) "0.0000"
3) 1) "116.31000012159347534"
2) "40.04999982043828055"
2) 1) "tianjin"
2) "6.8294"
3) 1) "116.38000041246414185"
2) "40.08000078008021916"
127.0.0.1:6379> georadiusbymember myloc beijing 20 km count 3 asc
1) "beijing"
2) "tianjin"
127.0.0.1:6379>
2.4.9 Stream 数据流类型
stream 是 Redis5.0 后新增的数据结构,用于可持久化的消息队列。
几乎满足了消息队列具备的全部内容,包括:
消息 ID 的序列化生成
消息遍历
消息的阻塞和非阻塞读取
消息的分组消费
未完成消息的处理
消息队列监控
每个 Stream 都有唯一的名称,它就是 Redis 的 key,首次使用 xadd 指令追加消息时自动创建。
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
xadd | xadd key id <*> field1 value1… | 将指定消息数据追加到指定队列(key)中,* 表示最新生成的 id(当前时间 + 序列号) |
xread | xread [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …] | 从消息队列中读取,COUNT:读取条数, BLOCK:阻塞读(默认不阻塞)key:队列 名称 id:消息 id |
xrange | xrange key start end [COUNT] | 读取队列中给定 ID 范围的消息 COUNT:返 回消息条数(消息 id 从小到大) |
xrevrange | xrevrange key start end [COUNT] | 读取队列中给定 ID 范围的消息 COUNT:返 回消息条数(消息 id 从大到小) |
xdel | xdel key id | 删除队列的消息 |
xgroup | xgroup create key groupname id | 创建一个新的消费组 |
xgroup | xgroup destory key groupname | 删除指定消费组 |
xgroup | xgroup delconsumer key groupname cname | 删除指定消费组中的某个消费者 |
xgroup | xgroup setid key id | 修改指定消息的最大 id |
xreadgroup | xreadgroup group groupname consumer COUNT streams key | 从队列中的消费组中创建消费者并消费数据 (consumer 不存在则创建) |
应用场景:
消息队列的使用
127.0.0.1:6379> xadd topic:001 * name zhangfei age 23
"1627544298648-0"
127.0.0.1:6379> xadd topic:001 * name zhaoyun age 24 name diaochan age 16
"1627544346099-0"
127.0.0.1:6379> xrange topic:001 - +
1) 1) "1627544298648-0"
2) 1) "name"
2) "zhangfei"
3) "age"
4) "23"
2) 1) "1627544346099-0"
2) 1) "name"
2) "zhaoyun"
3) "age"
4) "24"
5) "name"
6) "diaochan"
7) "age"
8) "16"
127.0.0.1:6379> xread COUNT 1 streams topic:001 0
1) 1) "topic:001"
2) 1) 1) "1627544298648-0"
2) 1) "name"
2) "zhangfei"
3) "age"
4) "23"
##创建的group1
127.0.0.1:6379> xgroup create topic:001 group1 0
OK
# 创建cus1加入到group1 消费 没有被消费过的消息 消费第一条
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
1) 1) "topic:001"
2) 1) 1) "1627544298648-0"
2) 1) "name"
2) "zhangfei"
3) "age"
4) "23"
#继续消费 第二条
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
1) 1) "topic:001"
2) 1) 1) "1627544346099-0"
2) 1) "name"
2) "zhaoyun"
3) "age"
4) "24"
5) "name"
6) "diaochan"
7) "age"
8) "16"
#没有可消费
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
(nil)
127.0.0.1:6379>
第二部分 Redis 扩展功能
第一节 发布与订阅
Redis 提供了发布订阅功能,可以用于消息的传输
Redis 的发布订阅机制包括三个部分,publisher,subscriber 和 Channel
发布者和订阅者都是 Redis 客户端,Channel 则为 Redis 服务器端。
发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
1.1 频道/模式的订阅与退订
subscribe:订阅 subscribe channel1 channel2 …
Redis 客户端 1 订阅频道 1 和频道 2
127.0.0.1:6379> subscribe channel1 channel2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "subscribe"
2) "channel2"
3) (integer) 2
publish:发布消息 publish channel message
Redis 客户端 2 将消息发布在频道 1 和频道 2 上
127.0.0.1:6379> publish channel1 weather-cloudy
(integer) 1
127.0.0.1:6379> publish channel2 weather-sunny
(integer) 1
Redis 客户端 1 接收到频道 1 和频道 2 的消息
1) "message"
2) "channel1"
3) "weather-cloudy"
1) "message"
2) "channel2"
3) "weather-sunny"
unsubscribe:退订 channel
Redis 客户端 1 退订频道 1
127.0.0.1:6379> unsubscribe channel1
1) "unsubscribe"
2) "channel1"
3) (integer) 0
psubscribe :模式匹配 psubscribe + 模式
Redis 客户端 1 订阅所有以 ch 开头的频道
127.0.0.1:6379> psubscribe ch*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "ch*"
3) (integer) 1
Redis 客户端 2 发布信息在频道 5 上
127.0.0.1:6379> publish ch3 sky-blue
(integer) 1
Redis 客户端 1 收到频道 5 的信息
1) "pmessage"
2) "ch*"
3) "ch3"
4) "sky-blue"
punsubscribe 退订模式
127.0.0.1:6379> punsubscribe ch*
1) "punsubscribe"
2) "ch*"
3) (integer) 0
1.2 发布订阅的机制
订阅某个频道或模式:
客户端(client):
属性为 pubsub_channels,该属性表明了该客户端订阅的所有频道
属性为 pubsub_patterns,该属性表示该客户端订阅的所有模式
服务器端(RedisServer):
属性为 pubsub_channels,该服务器端中的所有频道以及订阅了这个频道的客户端
属性为 pubsub_patterns,该服务器端中的所有模式和订阅了这些模式的客户端
typedef struct redisClient { ...
dict *pubsub_channels;
list *pubsub_patterns; ...
} redisClient;
struct redisServer {
...
dict *pubsub_channels;
ist *pubsub_patterns;
int notify_keyspace_events;
...
};
当客户端向某个频道发送消息时,Redis 首先在 redisServer 中的 pubsub_channels 中找出键为该频道的 结点,遍历该结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端。 然后,遍历结构体 redisServer 中的 pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订 阅了该模式的客户端。
1.3 使用场景:哨兵模式,Redisson 框架使用
在 Redis 哨兵模式中,哨兵通过发布与订阅的方式与 Redis 主服务器和 Redis 从服务器进行通信。这个我 们将在后面的章节中详细讲解。
Redisson 是一个分布式锁框架,在 Redisson 分布式锁释放的时候,是使用发布与订阅的方式通知的, 这个我们将在后面的章节中详细讲解。
第二节 Redis 中的事务
所谓事务(Transaction) ,是指作为单个逻辑工作单元执行的一系列操作
2.1 ACID 回顾
Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不 执行。
Redis:一个队列中的命令 执行或不执行 (非语法错误,将导致非原子性)
Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。
Redis: 集群中不能保证时时的一致性,只能是最终一致性
Isolation(隔离性):事务之间不会相互影响。
Redis: 命令是顺序执行的,在一个事务中,有可能被执行其他客户端的命令的(watch 对象被其他客户端改变,将会清空 command queue)
Durability(持久性):事务执行成功后必须全部写入磁盘。
Redis 有持久化但不保证 数据的完整性
2.2 Redis 事务
Redis 的事务是通过 multi、exec、discard 和 watch 这四个命令来完成的。
Redis 的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
Redis 不支持回滚操作
事务命令
multi:用于标记事务块的开始 Redis 会将后续的命令逐个放入队列中,然后使用 exec 原子化地执行这个 命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视 key
unwatch:清除监视 key
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 222
QUEUED
127.0.0.1:6379> hset set1 name zhangfei
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s2 333
QUEUED
127.0.0.1:6379> hset set2 age 23
QUEUED
127.0.0.1:6379> discard OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> watch s1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 555
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get s1
222
127.0.0.1:6379> unwatch
OK
2.3 事务机制
事务的执行
- 事务开始
在 RedisClient 中,有属性 flags,用来表示是否在事务中 flags=REDIS_MULTI - 命令入队
RedisClient 将命令存放在事务队列中 (EXEC,DISCARD,WATCH,MULTI 除外) - 事务队列
multiCmd *commands 用于存放命令 - 执行事务
RedisClient 向服务器端发送 exec 命令,RedisServer 会遍历事务队列,执行队列中的命令,最后将执 行的结果一次性返回给客户端。
如果某条命令在入队过程中发生错误,redisClient 将 flags 置为 REDIS_DIRTY_EXEC,EXEC 命令将会失败 返回。
Watch 的执行
类似与 cas 的版本号,旧的版本则舍弃命令队列
WATCH 命令监视数据库键
redisDb 有一个 watched_keys 字典,key 是某个被监视的数据的 key,值是一个链表.记录了所有监视这个数
据的客户端。
监视机制的触发 当修改数据后,监视这个数据的客户端的 flags 置为 REDIS_DIRTY_CAS
事务执行
RedisClient 向服务器端发送 exec 命令,服务器判断 RedisClient 的 flags,如果为 REDIS_DIRTY_CAS,则 清空事务队列。
Redis 的弱事务性
Redis 语法错误
则整个事务的命令在队列里都清除
127.0.0.1:6379> set computer mac
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set computer windows
QUEUED
127.0.0.1:6379> sets aa aa
(error) ERR unknown command `sets`, with args beginning with: `aa`, `aa`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get computer
"mac"
127.0.0.1:6379>
Redis 运行错误
在队列里正确的命令可以执行
(弱事务性) 弱事务性 :
1、在队列里正确的命令可以执行 (非原子操作)
2、不支持回滚
#语法错误
127.0.0.1:6379> set computer mac
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set computer windows
QUEUED
127.0.0.1:6379> sets aa aa
(error) ERR unknown command `sets`, with args beginning with: `aa`, `aa`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get computer
"mac"
#运行错误
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set computer linux
QUEUED
127.0.0.1:6379> lpush computer mac linux win
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get computer
"linux"
Redis 不支持事务回滚(为什么呢)
1、大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
2、Redis 为了性能方面而忽略了事务回滚。 (回滚记录历史版本)
2.4 Lua 脚本
lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放, 其设计目的是为了嵌入应用 程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 应用场景:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件。
nginx 上使用 lua 实现高并发
OpenRestry:一个可伸缩的基于 Nginx 的 Web 平台,是在 nginx 之上集成了 lua 模块的第三方服务器
OpenRestry 是一个通过 Lua 扩展 Nginx 实现的可伸缩的 Web 平台,内部集成了大量精良的 Lua 库、第三 方模块以及大多数的依赖项。 用于方便地搭建能够处理超高并发(日活千万级别)、扩展性极高的动态 Web 应用、Web 服务和动态网 关。
功能和 nginx 类似,就是由于支持 lua 动态脚本,所以更加灵活。 OpenRestry 通过 Lua 脚本扩展 nginx 功能,可提供负载均衡、请求路由、安全认证、服务鉴权、流量控 制与日志监控等服务。
类似的还有 Kong(Api Gateway)、tengine(阿里)
2.4.1 创建并修改 lua 环境
下载
地址:http://www.lua.org/download.html
可以本地下载上传到 linux,也可以使用 curl 命令在 linux 系统中进行在线下载
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
安装
yum -y install readline-devel ncurses-devel
tar -zxvf lua-5.3.5.tar.gz
#在src目录下
make linux
#或
make install
如果报错,说找不到 readline/readline.h, 可以通过 yum 命令安装
yum -y install readline-devel ncurses-devel
安装完以后再
make linux
#或者
make install
最后,直接输入 lua 命令即可进入 lua 的控制台
2.4.2 Lua 环境协作组件
从 Redis2.6.0 版本开始,通过内置的 lua 编译/解释器,可以使用 EVAL 命令对 lua 脚本进行求值。
脚本的命令是原子的,RedisServer 在执行脚本命令中,不允许插入新的命令
脚本的命令可以复制,RedisServer 在获得脚本后不执行,生成标识返回,Client 根据标识就可以随时执 行
EVAL 命令实现
通过执行 redis 的 eval 命令,可以运行一段 lua 脚本。
EVAL script numkeys key [key ...] arg [arg ...]
命令说明:
script 参数:是一段 Lua 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该) 定义为一个 Lua 函数。
numkeys 参数:用于指定键名参数的个数。
key [key …]参数: 从 EVAL 的第三个参数开始算起,使用了 numkeys 个键(key),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形 式访问( KEYS[1] , KEYS[2] ,以此类推)。必须大写
arg [arg …]参数:可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似(ARGV[1] 、 ARGV[2] ,诸如此类)。
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
lua 脚本中调用 Redis 命令
redis.call():
返回值就是 redis 命令执行的返回值 如果出错,则返回错误信息,不继续执行
redis.pcall():
返回值就是 redis 命令执行的返回值 如果出错,则记录错误信息,继续执行
注意事项
在脚本中,使用 return 语句将返回值返回给客户端,如果没有 return,则返回 nil
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 n1 zhaoyun
1
127.0.0.1:6379> eval "return redis.call('set', KEYS[1] , ARGV[1])" 1 pokemon pikaqiu
OK
127.0.0.1:6379> eval "redis.call('set', KEYS[1] , ARGV[1])" 1 pokemon2 pikaqiu2
(nil)
127.0.0.1:6379> get pokemon
"pikaqiu"
127.0.0.1:6379> get pokemon2
"pikaqiu2"
EVALSHA
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。
Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来 传送脚本主体并不是最佳选择。
为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但 它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)
SCRIPT 命令
SCRIPT FLUSH :清除所有脚本缓存
SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
SCRIPT LOAD :将一个脚本装入脚本缓存,返回 SHA1 摘要,但并不立即运行它
SCRIPT KILL :杀死当前正在运行的脚本
127.0.0.1:6379> SCRIPT HELP
1) SCRIPT <subcommand> arg arg ... arg. Subcommands are:
2) DEBUG (yes|sync|no) -- Set the debug mode for subsequent scripts executed.
3) EXISTS <sha1> [<sha1> ...] -- Return information about the existence of the scripts in the script cache.
4) FLUSH -- Flush the Lua scripts cache. Very dangerous on replicas.
5) KILL -- Kill the currently executing Lua script.
6) LOAD <script> -- Load a script into the scripts cache, without executing it.
1234567
127.0.0.1:6379> script load "return redis.call('set', KEYS[1], ARGV[1] )"
"511348f1a6f431925095b437edc89b97e2575f0f"
127.0.0.1:6379> evalsha 511348f1a6f431925095b437edc89b97e2575f0f 1 xiaogang nibishi
OK
127.0.0.1:6379> get xiaogang
"nibishi"
2.4.3 脚本管理命令实现
使用 redis-cli 直接执行 lua 脚本。
test.lua
vim test.lua
#添加内容
return redis.call('set',KEYS[1],ARGV[1])
:wq
#,两边都要有空格
./redis-cli -h 127.0.0.1 -p 6379 --eval test.lua name:6 , caocao
list.lua
vim list.lua
#添加
local key=KEYS[1]
local list=redis.call("lrange",key,0,-1);
return list;
#执行
./redis-cli --eval list.lua list1
1) "3"
2) "2"
3) "1"
利用 Redis 整合 Lua,主要是为了性能以及事务的原子性。因为 redis 帮我们提供的事务功能太差。
脚本复制
Redis 传播 Lua 脚本,在使用主从模式和开启 AOF 持久化的前提下:
当执行 lua 脚本时,Redis 服务器有两种模式:脚本传播模式和命令传播模式。
脚本传播模式
脚本传播模式是 Redis 复制脚本时默认使用的模式
Redis 会将被执行的脚本及其参数复制到 AOF 文件以及从服务器里面。 执行以下命令:
eval "redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])"
2 n1 n2 zhaoyun1 zhaoyun2
那么主服务器将向从服务器发送完全相同的 eval 命令:
eval "redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])"
2 n1 n2 zhaoyun1 zhaoyun2
注意:在这一模式下执行的脚本不能有时间、内部状态、随机函数(spop)等。执行相同的脚本以及参数 必须产生相同的效果。在 Redis5,也是处于同一个事务中。
命令传播模式
处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到 AOF 文件以及从服务器里面。
因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间、内部状态、随机函数 等,主服务器给所有从服务器复制的写命令仍然是相同的。
为了开启命令传播模式,用户在使用脚本执行任何写操作之前,需要先在脚本里面调用以下函数:
redis.replicate_commands()
redis.replicate_commands() 只对调用该函数的脚本有效:
在使用命令传播模式执行完当前脚本之后, 服务器将自动切换回默认的脚本传播模式。
如果我们在主服务器执行以下命令:
eval "redis.replicate_commands();redis.call('set',KEYS[1],ARGV[1]);redis.call('set',K EYS[2],ARGV[2])"
2 n1 n2 zhaoyun11 zhaoyun22
那么主服务器将向从服务器复制以下命令:
EXEC *1 $5 MULTI *3 $3 set $2 n1 $9 zhaoyun11 *3 $3 set $2 n2 $9 zhaoyun22 *1 $4 EXEC
管道(pipeline),事务和脚本(lua)三者的区别
三者都可以批量执行命令
管道无原子性,命令都是独立的,属于无状态的操作
事务和脚本是有原子性的,其区别在于脚本可借助 Lua 语言可在服务器端存储的便利性定制和简化操作
脚本的原子性要强于事务,脚本执行期间,另外的客户端 其它任何脚本或者命令都无法执行,脚本的执 行时间应该尽量短,不能太耗时的脚本
2.5 慢查询日志
我们都知道 MySQL 有慢查询日志 Redis 也有慢查询日志,可用于监视和优化查询
慢查询设置
在 redis.conf 中可以配置和慢查询日志相关的选项:
#执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录
slowlog-log-slower-than 10000
#slowlog-max-len 存储慢查询日志条数
slowlog-max-len 128
Redis 使用列表存储慢查询日志,采用队列方式(FIFO)
config set 的方式可以临时设置,
redis 重启后就无效
config set slowlog-log-slower-than 微秒
config set slowlog-max-len 条数
查看日志:slowlog get [n]
操作演示
127.0.0.1:6379> config set slowlog-log-slower-than 0
OK
127.0.0.1:6379> config set slowlog-max-len 2
OK
127.0.0.1:6379> set name:001 zhaoyun
OK
127.0.0.1:6379> set name:002 zhangfei
OK
127.0.0.1:6379> get name:002
"zhangfei"
127.0.0.1:6379> slowlog get #set和get都记录,第一条被移除了。
1) 1) (integer) 4 #日志的唯一标识符(uid)
2) (integer) 1627616880 #命令执行时的UNIX时间戳
3) (integer) 4 #命令执行的时长(微秒)
4) 1) "get" #执行命令及参数
2) "name:002"
5) "127.0.0.1:58834"
6) ""
2) 1) (integer) 3
2) (integer) 1627616874
3) (integer) 5
4) 1) "set"
2) "name:002"
3) "zhangfei"
5) "127.0.0.1:58834"
6) ""
127.0.0.1:6379>
慢查询定位&处理
使用 slowlog get 可以获得执行较慢的 redis 命令,针对该命令可以进行优化:
1、尽量使用短的 key,对于 value 有些也可精简,能使用 int 就 int。
2、避免使用 keys *、hgetall 等全量操作。
3、减少大 key 的存取,打散为小 key 100K 以上
4、将 rdb 改为 aof 模式
rdb fork 子进程 数据量过大 主进程阻塞 redis 性能大幅下降 关闭持久化 , (适合于数据量较小,有固定数据源)
5、想要一次添加多条数据的时候可以使用管道
6、尽可能地使用哈希存储
7、尽量限制下 redis 使用的内存大小,这样可以避免 redis 使用 swap 分区或者出现 OOM 错误 内存与硬盘的 swap
慢查询记录的保存
在 redisServer 中保存和慢查询日志相关的信息
慢查询日志的阅览&删除
初始化日志列表
void slowlogInit(void) {
server.slowlog = listCreate(); /* 创建一个list列表 */
server.slowlog_entry_id = 0; /* 日志ID从0开始 */
listSetFreeMethod(server.slowlog,slowlogFreeEntry); /* 指定慢查询日志list空间 的释放方法 */
}
查看日志数量的 slowlog len
def SLOWLOG_LEN():
# slowlog 链表的长度就是慢查询日志的条目数量
return len(redisServer.slowlog)
清除日志 slowlog reset
def SLOWLOG_RESET():
# 遍历服务器中的所有慢查询日志
for log in redisServer.slowlog:
# 删除日志
deleteLog(log)
2.6 监视器
Redis 客户端通过执行 MONITOR 命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理 的命令请求的相关信息。
此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条 命令请求的信息发送给所有监视器。
Redis 客户端 1
➜ bin ./redis-cli
127.0.0.1:6379> monitor
OK
1627617887.578979 [0 127.0.0.1:58834] "lpush" "domain" "lane.show" "galaxylib.com"
1627617933.485665 [0 127.0.0.1:58834] "lrange" "domain" "0" "-1"
Redis 客户端 2
127.0.0.1:6379> lpush domain lane.show galaxylib.com
(integer) 2
127.0.0.1:6379> lrange domain 0 -1
1) "galaxylib.com"
2) "lane.show"
127.0.0.1:6379>
Redis 监控平台
grafana、prometheus 以及 redis_exporter。
Grafana 是一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形 化选项,可以混合多种风格,支持多个数据源特点。
Prometheus 是一个开源的服务监控系统,它通过 HTTP 协议从远程的机器收集数据并存储在本地的时序 数据库上。 redis_exporter 为 Prometheus 提供了 redis 指标的导出,配合 Prometheus 以及 grafana 进行可视化及监 控。
第三部分 Redis 核心原理
第一节 Redis 持久化
1.1 为什么要持久化
Redis 是内存数据库,宕机后数据会消失。
Redis 重启后快速恢复数据,要提供持久化机制
Redis 持久化是为了快速的恢复数据而不是为了存储数据
Redis 有两种持久化方式:RDB 和 AOF
注意:Redis 持久化不保证数据的完整性。
当 Redis 用作 DB 时,DB 数据要完整,所以一定要有一个完整的数据源(文件、mysql)
在系统启动时,从这个完整的数据源中将数据 load 到 Redis 中
数据量较小,不易改变,比如:字典库(xml、Table)
通过 info 命令可以查看关于持久化的信息
27.0.0.1:6379> info
# Server
redis_version:5.0.5
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:ce72670116331b04
redis_mode:standalone
os:Linux 3.10.0-1160.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:4.8.5
process_id:2696
run_id:29027ef65c307bebc48c88248a8f6aabd3c64123
tcp_port:6379
uptime_in_seconds:9945
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:234881
executable:/usr/redis/bin/./redis-server
config_file:/usr/redis/bin/redis.conf
# Clients
connected_clients:1
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
# Memory
used_memory:3482352
used_memory_human:3.32M
used_memory_rss:19517440
used_memory_rss_human:18.61M
used_memory_peak:3502280
used_memory_peak_human:3.34M
used_memory_peak_perc:99.43%
used_memory_overhead:844078
used_memory_startup:791344
used_memory_dataset:2638274
used_memory_dataset_perc:98.04%
allocator_allocated:3617296
allocator_active:3883008
allocator_resident:16814080
total_system_memory:1019572224
total_system_memory_human:972.34M
used_memory_lua:72704
used_memory_lua_human:71.00K
used_memory_scripts:1552
used_memory_scripts_human:1.52K
number_of_cached_scripts:10
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.07
allocator_frag_bytes:265712
allocator_rss_ratio:4.33
allocator_rss_bytes:12931072
rss_overhead_ratio:1.16
rss_overhead_bytes:2703360
mem_fragmentation_ratio:5.67
mem_fragmentation_bytes:16077064
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:49694
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1627618492
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:6692864
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
1.2 RDB 介绍
RDB(Redis DataBase),是 redis 默认的存储方式,RDB 方式是通过快照(snapshot) 来完成的。
类似于拍照,只记录这一刻的数据 不关注过程
触发快照的方式
- 符合自定义配置的快照规则
- 执行 save 或者 bgsave 命令
- 执行 flushall 命令
- 执行主从复制操作 (第一次)
配置参数定期执行
在 redis.conf 中配置:save
多少秒内 数据变了多少
漏斗设计 提供性能
save "" # 不使用 RDB 存储 不能主从
save 900 1 # 表示 15 分钟(900 秒钟)内至少 1 个键被更改则进行快照。
save 300 10 # 表示 5 分钟(300 秒)内至少 10 个键被更改则进行快照。
save 60 10000 # 表示 1 分钟内至少 10000 个键被更改则进行快照。
12345678
命令显式触发
在客户端输入 bgsave 命令。
127.0.0.1:6379> bgsave
Background saving started
1.3 RDB 执行流程(原理)
- Redis 父进程首先判断:当前是否在执行 save,或 bgsave/bgrewriteaof(aof 文件重写命令)的子进程,如果在执行则 bgsave 命令直接返回。
- 父进程执行 fork(调用 OS 函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,Redis 不能执行来自客户端的任何命令。
- 父进程 fork 后,bgsave 命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令。
- 子进程创建 RDB 文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。(RDB 始终完整)
- 子进程发送信号给父进程表示完成,父进程更新统计信息。
- 父进程 fork 子进程后,继续工作。
1.3 RDB 文件结构
1、头部 5 字节固定为“REDIS”字符串
2、4 字节“RDB”版本号(不是 Redis 版本号),当前若为 9,填充后为 0009
3、辅助字段,以 key-value 的形式
4、存储数据库号码
5、字典大小
6、过期 key
7、主要数据,以 key-value 的形式存储
8、结束标志
9、校验和,就是看文件是否损坏,或者是否被修改。
字段名 | 字段值 | 字段名 | 字段值 |
---|---|---|---|
redis-ver | 5.0.5 | aof-preamble | 是否开启 aof |
redis-bits | 64/32 | repl-stream-db | 主从复制 |
ctime | 当前时间戳 | repl-id | 主从复制 |
used-mem | 使用内存 | repl-offset | 主从复制 |
直接 vim 打开
可以用 winhex 打开 dump.rdb 文件查看。
1.4 RDB 的优缺点
优点
RDB 是二进制压缩文件,占用空间小,便于传输(传给 slaver)
主进程 fork 子进程,可以最大化 Redis 性能,
主进程不能太大,Redis 的数据量不能太大,复制过程中主 进程阻塞
缺点
不保证数据完整性,会丢失最后一次快照以后更改的所有数据
1.6 AOF 介绍
AOF(append only file)是 Redis 的另一种持久化方式。
Redis 默认情况下是不开启的。
开启 AOF 持久化后 Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据 库状态的目的。
这样当 Redis 重启后只要按顺序回放这些命令就会恢复到原始状态了。
AOF 会记录命令顺序过程,RDB 只管结果
1.7 AOF 持久化实现
配置 redis.conf
# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
appendfilename appendonly.aof
1.8 AOF 原理
AOF 文件中存储的是 redis 的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加 到服务器的 AOF 缓存中。
文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
命令传播
当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。服务器在 接到客户端的请求之后, 它会根据协议文本的内容, 选择适当的命令函数, 并将各个参数从字符串文 本转换为 Redis 字符串对象( StringObject )。每当命令函数成功执行之后, 命令参数都会被传播到 AOF 程序。
缓存追加
当命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的协议文本。协议文本生成之后, 它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。
redisServer 结构维持着 Redis 服务器的状态, aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。
文件写入和保存
每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flflushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
AOF 保存模式
Redis 目前支持三种 AOF 保存模式,它们分别是:
AOF_FSYNC_NO :不保存。
AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)
以下三个小节将分别讨论这三种保存模式。
不保存
在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。
在这种模式下, SAVE 只会在以下任意一种情况中被执行:
Redis 被关闭
AOF 功能被关闭
系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
每一秒钟保存一次(推荐)
在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程(fork)调用 的, 所以它不会引起服务器主进程阻塞。
每执行一个命令保存一次
在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。
另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令 请求。
AOF 保存模式对性能和安全性的影响
对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下:
1.9 AOF 重写、触发方式、混合持久化
AOF 记录数据的变化过程,越来越大,需要重写“瘦身”
Redis 可以在 AOF 体积变得过大时,自动地在后台(Fork 子进程)对 AOF 进行重写。重写后的新 AOF 文 件包含了恢复当前数据集所需的最小命令集合。 所谓的“重写”其实是一个有歧义的词语, 实际上, AOF 重写并不需要对原有的 AOF 文件进行任何写入和读取, 它针对的是数据库中键的当前值。
举例如下:
set s1 11
set s1 22
set s1 33
没有优化的: set s1 11 set s1 22 set s1 33
优化后: set s1 33
lpush list1 1 2 3
lpush list1 4 5 6
优化后 lpush list1 1 2 3 4 5 6
Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进 程里执行, 这样处理的最大好处是:
1、子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
2、子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全 性。
不过, 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理 命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数 据不一致。
为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用, Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外, 还会追加到这个缓存中。
重写过程分析(整个重写操作是绝对安全的):
Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生 停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到 新 AOF 文件,并开始对新 AOF 文件进行追加操作。
当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
处理命令请求。
将写命令追加到现有的 AOF 文件中。
将写命令追加到 AOF 重写缓存中。
这样一来可以保证:
现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。 所有对数据库进行修改的命令都会被记录到 AOF 重写缓存中。
当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用 一个信号处理函数, 并完成以下工作:
将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。
Redis 数据库里的 +AOF 重写过程中的命令-------> 新的 AOF 文件----> 覆盖老的
当步骤 1 执行完毕之后, 现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了。
当步骤 2 执行完毕之后, 程序就完成了新旧两个 AOF 文件的交替。 这个信号处理函数执行完毕之后, 主进程就可以继续像往常一样接受命令请求了。 在整个 AOF 后台重 写过程中, 只有最后的写入缓存和改名操作会造成主进程阻塞, 在其他时候, AOF 后台重写都不会对 主进程造成阻塞, 这将 AOF 重写对性能造成的影响降到了最低。
以上就是 AOF 后台重写, 也即是 BGREWRITEAOF 命令(AOF 重写)的工作原理。
触发方式
1、配置触发 在 redis.conf 中配置
# 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过, 以启动时aof文件大小为准
auto-aof-rewrite-percentage 100
# 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化
auto-aof-rewrite-min-size 64mb
2、执行 bgrewriteaof 命令
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
127.0.0.1:6379>
混合持久化
RDB 和 AOF 各有优缺点,Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开,aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。
RDB 的头 +AOF 的身体---->appendonly.aof 开启混合持久化
修改 redis.conf
aof-use-rdb-preamble yes
1
我们可以看到该 AOF 文件是 rdb 文件的头和 aof 格式的内容,在加载时,首先会识别 AOF 文件是否以 REDIS 字符串开头,如果是就按 RDB 格式加载,加载完 RDB 后继续按 AOF 格式加载剩余部分。
AOF 文件的载入与数据还原
因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态 Redis
读取 AOF 文件并还原数据库状态的详细步骤如下: 1、创建一个不带网络连接的伪客户端(fake client):因为 Redis 的命令只能在客户端上下文中执行, 而载入 AOF 文件时所使用的命令直接来源于 AOF 文件而不是网络连接,所以服 务器使用了一个没有网络 连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命 令的效果完全一样
2、从 AOF 文件中分析并读取出一条写命令
3、使用伪客户端执行被读出的写命令
4、一直执行步骤 2 和步骤 3,直到 AOF 文件中的所有写命令都被处理完毕为止
当完成以上步骤之后,AOF 文件所保存的数据库状态就会被完整地还原出来,整个过程如下图所示:
1.10 RDB 与 AOF 对比
1、RDB 存某个时刻的数据快照,采用二进制压缩存储,AOF 存操作命令,采用文本存储(混合)
2、RDB 性能高、AOF 性能较低
3、RDB 在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF 设置为每秒保存一次,则最多 丢 2 秒的数据
4、Redis 以主服务器模式运行,RDB 不会保存过期键值对数据,Redis 以从服务器模式运行,RDB 会保 存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。AOF 写入文件时,对过期的 key 会追加一条 del 命令,当执行 AOF 重写时,会忽略过期 key 和 del 命令。
应用场景
内存数据库 rdb+aof 数据不容易丢
有原始数据源: 每次启动时都从原始数据源中初始化 ,则 不用开启持久化 (数据量较小)
缓存服务器 一般 rdb 性能高
数据还原时
有 rdb+aof 则还原 aof,因为 RDB 会造成文件的丢失,AOF 相对数据要完整。
只有 rdb,则还原 rdb
拉勾的配置策略
追求高性能:都不开 redis 宕机 从数据源恢复
字典库 : 不驱逐,保证数据完整性 不开持久化
用作 DB 不能主从 数据量小
做缓存 较高性能: 开 rdb
Redis 数据量存储过大,性能突然下降, fork 时间过长 阻塞主进程 则只开启 AOF
第二节 Redis 底层数据结构
Redis 作为 Key-Value 存储系统,数据结构如下:
Redis 没有表的概念,Redis 实例所对应的 db 以编号区分,db 本身就是 key 的命名空间。
比如:user:1000 作为 key 值,表示在 user 这个命名空间下 id 为 1000 的元素,类似于 user 表的 id=1000 的 行。
2.1 RedisDB 结构
Redis 中存在“数据库”的概念,该结构由 redis.h 中的 redisDb 定义。
当 redis 服务器初始化时,会预先分配 16 个数据库 所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中 redisClient 中存在一个名叫 db 的指针指向当前使用的数据库
RedisDB 结构体源码:
typedef struct redisDb {
int id; //id 是数据库序号,为 0-15(默认 Redis 有 16 个数据库)
long avg_ttl; //存储的数据库对象的平均 ttl(time to live),用于统计
dict *dict; //存储数据库所有的 key-value
dict *expires; //存储 key 的过期时间
dict *blocking_keys;//blpop 存储阻塞 key 和客户端对象
dict *ready_keys;//阻塞后 push 响应阻塞客户端 存储阻塞后 push 的 key 和客户端对象
dict *watched_keys;//存储 watch 监控的的 key 和客户端对象
} redisDb;
id
id 是数据库序号,为 0-15(默认 Redis 有 16 个数据库)
dict
存储数据库所有的 key-value,后面要详细讲解
expires
存储 key 的过期时间,后面要详细讲解
2.2 RedisObject 结构
对应于 Key 的 Value 对象结构
包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象
2.2.1 结构信息概览
typedef struct redisObject {
unsigned type:4;//类型 对象类型
unsigned encoding:4;//编码
void *ptr;//指向底层实现数据结构的指针
//...
int refcount;//引用计数 //...
unsigned lru:LRU_BITS; //LRU_BITS为24bit 记录最后一次被命令程序访问的时间 //...
}robj;
4 位 type
type 字段表示对象的类型,占 4 位;
REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有 序集合)。
当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型
127.0.0.1:6379> type xiaogang
string
4 位 encoding
encoding 表示对象的内部编码,占 4 位 每个对象有不同的实现编码
Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。
通过 object encoding 命令,可以查看对象采用的编码方式
127.0.0.1:6379> object encoding xiaogang
"embstr"
12
24 位 LRU
lru 记录的是对象最后一次被命令程序访问的时间,( 4.0 版本占 24 位,2.6 版本占 22 位)。
高 16 位存储一个分钟数级别的时间戳,低 8 位存储访问计数(lfu : 最近访问次数)
lru----> 高 16 位: 最后被访问的时间
lfu-----> 低 8 位:最近访问次数
refcount refcount
记录的是该对象被引用的次数,类型为整型。
refcount 的作用,主要在于对象的引用计数和内存回收。
当对象的 refcount>1 时,称为共享对象
Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对 象。
ptr
ptr 指针指向具体的数据,比如:set hello world,ptr 指向包含字符串 world 的 SDS。
2.2.2 7 种 type 类型
1、字符串对象
C 语言: 字符数组 “\0”
Redis 使用了 SDS(Simple Dynamic String)。简单动态字符串结构 用于存储字符串和整型数据。
struct sdshdr{
//记录buf数组中已使用字节的数量
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字符数组,用于保存字符串
char buf[];
}
123456789
buf[] 的长度=len+free+1
SDS 的优势:
1、SDS 在 C 字符串的基础上加入了 free 和 len 字段,获取字符串长度:SDS 是 O(1),C 字符串是 O(n)。 buf 数组的长度=free+len+1
2、 SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
3、可以存取二进制数据,以字符串长度 len 来作为结束标识
C: \0 空字符串 二进制数据包括空字符串,所以没有办法存取二进制数据
SDS : 非二进制 \0 二进制: 字符串长度 len 可以存二进制数据
使用场景:
SDS 的主要应用在:存储字符串和整型数据、存储 key、AOF 缓冲区和用户输入缓冲。
2、跳跃表(重点)
跳跃表是有序集合(sorted-set)的底层实现,效率高,实现简单。
跳跃表的基本思想: 将有序链表中的部分节点分层,每一层都是一个有序链表。
查找
在查找时优先从最高层开始向后查找,当到达某个节点时,如果 next 节点值大于要查找的值或 next 指针 指向 null,则从当前节点下降一层继续向后查找。
举例:
查找元素 9,按道理我们需要从头结点开始遍历,一共遍历 8 个结点才能找到元素 9。
第一次分层: 遍历 5 次找到元素 9(红色的线为查找路径)
第二次分层: 遍历 4 次找到元素 9
第三层分层: 遍历 4 次找到元素 9
这种数据结构,就是跳跃表,它具有二分查找的功能。
插入与删除
上面例子中,9 个结点,一共 4 层,是理想的跳跃表。
通过抛硬币(概率 1/2)的方式来决定新插入结点跨越的层数:
正面:插入上层
背面:不插入
达到 1/2 概率(计算次数)(不知是否每次每层插入完都抛)
删除
找到指定元素并删除每层的该元素即可
跳跃表特点:
每层都是一个有序链表
查找次数近似于层数(1/2) log2n
底层包含所有元素
空间复杂度 O(n)
Redis 跳跃表的实现
完整的跳跃表结构体:
跳跃表的优势:
1、可以快速查找到需要的节点 O(logn)
2、可以在 O(1)的时间复杂度下,快速获得跳跃表的头节点、尾结点、长度和高度。
应用场景:有序集合的实现
3、字典(散列表 Hash)(重点 + 难点)
字典 dict 又称散列表(hash),是用来存储键值对的一种数据结构。
Redis 整个数据库是用字典来存储的。(K-V 结构)
对 Redis 进行 CURD 操作其实就是对字典中的数据进行 CURD 操作。
数组
数组:用来存储数据的容器,采用头指针 + 偏移量的方式能够以 O(1)的时间复杂度定位到数据所在的内 存地址。
Redis 海量存储 快速查找
Hash 函数
Hash(散列),作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值。
hash 函数可以把 Redis 里的 key:包括字符串、整数、浮点数统一转换成整数。
key=100.1 String “100.1” 5 位长度的字符串
Redis-cli :times 33(hash 算法)
Redis-Server : MurmurHash(hash 算法)
数组下标 (hash 值 % 数组容量得到的余数) = hash(key)% 数组容量
如 6 = hash(aa)%1000=1006%1000=6
Hash 冲突
不同的 key 经过计算后出现数组下标一致,称为 Hash 冲突。
采用单链表在相同的下标位置处存储原始 key 和 value
当根据 key 找 Value 时,找到数组下标,遍历单链表可以找出 key 相同的 value
如图 name 和 sname 存在 hash 后下标都是 3 的情况
显然 sname 存储的值不应该覆盖 name 的值 ,通过维护一个链表节点存储 key-value 的形式来存储
name:zhangfei sname:zhaoyun,查找的时候再比较 key 即可取出 value
Redis 字典的实现
Redis 字典实现包括:字典(dict)、Hash 表(dictht)、Hash 表节点(dictEntry)。
Hash 表
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表数组的大小
unsigned long sizemask; // 用于映射位置的掩码值永远等于(size-1)
unsigned long used; // 哈希表已有节点的数量,包含 next 单链表数据
} dictht;
1、hash 表的数组初始容量为 4,随着 k-v 存储量的增加需要对 hash 表数组进行扩容,新扩容量为当前量 的一倍,即 4,8,16,32
2、索引值=Hash 值&掩码值(Hash 值与 Hash 表容量取余)
Hash 表节点
typedef struct dictEntry {
void *key; // 键
union { // 值v的类型可以是以下4种类型
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 指向下一个哈希表节点,形成单向链表 解决hash冲突
} dictEntry;
12345678910
key 字段存储的是键值对中的键
v 字段是个联合体,存储的是键值对中的值。
next 指向下一个哈希表节点,用于解决 hash 冲突
dict 字典
type 字段,指向 dictType 结构体,里边包括了对该字典操作的函数指针
Redis 字典除了主数据库的 K-V 数据存储以外,还可以用于:散列表对象、哨兵模式中的主从节点管理等
在不同的应用中,字典的形态都可能不同,dictType 是为了实现各种形态的字典而抽象出来的操作函数 (多态)。
完整的 Redis 字典数据结构:
字典扩容
字典达到存储上限(阈值 0.75),需要 rehash(扩容)
扩容流程:
说明:
- 初次申请默认容量为 4 个 dictEntry,非初次申请为当前 hash 表容量的一倍。
- rehashidx=0 表示要进行 rehash 操作。
- 新增加的数据在新的 hash 表 h[1]
- 修改、删除、查询在老 hash 表 h[0]、新 hash 表 h[1]中(rehash 中)
- 将老的 hash 表 h[0]的数据重新计算索引值后全部迁移到新的 hash 表 h[1]中,这个过程称为 rehash。
渐进式 rehash
当数据量巨大时 rehash 的过程是非常缓慢的,所以需要进行优化。
服务器忙,则只对一个节点进行 rehash
服务器闲,可批量 rehash(100 节点)
应用场景:
1、主数据库的 K-V 数据存储
2、散列表对象(hash)
3、哨兵模式中的主从节点管理
4、压缩列表
压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构 节省内存 是一个字节数组,可以包含多个节点(entry)。每个节点可以保存一个字节数组或一个整数。 (value encoding 存储结构体)
压缩列表的数据结构如下:
zlbytes:压缩列表的字节长度
zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量
zllen:压缩列表的元素个数
entry1…entryX : 压缩列表的各个节点
zlend:压缩列表的结尾,占一个字节,恒为 0xFF(255)
entryX 元素的编码结构:
previous_entry_length:前一个元素的字节长度
encoding:表示当前元素的编码
content:数据内容
ziplist 结构体如下:
应用场景:
sorted-set 和 hash 元素个数少且是小整数或短字符串(直接使用)
list 用快速链表(quicklist)数据结构存储,而快速链表是双向列表与压缩列表的组合。(间接使用)
5、整数集合
整数集合(intset)是一个有序的(整数升序)、存储整数的连续存储结构。(value encoding 存储结构体)
当 Redis 集合类型的元素都是整数并且都处在 64 位有符号整数范围内(2^64),使用该结构体存储。
127.0.0.1:6379> sadd set:001 1 3 5 6 2
(integer) 5
127.0.0.1:6379> object encoding set:001
"intset"
127.0.0.1:6379> sadd set:004 1 100000000000000000000000000 9999999999
(integer) 3
127.0.0.1:6379> object encoding set:004
"hashtable"
127.0.0.1:6379>
应用场景:
可以保存类型为 int16_t、int32_t 或者 int64_t 的整数值,并且保证集合中不会出现重复元素。
6、快速列表(重要)
快速列表(quicklist)是 Redis 底层重要的数据结构。是列表的底层实现。(在 Redis3.2 之前,Redis 采 用双向链表(adlist)和压缩列表(ziplist)实现。)
在 Redis3.2 以后结合 adlist 和 ziplist 的优势 Redis 设 计出了 quicklist。
127.0.0.1:6379> lpush list:001 1 2 5 4 3
(integer) 5
127.0.0.1:6379> object encoding list:001
"quicklist"
双向链表(adlist)
双向链表优势:
- 双向:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为 O(1)。
- 普通链表(单链表):节点类保留下一节点的引用。链表类只保留头节点的引用,只能从头节点插 入删除
- 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结 束。 环状:头的前一个节点指向尾节点
- 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
- 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
快速列表
quicklist 是一个双向链表,链表中的每个节点时一个 ziplist 结构。quicklist 中的每个节点 ziplist 都能够存 储多个数据元素。
quicklist 的结构定义如下:
quicklistNode 的结构定义如下:
数据压缩
quicklist 每个节点的实际数据存储结构为 ziplist,这种结构的优势在于节省存储空间。为了进一步降低 ziplist 的存储空间,还可以对 ziplist 进行压缩。
Redis 采用的压缩算法是 LZF。其基本思想是:数据与前 面重复的记录重复位置及长度,不重复的记录原始数据。
压缩过后的数据可以分成多个片段,每个片段有两个部分:解释字段和数据字段。
quicklistLZF 的结构 体如下:
typedef struct quicklistLZF {
unsigned int sz; // LZF压缩后占用的字节数
char compressed[]; // 柔性数组,指向数据部分
} quicklistLZF;
应用场景
列表(List)的底层实现、发布与订阅、慢查询、监视器等功能。
7、流对象
stream 主要由:消息、生产者、消费者和消费组构成。
Redis Stream 的底层主要使用了 listpack(紧凑列表)和 Rax 树(基数树)。
listpack
listpack 表示一个字符串列表的序列化,listpack 可用于存储字符串或整数。用于存储 stream 的消息内 容。
结构如下图:
Rax 树
Rax 是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操 作。
Rax 被用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀 10 种 encoding 息可以理解为时间序列消息。
使用 Rax 结构 进行存储就可以快速地根据消息 ID 定位到具 体的消息,然后继续遍历指定消息 之后的所有消息。
应用场景: stream 的底层实现
2.2.3 10 种 encoding
encoding 表示对象的内部编码,占 4 位。
Redis 通过 encoding 属性为对象设置不同的编码 对于少的和小的数据,Redis 采用小的和压缩的存储方式,体现 Redis 的灵活性 大大提高了 Redis 的存储量和执行效率
比如 Set 对象:
intset : 元素是 64 位以内的整数
hashtable:元素是 64 位以外的整数
如下所示:
➜ bin ./redis-cli
127.0.0.1:6379> sadd set:001 1 3 5 6 2
(integer) 0
127.0.0.1:6379> object encoding set:001
"intset"
127.0.0.1:6379> sadd set:004 1 100000000000000000000000000 9999999999
(integer) 0
127.0.0.1:6379> object encoding set:004
"hashtable"
127.0.0.1:6379>
String
int、raw、embstr
int
REDIS_ENCODING_INT(int 类型的整数)
127.0.0.1:6379> set n1 123 OK
127.0.0.1:6379> object encoding n1 "int"
embstr
REDIS_ENCODING_EMBSTR(编码的简单动态字符串)
小字符串 长度小于 44 个字节
127.0.0.1:6379> set name:001 zhangfei
OK
127.0.0.1:6379> object encoding name:001 "embstr"
raw
REDIS_ENCODING_RAW (简单动态字符串)
大字符串 长度大于 44 个字节
127.0.0.1:6379> set address:001
asdasdasdasdasdasdsadasdasdasdasdasdasdasdasdasdasdasdasdasdasdasdasdasdasdasdas dasdasdas
OK
127.0.0.1:6379> object encoding address:001 "raw"
list
列表的编码是 quicklist。 REDIS_ENCODING_QUICKLIST(快速列表)
127.0.0.1:6379> lpush list:0 1 2 3 4 5
(integer) 5
127.0.0.1:6379> object encoding list:0
"quicklist"
127.0.0.1:6379>
hash
散列的编码是字典和压缩列表
dict
REDIS_ENCODING_HT(字典)
当散列表元素的个数比较多或元素不是小整数或短字符串时。
127.0.0.1:6379> hmset user:3 username11111111111111
111111111111111111111111111111111111111111111111111
1111111 11111111111111111111111111111111
zhangfei password 111 num 230000000000000000000000000000
000000000000000000000
OK
127.0.0.1:6379> object encoding user:3
"hashtable"
ziplist
REDIS_ENCODING_ZIPLIST(压缩列表)
当散列表元素的个数比较少,且元素都是小整数或短字符串时。
127.0.0.1:6379> hmset user:1 name zhangsan hobby tableball love reading
OK
127.0.0.1:6379> object encoding user:1
"ziplist"
set
集合的编码是整形集合和字典
intset
REDIS_ENCODING_INTSET(整数集合)
当 Redis 集合类型的元素都是整数并且都处在 64 位有符号整数范围内(<18446744073709551616)
127.0.0.1:6379> sadd set:1 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding set:1
"intset"
127.0.0.1:6379>
dict
REDIS_ENCODING_HT(字典)
当 Redis 集合类型的元素是非整数或都处在 64 位有符号整数范围外(>18446744073709551616)
127.0.0.1:6379> sadd set:004 1 100000000000000000000000000 9999999999
(integer) 0
127.0.0.1:6379> object encoding set:004
"hashtable"
127.0.0.1:6379>
zset
有序集合的编码是压缩列表和跳跃表 + 字典
REDIS_ENCODING_ZIPLIST(压缩列表) 当元素的个数比较少,且元素都是小整数或短字符串时。
127.0.0.1:6379> zadd mysocre 80 english 70 chemistry 90 chines 88 history
(integer) 4
127.0.0.1:6379> object encoding myscore
"ziplist"
skiplist + dict
REDIS_ENCODING_SKIPLIST(跳跃表 + 字典)
当元素的个数比较多或元素不是小整数或短字符串时。
127.0.0.1:6379> zadd hit:2 100 item111111111111111111
111111111111111111111111111111111111111111111111111
1111111 1111111111111111111111111111111111 20 item2 45 item3 (integer) 3
127.0.0.1:6379> object encoding hit:2 "skiplist"
第三节缓存过期和淘汰策略
Redis 性能高:
官方数据 读:110000 次/s 写:81000 次/s
长期使用,key 会不断增加,Redis 作为缓存使用,物理内存也会满
造成内存与硬盘交换(swap)虚拟内存 ,频繁 IO 性能急剧下降
3.1 Maxmemory
不设置的场景
Redis 的 key 是固定的,不会增加
Redis 作为 DB 使用(字典表),保证数据的完整性,不能淘汰 , 可以做集群,横向扩展
缓存淘汰策略:禁止驱逐 (默认)
设置的场景
Redis 是作为缓存使用,不断增加 Key
maxmemory : 默认为 0 不限制
问题:达到物理内存后性能急剧下架,甚至崩溃 内存与硬盘交换(swap) 虚拟内存 ,频繁 IO 性能急剧下降
设置多少?
与业务有关 只有 1 个 Redis 实例,保证系统运行 1 G ,剩下的就都可以设置 物理内存的 3/4
slaver : 留出一定的内存
如何设置
在 redis.conf 中
maxmemory 1024mb
命令: 获得 maxmemory 数
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
设置 maxmemory 后,当趋近 maxmemory 时,通过缓存淘汰策略,从内存中删除对象
不设置 maxmemory 无最大内存限制 maxmemory-policy noeviction (禁止驱逐) 不淘汰
设置 maxmemory 则 maxmemory-policy 要配置
3.2 expire 数据结构
在 Redis 中可以使用 expire 命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动 被删除。
expire 的使用
expire 命令的使用方法如下:
expire key ttl(单位秒)
127.0.0.1:6379> get sky
"blue"
127.0.0.1:6379> expire sky 60
(integer) 1
127.0.0.1:6379> ttl sky
(integer) 55 #50s
127.0.0.1:6379> ttl sky
(integer) -2 #失效
127.0.0.1:6379> ttl weather
(integer) -1 #永久
expire 原理
typedef struct redisDb {
dict *dict; -- key Value
dict *expires; -- key ttl
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
} redisDb;
上面的代码是 Redis 中关于数据库的结构体定义,这个结构体定义中除了 id 以外都是指向字典的指针, 其中我们只看 dict 和 expires。
dict 用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对,expires 则用于维护一个 Redis 数据 库中设置了失效时间的键(即 key 与失效时间的映射)。
当我们使用 expire 命令设置一个 key 的失效时间时,Redis 首先到 dict 这个字典表中查找要设置的 key 是 否存在,如果存在就将这个 key 和失效时间添加到 expires 这个字典表。
当我们使用 setex 命令向系统插入数据时,Redis 首先将 Key 和 Value 添加到 dict 这个字典表中,然后 将 Key 和失效时间添加到 expires 这个字典表中。
简单地总结来说就是,设置了失效时间的 key 和具体的失效时间全部都维护在 expires 这个字典表中。
3.3 删除策略
Redis 的数据删除有定时删除、惰性删除和主动删除三种方式。
Redis 目前采用惰性删除 + 主动删除的方式。
定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除 操作。
需要创建定时器,而且消耗 CPU,一般不推荐使用。
惰性删除
在 key 被访问时如果发现它已经失效,那么就删除它。
调用 expireIfNeeded 函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删 除它
主动删除
在 redis.conf 文件中可以配置主动删除策略,默认是 no-enviction(不删除)
maxmemory-policy allkeys-lru
1、LRU
LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想 是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
- 在 Java 中可以使用 LinkHashMap(哈希链表)去实现 LRU
Version:0.9 StartHTML:0000000105 EndHTML:0000000594 StartFragment:0000000141 EndFragment:0000000554 让我们以用户信息的需求为例,来演示一下 LRU 算法的基本思路:
1.假设我们使用哈希链表来缓存用户信息,目前缓存了 4 个用户,这 4 个用户是按照时间顺序依次从链表 右端插入的。
2.此时,业务方访问用户 5,由于哈希链表中没有用户 5 的数据,我们从数据库中读取出来,插入到缓存 当中。这时候,链表中最右端是最新访问到的用户 5,最左端是最近最少访问的用户 1。
3.接下来,业务方访问用户 2,哈希链表中存在用户 2 的数据,我们怎么做呢?我们把用户 2 从它的前驱 节点和后继节点之间移除,重新插入到链表最右端。这时候,链表中最右端变成了最新访问到的用户 2,最左端仍然是最近最少访问的用户 1。
4.接下来,业务方请求修改用户 4 的信息。同样道理,我们把用户 4 从原来的位置移动到链表最右侧,并 把用户信息的值更新。这时候,链表中最右端是最新访问到的用户 4,最左端仍然是最近最少访问的用 户 1。
5.业务访问用户 6,用户 6 在缓存里没有,需要插入到哈希链表。假设这时候缓存容量已经达到上限,必 须先删除最近最少访问的数据,那么位于哈希链表最左端的用户 1 就会被删除掉,然后再把用户 6 插入到 最右端。
Redis 的 LRU 数据淘汰机制
在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新, server.lrulock 的值是根据 server.unixtime 计算出来的。
另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。可以想象的是,每一 次访问数据的时候,会更新 redisObject.lru。
LRU 数据淘汰机制是这样的:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。
不会遍历所有的 key
用当前时间-最近访问 越大 说明 访问间隔时间越长
volatile-lru
从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
allkeys-lru
从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
2、LFU
LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将 来一段时间内被使用的可能性也很小。
volatile-lfu
allkeys-lfu
3、random
随机
volatile-random
从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-random
从数据集(server.db[i].dict)中任意选择数据淘汰
4、ttl
volatile-ttl
从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。
TTL 数据淘汰机制:从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。
5 、noenviction
禁止驱逐数据,不删除 (默认)
缓存淘汰策略的选择
allkeys-lru : 在不确定时一般采用的策略。 冷热数据交换
volatile-lru : 比 allkeys-lru 性能差 存 : 过期时间
allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)
自己控制:volatile-ttl 缓存穿透
案例分享:字典库失效
key-Value 业务表存 code 显示 文字
拉勾早期将字典库,设置了 maxmemory,并设置缓存淘汰策略为 allkeys-lru 结果造成字典库某些字段失效,缓存击穿 , DB 压力剧增,差点宕机。
分析: 字典库 : Redis 做 DB 使用,要保证数据的完整性 maxmemory 设置较小,采用 allkeys-lru,会对没有经常访问的字典库随机淘汰 当再次访问时会缓存击穿,请求会打到 DB 上。
解决方案:
1、不设置 maxmemory
2、使用 noenviction 策略
Redis 是作为 DB 使用的,要保证数据的完整性,所以不能删除数据。 可以将原始数据源(XML)在系统启动时一次性加载到 Redis 中。 Redis 做主从 + 哨兵 保证高可用
第四节 通讯协议及事件处理机制
通信协议
Redis 是单进程单线程的。 应用系统和 Redis 通过 Redis 协议(RESP)进行交互。
事件处理机制
Redis 服务器是典型的事件驱动系统。
MVC : java 上层调下层
事件驱动: js Redis 将事件分为两大类:文件事件和时间事件。
4.1 请求响应模式
Redis 协议位于 TCP 层之上,即客户端和 Redis 实例保持双工的连接。
串行的请求响应模式(ping-pong)
串行化是最简单模式,客户端与服务器端建立长连接
连接通过心跳机制检测(ping-pong) ack 应答
客户端发送请求,服务端响应,
客户端收到响应后,再发起第二个请求,服务器端再响应。
telnet 和 redis-cli 发出的命令 都属于该种模式
特点:
有问有答
耗时在网络传输命令
性能较低
双工的请求响应模式(pipeline)
批量请求,批量响应
请求响应交叉进行,不会混淆(TCP 双工)
pipeline 的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回。
通过 pipeline,一次 pipeline(n 条命令)= 一次网络时间 + n 次命令时间
通过 Jedis 可以很方便的使用 pipeline
Jedis redis = new Jedis("192.168.1.111", 6379);
redis.auth("12345678");//授权密码 对应redis.conf的requirepass密码
Pipeline pipe = jedis.pipelined();
for (int i = 0; i <50000; i++) {
pipe.set("key_"+String.valueOf(i),String.valueOf(i));
} //将封装后的PIPE一次性发给redis
pipe.sync();
原子化的批量请求响应模式(事务)
Redis 可以利用事务机制批量执行命令。
发布订阅模式(pub/sub)
发布订阅模式是:一个客户端触发,多个客户端被动接收,通过服务器中转。
脚本化的批量执行(lua)
客户端向服务器端提交一个 lua 脚本,服务器端执行该脚本。
4.2 请求数据格式
Redis 客户端与服务器交互采用序列化协议(RESP)。
请求以字符串数组的形式来表示要执行命令的参数
Redis 使用命令特有(command-specific)数据类型作为回复。
Redis 通信协议的主要特点有:
客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。
在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。 简单,高效,易读。
内联格式
可以使用 telnet 给 Redis 发送命令,首字符为 Redis 命令名的字符,格式为 str1 str2 str3…
➜ ~ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
ping
+PONG
exists name
:1
get sky
$-1
规范格式(redis-cli) RESP
1、间隔符号,在 Linux 下是\r\n,在 Windows 下是\n
2、简单字符串 Simple Strings, 以 "+"加号 开头
3、错误 Errors, 以"-"减号 开头
4、整数型 Integer, 以 “:” 冒号开头
5、大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制 512M
6、数组类型 Arrays,以 "*"星号开头
用 SET 命令来举例说明 RESP 协议的格式。
➜ ~ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
set hello world
+OK
实际发送的请求数据:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
#3个字符串、5个字符
*3 $3 SET $5 hello $5 world
实际收到的响应数据:
+OK\r\n
4.3 命令处理流程
整个流程包括:服务器启动监听、接收命令请求并解析、执行命令请求、返回命令回复等。
Server 启动时监听 socket
启动调用 initServer 方法:
创建 eventLoop(事件机制)
注册时间事件处理器
注册文件事件(socket)处理器
监听 socket 建立连接
建立 Client
redis-cli 建立 socket
redis-server 为每个连接(socket)创建一个 Client 对象
创建文件事件监听
socket 指定事件处理函数
读取 socket 数据到输入缓冲区
从 client 中读取客户端的查询缓冲区内容。
解析获取命令
将输入缓冲区中的数据解析成对应的命令
判断是单条命令还是多条命令并调用相应的解析器解析
执行命令
解析成功后调用 processCommand 方法执行命令,如下图:
大致分三个部分:
调用 lookupCommand 方法获得对应的 redisCommand
检测当前 Redis 是否可以执行该命令
调用 call 方法真正执行命令
4.4 协议响应格式
状态回复
对于状态,回复的第一个字节是“+”
"+OK"
错误回复
对于错误,回复的第一个字节是“ - ”
1. -ERR unknown command 'foobar'
2. -WRONGTYPE Operation against a key holding the wrong kind of value
整数回复
对于整数,回复的第一个字节是“:”
":6"
批量回复
对于批量字符串,回复的第一个字节是“$”
"$6 foobar"
多条批量回复
对于多条批量回复(数组),回复的第一个字节是“*”
"*3"
4.5 协议解析及处理
包括协议解析、调用命令、返回结果。
协议解析
用户在 Redis 客户端键入命令后,Redis-cli 会把命令转化为 RESP 协议格式,然后发送给服务器。服务器 再对协议进行解析,分为三个步骤
1、解析命令请求参数数量
命令请求参数数量的协议格式为"*N\r\n" ,其中 N 就是数量,比如
127.0.0.1:6379> set name:1 zhaoyun
1
我们打开 aof 文件可以看到协议内容
*3(/r/n)
$3(/r/n)
set(/r/n)
$7(/r/n)
name:10(/r/n)
$7(/r/n)
zhaoyun(/r/n)
首字符必须是“*”,使用"\r"定位到行尾,之间的数就是参数数量了。
2、循环解析请求参数
首字符必须是" " ,使用 " / r " 定位到行尾,之间的数是参数的长度,从 / n 后到下一个 " ",使用"/r"定位到行尾,之间的数是参数的长度,从/n 后到下一个" ",使用"/r"定位到行尾,之间的数是参数的长度,从/n后到下一个""之间就是参
数的值了,循环解析直到没有"$"。
4.6 协议执行
协议的执行包括命令的调用和返回结果
判断参数个数和取出的参数是否一致
RedisServer 解析完命令后,会调用函数 processCommand 处理该命令请求
quit 校验,如果是“quit”命令,直接返回并关闭客户端
命令语法校验,执行 lookupCommand,查找命令(set),如果不存在则返回:“unknown command”错误。
参数数目校验,参数数目和解析出来的参数个数要匹配,如果不匹配则返回:“wrong number of arguments”错误。
此外还有权限校验,最大内存校验,集群校验,持久化校验等等。
校验成功后,会调用 call 函数执行命令,并记录命令执行时间和调用次数 如果执行命令时间过长还要记录慢查询日志
执行命令后返回结果的类型不同则协议格式也不同,分为 5 类:状态回复、错误回复、整数回复、批量 回复、多条批量回复。
事件处理机制
Redis服务器是典型的事件驱动系统。
MVC : java 上层调下层
事件驱动: js
Redis将事件分为两大类:文件事件和时间事件。
4.7 文件事件处理机制
文件事件即 Socket 的读写事件,也就是 IO 事件。
file descriptor (文件描述符) 客户端的连接、命令请求、数据回复、连接断开
Socket
套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据。
Reactor
Redis 事件处理机制采用单线程的 Reactor 模式,属于 I/O 多路复用的一种常见模式。
IO 多路复用( I/O multiplexing )指的通过单个线程管理多个 Socket。
Reactor pattern(反应器设计模式)是一种为处理并发服务请求,并将请求提交到 一个或者多个服务处理 程序的事件设计模式。
Reactor 模式是事件驱动
一个或多个并发输入源(文件事件)
一个 Service Handler
多个 Request Handlers
这个 Service Handler 会同步的将输入的请求(Event)多路复用的分发给相应的 Request Handler
Handle:I/O 操作的基本文件句柄,在 linux 下就是 fd(文件描述符)
Synchronous Event Demultiplexer :同步事件分离器,阻塞等待 Handles 中的事件发生。(系统)
Reactor: 事件分派器,负责事件的注册,删除以及对所有注册到事件分派器的事件进行监控, 当事件 发生时会调用 Event Handler 接口来处理事件。
Event Handler: 事件处理器接口,这里需要 Concrete Event Handler 来实现该接口
Concrete Event Handler:真实的事件处理器,通常都是绑定了一个 handle,实现对可读事件 进行读 取或对可写事件进行写入的操作。
主程序向事件分派器(Reactor)注册要监听的事件
Reactor 调用 OS 提供的事件处理分离器,监听事件(wait)
当有事件产生时,Reactor 将事件派给相应的处理器来处理 handle_event()
4 种 IO 多路复用模型与选择
select,poll,epoll、kqueue 都是 IO 多路复用的机制。
I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符(socket),一旦某个描述符就绪(一 般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
select 函数监视的文件描述符分 3 类,分别是:
writefds
readfds
exceptfds
调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有 except),或者超时 (timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。
当 select 函数返回后,可以 通过 遍历 fd 列表,来找到就绪的描述符。
优点
select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
windows linux …
缺点
单个进程打开的文件描述是有一定限制的,它由 FD_SETSIZE 设置,默认值是 1024,采用数组存储
另外在检查数组中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些 socket 是不是活 跃的,都轮询一遍,所以效率比较低
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; //文件描述符
short events; //要监视的事件
short revents; //实际发生的事件 };
poll 使用一个 pollfd 的指针实现,pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参 数-值”传递的方式。
优点:
采样链表的形式存储,它监听的描述符数量没有限制,可以超过 select 默认限制的 1024 大小
缺点:
另外在检查链表中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些 socket 是不是活 跃的,都轮询一遍,所以效率比较低。
epoll
epoll 是在 linux2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,epoll 更 加灵活,没有描述符限制。
epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件 存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。
int epoll_create(int size)
创建一个 epoll 的句柄。自从 linux2.6.8 之后,size 参数是被忽略的。
需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看/proc/进程 id/fd/,是能够看到这个 fd 的,所 以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
poll 的事件注册函数,它不同于 select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先 注册要监听的事件类型。
第一个参数是 epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
第三个参数是需要监听的 fd。
第四个参数是告诉内核需要监听什么事
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待内核返回的可读写事件,最多返回 maxevents 个事件。
优点:
epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,举个例子,在 1GB 内存的机器上大约 是 10 万左 右
效率提升, epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环 境 中, epoll 的效率就会远远高于 select 和 poll 。
epoll 使用了共享内存,不用做内存拷贝
kqueue
kqueue 是 unix 下的一个 IO 多路复用库。最初是 2000 年 Jonathan Lemon 在 FreeBSD 系统上开发的一个 高性能的事件通知接口。注册一批 socket 描述符到 kqueue 以后,当其中的描述符状态发生变化时, kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了。
优点: 能处理大量数据,性能较高
文件事件分派器
在 redis 中,对于文件事件的处理采用了 Reactor 模型。采用的是 epoll 的实现方式。
Redis 在主循环中统一处理文件事件和时间事件,信号事件则由专门的 handler 来处理。
主循环
事件处理器
连接处理函数 acceptTCPHandler 当客户端向 Redis 建立 socket 时,aeEventLoop 会调用 acceptTcpHandler 处理函数,服务器会为每 个链接创建一个 Client 对象,并创建相应文件事件来监听 socket 的可读事件,并指定事件处理函数。
请求处理函数 readQueryFromClient
当客户端通过 socket 发送来数据后,Redis 会调用 readQueryFromClient 方法,readQueryFromClient 方法会调用 read 方法从 socket 中读取数据到输入缓冲区中,然后判断其大小是否大于系统设置的 client_max_querybuf_len,如果大于,则向 Redis 返回错误信息,并关闭 client。
命令回复处理器 sendReplyToClient
sendReplyToClient 函数是 Redis 的命令回复处理器,这个处理器负责将服务器执行命令后得到的命令 回复通过套接字返回给客户端。
1、将 outbuf 内容写入到套接字描述符并传输到客户端
2、aeDeleteFileEvent 用于删除 文件写事件
4.8 时间事件
时间事件分为定时事件与周期事件:
一个时间事件主要由以下三个属性组成:
id(全局唯一 id)
when (毫秒时间戳,记录了时间事件的到达时间)
timeProc(时间事件处理器,当时间到达时,Redis 就会调用相应的处理器来处理事件)
serverCron
时间事件的最主要的应用是在 redis 服务器需要对自身的资源与配置进行定期的调整,从而确保服务器的 长久运行,这些操作由 redis.c 中的 serverCron 函数实现。该时间事件主要进行以下操作:
1)更新 redis 服务器各类统计信息,包括时间、内存占用、数据库占用等情况。
2)清理数据库中的过期键值对。
3)关闭和清理连接失败的客户端。
4)尝试进行 aof 和 rdb 持久化操作。
5)如果服务器是主服务器,会定期将数据向从服务器做同步操作。
6)如果处于集群模式,对集群定期进行同步与连接测试操作。
redis 服务器开启后,就会周期性执行此函数,直到 redis 服务器关闭为止。默认每秒执行 10 次,平 均 100 毫秒执行一次,可以在 redis 配置文件的 hz 选项,调整该函数每秒执行的次数。
server.hz
serverCron 在一秒内执行的次数 , 在 redis/conf 中可以配置
hz 10
#10次/秒 100毫秒一次
比如:server.hz 是 100,也就是 servreCron 的执行间隔是 10ms
run_with_period
#define run_with_period(_ms_) \
if ((_ms_ <= 1000/server.hz) || !(server.cronloops%((_ms_)/(1000/server.hz))))
定时任务执行都是在 10 毫秒的基础上定时处理自己的任务(run_with_period(ms)),即调用 run_with_period(ms)[ms 是指多长时间执行一次,单位是毫秒]来确定自己是否需要执行。
返回 1 表示执行。
假如有一些任务需要每 500ms 执行一次,就可以在 serverCron 中用 run_with_period(500)把每 500ms 需 要执行一次的工作控制起来。
定时事件
定时事件:让一段程序在指定的时间之后执行一次
aeTimeProc(时间处理器)的返回值是 AE_NOMORE
该事件在达到后删除,之后不会再重复。
周期性事件
周期性事件:让一段程序每隔指定时间就执行一次
aeTimeProc(时间处理器)的返回值不是 AE_NOMORE
当一个时间事件到达后,服务器会根据时间处理器的返回值,对时间事件的 个事件在一段时间后再次达到。
serverCron 就是一个典型的周期性事件。
4.9 aeEventLoop
aeEventLoop 是整个事件驱动的核心,Redis 自己的事件处理机制
它管理着文件事件表和时间事件列表,
不断地循环处理着就绪的文件事件和到期的时间事件。
初始化
Redis 服务端在其初始化函数 initServer 中,会创建事件管理器 aeEventLoop 对象。
函数 aeCreateEventLoop 将创建一个事件管理器,主要是初始化 aeEventLoop 的各个属性值,比如
events 、 fired 、 timeEventHead 和 apidata :
首先创建 aeEventLoop 对象。
初始化注册的文件事件表、就绪文件事件表。
events 指针指向注册的文件事件表、 fired 指针指向就绪文件事件表。
表的内容在后面添加具体事件时进行初变更。
初始化时间事件列表,设置 timeEventHead 和 timeEventNextId 属性。
调用 aeApiCreate 函数创建 epoll 实例,并初始化 apidata 。
stop
停止标志,1 表示停止,初始化为 0。
文件事件: events, fired, apidata
aeFileEvent 结构体为已经注册并需要监听的事件的结构体。
aeFiredEvent:已就绪的文件事件
typedef struct aeFiredEvent {
// 已就绪文件描述符
int fd;
// 事件类型掩码,
// 值可以是 AE_READABLE 或 AE_WRITABLE
// 或者是两者的或
int mask;
} aeFiredEvent;
void *apidata:
在 ae 创建的时候,会被赋值为 aeApiState 结构体,结构体的定义如下:
typedef struct aeApiState {
// epoll_event 实例描述符
int epfd;
// 事件槽
struct epoll_event *events;
} aeApiState;
这个结构体是为了 epoll 所准备的数据结构。redis 可以选择不同的 io 多路复用方法。因此 apidata 是个 void 类型,根据不同的 io 多路复用库来选择不同的实现
时间事件: timeEventHead, beforesleep, aftersleep
aeTimeEvent 结构体为时间事件,Redis 将所有时间事件都放在一个无序链表中,每次 Redis 会遍历整 个链表,查找所有已经到达的时间事件,并且调用相应的事件处理器。
beforesleep 对象是一个回调函数,在 redis-server 初始化时已经设置好了。
功能:
检测集群状态
随机释放已过期的键
在数据同步复制阶段取消客户端的阻塞
处理输入数据,并且同步副本信息
处理非阻塞的客户端请求
AOF 持久化存储策略,类似于 mysql 的 bin log
使用挂起的输出缓冲区处理写入
aftersleep 对象是一个回调函数,在 IO 多路复用与 IO 事件处理之间被调用。
4.10 aeMain
aeMain 函数其实就是一个封装的 while 循环,循环中的代码会一直运行直到 eventLoop 的 stop 被设 置为 1(true)。
它会不停尝试调用 aeProcessEvents 对可能存在的多种事件进行处理,而 aeProcessEvents 就是实际用于处理事件的函数。
aemain 函数中,首先调用 Beforesleep。这个方法在 Redis 每次进入 sleep/wait 去等待监听的端口发生 I/O 事件之前被调用。当有事件发生时,调用 aeProcessEvent 进行处理。
4.11 aeProcessEvent
首先计算距离当前时间最近的时间事件,以此计算一个超时时间; 然后调用 aeApiPoll 函数去等待底层的 I/O 多路复用事件就绪; aeApiPoll 函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。
计算最早时间事件的执行时间,获取文件时间可执行时间
aeSearchNearestTimer
aeProcessEvents 都会先 计算最近的时间事件发生所需要等待的时间 ,然后调用 aeApiPoll 方法在这 段时间中等待事件的发生,在这段时间中如果发生了文件事件,就会优先处理文件事件,否则就会一直 等待,直到最近的时间事件需要触发
堵塞等待文件事件产生
aeApiPoll 用到了 epoll,select,kqueue 和 evport 四种实现方式。
处理文件事件
rfileProc 和 wfileProc 就是在文件事件被创建时传入的函数指针
处理读事件:rfileProc
处理写事件:wfileProc
处理时间事件
processTimeEvents
取得当前时间,循环时间事件链表,如果当前时间 >=预订执行时间,则执行时间处理函数。
第四部分 Redis 企业实战
第一节 架构设计
1.1 多级缓存
缓存的设计要分多个层次,在不同的层次上选择不同的缓存,包括 JVM 缓存、文件缓存和 Redis 缓存
JVM 缓存
JVM 缓存就是本地缓存,设计在应用服务器中(tomcat)。
通常可以采用 Ehcache 和 Guava Cache,在互联网应用中,由于要处理高并发,通常选择 Guava Cache。
适用本地(JVM)缓存的场景:
1、对性能有非常高的要求。
2、不经常变化
3、占用内存不大
4、有访问整个集合的需求
5、数据允许不时时一致
文件缓存
这里的文件缓存是基于 http 协议的文件缓存,一般放在 nginx 中。 因为静态文件(比如 css,js, 图片)中,很多都是不经常更新的。nginx 使用 proxy_cache 将用户的请 求缓存到本地一个目录。下一个相同请求可以直接调取缓存文件,就不用去请求服务器了。
server {
listen 80 default_server;
server_name localhost;
root /mnt/blog/;
location / {
}
#要缓存文件的后缀,可以在以下设置。
location ~ .*\.(gif|jpg|png|css|js)(.*) {
proxy_pass http://ip地址:90;
proxy_redirect off;
proxy_set_header Host $host;
proxy_cache cache_one;
proxy_cache_valid 200 302 24h;
proxy_cache_valid 301 30d;
proxy_cache_valid any 5m;
expires 90d;
add_header wall "hello lagou.";
}
}
12345678910111213141516171819
Redis 缓存
分布式缓存,采用主从 + 哨兵或 RedisCluster 的方式缓存数据库的数据。
在实际开发中
作为数据库使用,数据要完整
作为缓存使用 作为 Mybatis 的二级缓存使用
1.2 缓存的介绍
缓存的大小
GuavaCache 的缓存设置方式:
CacheBuilder.newBuilder().maximumSize(num) // 超过num会按照LRU算法来移除缓存
1
Nginx 的缓存设置方式:
http {
...
proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
proxy_cache mycache;
location / {
proxy_pass http://localhost:8000;
}
}
}
12345678910
Redis 缓存设置:
maxmemory=num
# 最大缓存量 一般为内存的3/4
maxmemory-policy allkeys lru
123
缓存淘汰策略的选择
allkeys-lru : 在不确定时一般采用策略。
volatile-lru : 比 allkeys-lru 性能差 存 : 过期时间
allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)
自己控制:volatile-ttl 缓存穿透
禁止驱逐 用作 DB 不设置 maxmemory
key 数量
官方说 Redis 单例能处理 key:2.5 亿个
一个 key 或是 value 大小最大是 512M
读写峰值
Redis 采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由 C 语言编写,官方提供的数据是 可以达到 110000+ 的 QPS(每秒内查询次数)。80000 的写
命中率
命中:可以直接通过缓存获取到需要的数据。
不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其它的操作。
原因可能是 由于缓存中根本不存在,或者缓存已经过期。
通常来讲,缓存的命中率越高则表示使用缓存的收益越高,应用的性能越好(响应时间越短、吞吐量越 高),抗并发的能力越强。 由此可见,在高并发的互联网系统中,缓存的命中率是至关重要的指标。 通过 info 命令可以监控服务器状态
# Server
redis_version:5.0.5
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:e188a39ce7a16352
redis_mode:standalone
os:Linux 3.10.0-229.el7.x86_64 x86_64
arch_bits:64
#缓存命中
keyspace_hits:1000
#缓存未命中
keyspace_misses:20
used_memory:433264648
expired_keys:1333536
evicted_keys:1547380
命中率=1000/1000+20=83%
一个缓存失效机制,和过期时间设计良好的系统,命中率可以做到 95% 以上。
影响缓存命中率的因素:
1、缓存的数量越少命中率越高,比如缓存单个对象的命中率要高于缓存集合
2、过期时间越长命中率越高
3、缓存越大缓存的对象越多,则命中的越多
过期策略
Redis 的过期策略是定时删除 + 惰性删除
性能监控指标
利用 info 命令就可以了解 Redis 的状态了,主要监控指标有:
connected_clients:68 #连接的客户端数量
used_memory_rss_human:847.62M #系统给 redis 分配的内存
used_memory_peak_human:794.42M #内存使用的峰值大小
total_connections_received:619104 #服务器已接受的连接请求数量
instantaneous_ops_per_sec:1159 #服务器每秒钟执行的命令数量 qps
instantaneous_input_kbps:55.85 #redis 网络入口 kps
instantaneous_output_kbps:3553.89 #redis 网络出口 kps
rejected_connections:0 #因为最大客户端数量限制而被拒绝的连接请求数量
expired_keys:0 #因为过期而被自动删除的数据库键数量
evicted_keys:0 #因为最大内存容量限制而被驱逐(evict)的键数量
keyspace_hits:0 #查找数据库键成功的次数
keyspace_misses:0 #查找数据库键失败的次数
Redis 监控平台:
grafana、prometheus 以及 redis_exporter。
缓存预热
缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询 数据库,然后再将数据缓存的问题。用户直接查询实现被预热的缓存数据。
加载缓存思路:
数据量不大,可以在项目启动的时候自动进行加载 利用定时任务刷新缓存,将数据库的数据刷新到缓存中
第二节 缓存问题
2.1 缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端系统查找(比如 DB)。
缓存穿透是指在高并发下查询 key 不存在的数据(不存在的 key),会穿过缓存查询数据库。导致数据库 压力过大而宕机
解决方案:
对查询结果为空的情况也进行缓存
缓存时间(ttl)设置短一点,或者该 key 对应的数据 insert 了 之后清理缓存。
问题:缓存太多空值占用了更多的空间
使用布隆过滤器
在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否 存在,如果不存在就直接返回,存在再查缓存和 DB。
布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。
它实际上是一个很长的二进制向量和一系列随机 hash 映射函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般 的算法。
布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个数组中的 K 个点,把它们置为 1。
检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如 果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的 基本思想。
把字符串------> 位 省空间 (1 或 0)
不用循环------> 比较位置 省时间
2.2 缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如 DB)带来很大压力。 突然间大量的 key 失效了或 redis 重启,大量访问数据库,数据库崩溃
大量的 key 失效,导致直接访问 DB 的请求瞬间增多
解决方案:
1、 key 的失效期分散开 不同的 key 设置不同的有效期
2、设置二级缓存(数据不一定一致)
3、高可用(脏读)
2.3 缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热 点”的数据。
这个时候,需要考虑一个问题:缓存被“击穿”的问题
这个和缓存雪崩的区别在于这里针对 某一 key 缓存,前者则是很多 key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓 存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。
某一刻热点的 key 失效,大量请求打到 DB
解决方案:
1、用分布式锁控制访问的线程
使用 redis 的 setnx 互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数 据库。
2、不设超时时间,采用 volatile-lru 但会造成写一致问题
当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的 不一致,应用会从缓存中读取到脏数据。
可采用延时双删策略处理,也就是删除两次。
2.4 数据不一致
缓存和 DB 的数据不一致的根源 : 数据源不一样
如何解决
强一致性很难,追求最终一致性(时间)
互联网业务数据处理的特点 高吞吐量 低延迟 数据敏感性低于金融业 时序控制是否可行?
先更新数据库再更新缓存或者先更新缓存再更新数据库 本质上不是一个原子操作,所以时序控制不可行
高并发情况下会产生不一致
保证数据的最终一致性(延时双删)
1、先更新数据库同时删除缓存项(key),等读的时候再填充缓存
2、2 秒后再删除一次缓存项(key)
3、设置缓存过期时间 Expired Time 比如 10 秒 或 1 小时
4、将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长 7*24)
升级方案
通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog 日志采集发送到 MQ 中,然后通过 ACK 机 制确认处理删除缓存。
2.5 数据并发竞争
这里的并发指的是多个 redis 的 client 同时 set 同一个 key 引起的并发问题。 多客户端(Jedis)同时并发写一个 key,一个 key 的值是 1,本来按顺序修改为 2,3,4,最后是 4,但是顺 序变成了 4,3,2,最后变成了 2。
第一种方案:分布式锁 + 时间戳
1、整体技术方案
这种情况,主要是准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作。 加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。
2、Redis 分布式锁的实现
主要用到的 redis 函数是 setnx()
用 SETNX 实现分布式锁
时间戳
由于上面举的例子,要求 key 的操作需要顺序执行,所以需要保存一个时间戳判断 set 顺序。
系统 A key 1 {ValueA 7:00}
系统 B key 1 { ValueB 7:05}
假设系统 B 先抢到锁,将 key1 设置为{ValueB 7:05}。接下来系统 A 抢到锁,发现自己的 key1 的时间戳早 于缓存中的时间戳(7:00<7:05),那就不做 set 操作了。
第二种方案:利用消息队列
在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。
把 Redis 的 set 操作放在队列中使其串行化,必须一个一个执行。
Hot Key
当有大量的请求(几十万)访问某个 Redis 某个 key 时,由于流量集中达到网络上限,从而导致这个 redis 的 服务器宕机。造成缓存击穿,接下来对这个 key 的访问将直接访问数据库造成数据库崩溃,或者访问数 据库回填 Redis 再访问 Redis,继续崩溃。
如何发现热 key
1、预估热 key,比如秒杀的商品、火爆的新闻等
2、在客户端进行统计,实现简单,加一行代码即可
3、如果是 Proxy,比如 Codis,可以在 Proxy 端收集
4、利用 Redis 自带的命令,monitor、hotkeys。但是执行缓慢(不要用)
5、利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如 Storm、Spark Streaming、Flink,这些技术都是可以的。发现热点数据后可以写到 zookeeper 中
如何处理热 Key:
1、变分布式缓存为本地缓存
发现热 key 后,把缓存数据取出后,直接加载到本地缓存中。可以采用 Ehcache、Guava Cache 都可 以,这样系统在访问热 key 数据时就可以直接访问自己的缓存了。(数据不要求时时一致)
2、在每个 Redis 主节点上备份热 key 数据,这样在读取时可以采用随机读取的方式,将访问压力负载到 每个 Redis 上。
3、利用对热点数据访问的限流熔断保护措施
每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群, 直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(首页不行,系统友好性差)
通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群。
Big Key
大 key 指的是存储的值(Value)非常大,
常见场景:
热门话题下的讨论
大 V 的粉丝列表
序列化后的图片
没有及时处理的垃圾数据
…
大 key 的影响:
大 key 会大量占用内存,在集群中无法均衡
Redis 的性能下降,主从复制异常
在主动删除或过期删除时会操作时间过长而引起服务阻塞
如何发现大 key:
1、redis-cli --bigkeys 命令。可以找到某个实例 5 种数据类型(String、hash、list、set、zset)的最大 key。 但如果 Redis 的 key 比较多,执行该命令会比较慢
2、获取生产 Redis 的 rdb 文件,通过 rdbtools 分析 rdb 生成 csv 文件,再导入 MySQL 或其他数据库中进行 分析统计,根据 size_in_bytes 统计 bigkey
大 key 的处理:
优化 big key 的原则就是 string 减少字符串长度,list、hash、set、zset 等减少成员数。
1、string 类型的 big key,尽量不要存入 Redis 中,可以使用文档型数据库 MongoDB 或缓存到 CDN 上。
如果必须用 Redis 存储,最好单独存储,不要和其他的 key 一起存储。采用一主一从或多从。
2、单个简单的 key 存储的 value 很大,可以尝试将对象分拆成几个 key-value, 使用 mget 获取值,这样 分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对 redis 的 IO 影响。
如 hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。(常见)
以hash类型举例来说,对于field过多的场景,可以根据field进行hash取模,生成一个新的key,例如 原来的
hash_key:{filed1:value, filed2:value, filed3:value ...},
可以hash取模后形成如下 key:value形式
hash_key:1:{filed1:value}
hash_key:2:{filed2:value}
hash_key:3:{filed3:value} ...
取模后,将原先单个key分成多个key,每个key filed个数为原先的1/N
3、删除大 key 时不要使用 del,因为 del 是阻塞命令,删除时会影响性能。
4、使用 lazy delete (unlink 命令)
删除指定的 key(s),若 key 不存在则该 key 被跳过。但是,相比 DEL 会产生阻塞,该命令会在另一个线程中 回收内存,因此它是非阻塞的。 这也是该命令名字的由来:仅将 keys 从 key 空间中删除,真正的数据删 除会在后续异步操作。
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> UNLINK key1 key2 key3
(integer) 2
123456
第三节 缓存与数据库一致性
3.1 缓存更新策略
利用 Redis 的缓存淘汰策略被动更新 LRU 、LFU
利用 TTL 被动更新
在更新数据库时主动更新 (先更数据库再删缓存----延时双删)
异步更新 定时任务 数据不保证时时一致 不穿 DB
不同策略之间的优缺点
策略 | 一致性 | 维护成本 |
---|---|---|
利用 Redis 的缓存淘汰策略被动更新 | 最差 | 最低 |
利用 TTL 被动更新 | 较差 | 较低 |
在更新数据库时主动更新 | 较强 | 最高 |
3.2 与 Mybatis 整合
可以使用 Redis 做 Mybatis 的二级缓存,在分布式环境下可以使用。 框架采用 springboot+Mybatis+Redis。框架的搭建就不赘述了。
1、在 pom.xml 中添加 Redis 依赖
2、在 application.yml 中添加 Redis 配置
3、缓存实现 ApplicationContextHolder 用于注入 RedisTemplate
4、RedisCache 使用 redis 实现 mybatis 二级缓存
5、在 mapper 中增加二级缓存开启(默认不开启)
6、在启动时允许缓存
具体实现参考{{Redis 整合实现 Mybatis 二级缓存}}
第四节 分布式锁
4.1 利用 Watch 实现 Redis 乐观锁
乐观锁基于 CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消 耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用 redis 来实 现乐观锁。
具体思路如下:
1、利用 redis 的 watch 功能,监控这个 redisKey 的状态值
2、获取 redisKey 的值
3、创建 redis 事务
4、给这个 key 的值 +1
5、然后去执行这个事务,如果 key 的值被修改过则回滚,key 不加 1
Redis 乐观锁实现秒杀
4.2 setnx
实现原理
共享资源互斥
共享资源串行化
单应用中使用锁:(单进程多线程)
synchronized、ReentrantLock
分布式应用中使用锁:(多进程多线程)
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
利用 Redis 的单线程特性对共享资源进行串行化处理
实现方式
获取锁
释放锁
存在问题
单机 无法保证高可用
主–从 无法保证数据的强一致性,在主机宕机时会造成锁的重复获得。
无法续租 超过 expireTime 后,不能继续使用
本质分析
CAP 模型分析
在分布式环境下不可能满足三者共存,只能满足其中的两者共存,在分布式下 P 不能舍弃(舍弃 P 就是单 机了)。 所以只能是 CP(强一致性模型)和 AP(高可用模型)。
分布式锁是 CP 模型,Redis 集群是 AP 模型。 (base) Redis 集群不能保证数据的随时一致性,只能保证数据的最终一致性。 为什么还可以用 Redis 实现分布式锁?
与业务有关 当业务不需要数据强一致性时,比如:社交场景,就可以使用 Redis 实现分布式锁 当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账)就不要使 用 可以使用 CP 模型实现,比如:zookeeper 和 etcd。
4.3 Redisson 分布式锁的使用
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。
Redisson 在基于 NIO 的 Netty 框架上,生产环境使用分布式锁。
加入 jar 包的依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.7.0</version>
</dependency>
配置 Redisson
package com.galaxy.redision;
import org.redisson.Redisson;
import org.redisson.config.Config;
/**
* @author lane
* @date 2021年08月06日 下午3:05
*/
public class RedissonManager {
private static Config config = new Config(); //声明redisso对象
private static Redisson redisson = null;
//实例化redisson
static {
config.useClusterServers()
// 集群状态扫描间隔时间,单位是毫秒
.setScanInterval(2000)
//cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)
.addNodeAddress("redis://127.0.0.1:6379")
.addNodeAddress("redis://127.0.0.1:6380")
.addNodeAddress("redis://127.0.0.1:6381")
.addNodeAddress("redis://127.0.0.1:6382")
.addNodeAddress("redis://127.0.0.1:6383")
.addNodeAddress("redis://127.0.0.1:6384");
//得到redisson对象
redisson = (Redisson) Redisson.create(config);
}
//获取redisson对象的方法
public static Redisson getRedisson(){ return redisson; } }
业务逻辑中使用分布式锁
2221package com.galaxy.redision;
import java.io.IOException;
/**
* 业务逻辑中使用分布式锁
* @author lane
* @date 2021年08月06日 下午3:23
*/
public class UseRedision {
public String discount() throws IOException {
String key = "lock001";
//加锁
DistributedRedisLock.acquire(key);
//执行具体业务逻辑
System.out.println("执行业务逻辑成功");
// 释放锁
DistributedRedisLock.release(key);
//返回结果
return "soming";
}
}
Redisson 分布式锁的实现原理
加锁机制
如果该客户端面对的是一个 redis cluster 集群,他首先会根据 hash 节点选择一台机器。 发送 lua 脚本到 redis 服务器上,lua 的作用:保证这段复杂业务逻辑执行的原子性。
lua 脚本
锁互斥机制
那么在这个时候,如果客户端 2 来尝试加锁,执行了同样的一段 lua 脚本,会咋样呢?
很简单,第一个 if 判断会执行“exists myLock”,发现 myLock 这个锁 key 已经存在了。
接着第二个 if 判断,判断一下,myLock 锁 key 的 hash 数据结构中,是否包含客户端 2 的 ID,但是明显不 是的,因为那里包含的是客户端 1 的 ID。 所以,客户端 2 会获取到 pttl myLock 返回的一个数字,这个数字代表了 myLock 这个锁 key 的剩余生存时 间。比如还剩 15000 毫秒的生存时间。 此时客户端 2 会进入一个 while 循环,不停的尝试加锁。
自动延时机制
只要客户端 1 一旦加锁成功,就会启动一个 watch dog 看门狗,他是一个后台线程,会每隔 10 秒检查一 下,如果客户端 1 还持有锁 key,那么就会不断的延长锁 key 的生存时间。
可重入锁机制
第一个 if 判断肯定不成立,“exists myLock”会显示锁 key 已经存在了。 第二个 if 判断会成立,因为 myLock 的 hash 数据结构中包含的那个 ID,就是客户端 1 的那个 ID,也就是 “8743c9c0-0795-4907-87fd-6c719a6b4586:1” 此时就会执行可重入加锁的逻辑,他会用: incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1 通过这个命令,对客户端 1 的加锁次数,累加 1。数据结构会变成: myLock :{“8743c9c0-0795-4907-87fd-6c719a6b4586:1”:2 }
释放锁机制
每次都对 myLock 数据结构中的那个加锁次数减 1。 如果发现加锁次数是 0 了,说明这个客户端已经不再持有锁了,此时就会用: “del myLock”命令,从 redis 里删除这个 key。 然后呢,另外的客户端 2 就可以尝试完成加锁了。
4.4 分布式锁特性与应用
锁的特性
Version:0.9 StartHTML:0000000105 EndHTML:0000001765 StartFragment:0000000141 EndFragment:0000001725 互斥性
任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
同一性
锁只能被持有该锁的客户端删除,不能由其它客户端删除。
可重入性
持有某个锁的客户端可继续对该锁加锁,实现锁的续租
容错性
锁失效后(超过生命周期)自动释放锁(key 失效),其他客户端可以继续获得该锁,防止死锁
锁的应用
数据并发竞争
利用分布式锁可以将处理串行化。
防止库存超卖
订单 1 下单前会先查看库存,库存为 10,所以下单 5 本可以成功;
订单 2 下单前会先查看库存,库存为 10,所以下单 8 本可以成功;
订单 1 和订单 2 同时操作,共下单 13 本,但库存只有 10 本,显然库存不够了,这种情况称为库存超卖。
可以采用分布式锁解决这个问题。
订单 1 和订单 2 都从 Redis 中获得分布式锁(setnx),谁能获得锁谁进行下单操作,这样就把订单系统下单 的顺序串行化了,就不会出现超卖的情况了。伪码如下:
//加锁并设置有效期
if(redis.lock("RDL",200)){
//判断库存
if (orderNum<getCount()){
//加锁成功 ,可以下单
order(5);
//释放锁
redis,unlock("RDL"); }
}
注意此种方法会降低处理效率,这样不适合秒杀的场景,秒杀可以使用 CAS 和 Redis 队列的方式。
4.5 Zookeeper 分布式锁的对比
基于 Redis 的 set 实现分布式锁
基于 zookeeper 临时节点的分布式锁
基于 etcd 实现
三者的对比,如下表
Redis | zookeeper | etcd | |
---|---|---|---|
一致性算法 | 无 | paxos(ZAB) | raft |
CAP | AP | CP | CP |
高可用 | 主从集群 | n+1 (n 至少为 2) | n+1 |
接口类型 | 客户端 | 客户端 | http/grpc |
实现 | setNX | createEphemeral | restful API |
4.6 分布式集群架构中的 session 分离
传统的 session 是由 tomcat 自己进行维护和管理,但是对于集群或分布式环境,不同的 tomcat 管理各自 的 session,很难进行 session 共享,通过传统的模式进行 session 共享,会造成 session 对象在各个 tomcat 之间,通过网络和 Io 进行复制,极大的影响了系统的性能。
可以将登录成功后的 Session 信息,存放在 Redis 中,这样多个服务器(Tomcat)可以共享 Session 信息。 利用 spring-session-data-redis(SpringSession),可以实现基于 redis 来实现的 session 分离。这个 知识点在讲 Spring 的时候可以讲过了。
第五节 阿里 Redis 使用手册
主要介绍在使用阿里云 Redis 的开发规范,从下面几个方面进行说明。
键值设计
命令使用 ·
客户端使用 ·
相关工具
5.1 键值设计
1、key 名设计
可读性和可管理性
以业务名(或数据库名)为前缀(防止 key 冲突),用冒号分隔,比如业务名:表名:id
简洁性
保证语义的前提下,控制 key 的长度,当 key 较多时,内存占用也不容忽视
不要包含特殊字符 反例:包含空格、换行、单双引号以及其他转义字符
2、value 设计
拒绝 bigkey 防止网卡流量、慢查询,string 类型控制在 10KB 以内,hash、list、set、zset 元素个数不要超过 5000。 反例:一个包含 200 万个元素的 list。 拆解
选择适合的数据类型
控制 key 的生命周期
redis 不是垃圾桶,建议使用 expire 设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期 的数据重点关注 idletime。
5.2 命令使用
1、O(N)命令关注 N 的数量
例如 hgetall、lrange、smembers、zrange、sinter 等并非不能使用,但是需要明确 N 的值。有遍历的 需求可以使用 hscan、sscan、zscan 代替。
2、禁用命令
禁止线上使用 keys、flushall、flushdb 等,通过 redis 的 rename 机制禁掉命令,或者使用 scan 的方式渐 进式处理。
3、合理使用 select
redis 的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线 程处理,会有干扰。
4、使用批量操作提高效率
1.原生命令:例如 mget、mset。
2.非原生命令:可以使用 pipeline 提高效率。
但要注意控制一次批量操作的元素个数(例如 500 以内,实际也和元素字节数有关)。
注意两者不同:
1.原生是原子操作,pipeline 是非原子操作。
2.pipeline 可以打包不同的命令,原生做不到
3.pipeline 需要客户端和服务端同时支持。
5、不建议过多使用 Redis 事务功能 Redis 的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的 key 必须在一个 slot 上(可以使用 hashtag 功能解决)
6、Redis 集群版本在使用 Lua 上有特殊要求 所有 key 都应该由 KEYS 数组来传递 所有 key,必须在 1 个 slot 上,
7、monitor 命令 必要情况下使用 monitor 命令时,要注意不要长时间使用。
5.3 客户端使用
1、避免多个应用使用一个 Redis 实例 不相干的业务拆分,公共数据做服务化。
2、使用连接池 可以有效控制连接,同时提高效率,标准使用方式:
3、熔断功能 高并发下建议客户端添加熔断功能(例如 netflix hystrix)
4、合理的加密 设置合理的密码,如有必要可以使用 SSL 加密访问(阿里云 Redis 支持)
5、淘汰策略 根据自身业务类型,选好 maxmemory-policy(最大内存淘汰策略),设置好过期时间。 默认策略是 volatile-lru,即超过最大内存后,在过期键中使用 lru 算法进行 key 的剔除,保证不过期数据 不被删除,但是可能会出现 OOM 问题。
其他策略如下:
· allkeys-lru:根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
· allkeys-random:随机删除所有键,直到腾出足够空间为止。
· volatile-random: 随机删除过期键,直到腾出足够空间为止。
· volatile-ttl:根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。
· noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时 Redis 只响应读操作。
5.4 相关工具
1、数据同步 redis 间数据同步可以使用:redis-port
2、big key 搜索 redis 大 key 搜索工具
3、热点 key 寻找 内部实现使用 monitor,所以建议短时间使用 facebook 的 redis-faina 阿里云 Redis 已经在内核层面解决热点 key 问题
5.5 删除 bigkey
1.下面操作可以使用 pipeline 加速。
2.redis 4.0 已经支持 key 的异步删除,欢迎使用。
1、Hash 删除: hscan + hdel
2、List 删除: ltrim
3、Set 删除: sscan + srem
4、SortedSet 删除: zscan + zrem
第五部分 Redis 高可用方案
“高可用性”(High Availability)通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服 务的高度可用性。CAP 的 A AP 模型
单机的 Redis 是无法保证高可用性的,当 Redis 服务器宕机后,即使在有持久化的机制下也无法保证不丢 失数据。 所以我们采用 Redis 多机和集群的方式来保证 Redis 的高可用性。
单进程 + 单线程 + 多机 (集群)
第一节 主从复制
Redis 支持主从复制功能,可以通过执行 slaveof(Redis5 以后改成 replicaof)或者在配置文件中设置 slaveof(Redis5 以后改成 replicaof)来开启复制功能。
主对外从对内
主可写从不可写
主挂了,从不可为主
1.1 主从配置
主 Redis 配置
无需多余的配置
从 Redis 配置
修改从服务器上的 redis.conf 文件:
# slaveof <masterip> <masterport>
# 表示当前【从服务器】对应的【主服务器】的IP是192.168.10.135,端口是6379。
replicaof 127.0.0.1 6379
作用
读写分离
一主多从,主从同步
主负责写,从负责读
提升 Redis 的性能和吞吐量
主从的数据一致性问题
数据容灾
从机是主机的备份 主机宕机,从机可读不可写
默认情况下主机宕机后,从机不可为主机
利用哨兵可以实现主从切换,做到高可用
1.2 原理与实现
复制流程
保存主节点信息
当客户端向从服务器发送 slaveof(replicaof) 主机地址(127.0.0.1) 端口(6379)时:从服务器将主 机 ip(127.0.0.1)和端口(6379)保存到 redisServer 的 masterhost 和 masterport 中。
Struct redisServer{
char *masterhost;//主服务器ip
int masterport;//主服务器端口
} ;
从服务器将向发送 SLAVEOF 命令的客户端返回 OK,表示复制指令已经被接收,而实际上复制工作是在 OK 返回之后进行。
建立 socket 连接
slaver 与 master 建立 socket 连接 slaver 关联文件事件处理器 该处理器接收 RDB 文件(全量复制)、接收 Master 传播来的写命令(增量复制)
主服务器 accept 从服务器 Socket 连接后,创建相应的客户端状态。相当于从服务器是主服务器的 Client 端。
发送 ping 命令
Slaver 向 Master 发送 ping 命令
1、检测 socket 的读写状态
2、检测 Master 能否正常处理
Master 的响应:
1、发送“pong” , 说明正常
2、返回错误,说明 Master 不正常
3、timeout,说明网络超时
权限验证
主从正常连接后,进行权限验证 主未设置密码(requirepass=“”) ,从也不用设置密码(masterauth=“”) 主设置密码(requirepass!=“”),从需要设置密码(masterauth=主的 requirepass 的值) 或者从通过 auth 命令向主发送密码
发送端口信息
在身份验证步骤之后,从服务器将执行命令 REPLCONF listening-port ,向主服务器发送从服务器的监 听端口号。
同步数据
Redis 2.8 之后分为全量同步和增量同步,具体的后面详细讲解。
命令传播
当同步数据完成后,主从服务器就会进入命令传播阶段,主服务器只要将自己执行的写命令发送给从服 务器,而从服务器只要一直执行并接收主服务器发来的写命令。
同步数据集
Redis 2.8 以前使用 SYNC 命令同步复制 Redis 2.8 之后采用 PSYNC 命令替代 SYNC
旧版本 Redis 2.8 以前
没有全量同步和增量同步的概念,从服务器在同步时,会清空所有数据。
主从服务器断线后重复制,主服务器会重新生成 RDB 文件和重新记录缓冲区的所有命令,并全量同步到 从服务器上。
新版 Redis 2.8 以后
实现方式
在 Redis 2.8 之后使用 PSYNC 命令,具备完整重同步和部分重同步模式。 Redis 的主从同步,分为全量同步和增量同步。 只有从机第一次连接上主机是全量同步。 断线重连有可能触发全量同步也有可能是增量同步( master 判断 runid 是否一致)。
除此之外的情况都是增量同步。
全量同步
Redis 的全量同步过程主要分三个阶段:
同步快照阶段: Master 创建并发送快照 RDB 给 Slave , Slave 载入并解析快照。 此阶段所产生的新的写命令存储到缓冲区。
同步写缓冲阶段: Master 向 Slave 同步存储在缓冲区的写操作命令。
同步增量阶段: Master 向 Slave 同步写操作命令
增量同步
Redis 增量同步主要指 Slave 完成初始化后开始正常工作时, Master 发生的写操作同步到 Slave 的 过程。 通常情况下, Master 每执行一个写命令就会向 Slave 发送相同的写命令,然后 Slave 接收并执 行。
心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送命令:
replconf ack <replication_offset>
#ack :应答
#replication_offset:从服务器当前的复制偏移量
主要作用有三个:
1、检测主从的连接状态
检测主从服务器的网络连接状态
通过向主服务器发送 INFO replication 命令,可以列出从服务器列表,可以看出从最后一次向主发 送命令距离现在过了多少秒。lag 的值应该在 0 或 1 之间跳动,如果超过 1 则说明主从之间的连接有 故障。
2、辅助实现 min-slaves
Redis 可以通过配置防止主服务器在不安全的情况下执行写命令
min-slaves-to-write 3 (min-replicas-to-write 3 )
min-slaves-max-lag 10 (min-replicas-max-lag 10)
上面的配置表示:从服务器的数量少于 3 个,或者三个从服务器的延迟(lag)值都大于或等于 10 秒时,主服务器将拒绝执行写命令。这里的延迟值就是上面 INFOreplication 命令的 lag 值。
3、检测命令丢失
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发 送 REPLCONF ACK 命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量, 然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数 据,并将这些数据重新发送给从服务器。(补发) 网络不断
增量同步:网断了,再次连接时
第二节 哨兵模式
哨兵(sentinel)是 Redis 的高可用性(High Availability)的解决方案:
由一个或多个 sentinel 实例组成 sentinel 集群可以监视一个或多个主服务器和多个从服务器。
当主服务器进入下线状态时,sentinel 可以将该主服务器下的某一从服务器升级为主服务器继续提供服 务,从而保证 redis 的高可用性。
2.1 部署方案实现
{{Redis 搭建主从加哨兵}}
2.2 执行流程
启动并初始化 Sentinel
Sentinel 是一个特殊的 Redis 服务器不会进行持久化
Sentinel 实例启动后 每个 Sentinel 会创建 2 个连向主服务器的网络连接
命令连接:用于向主服务器发送命令,并接收响应;
订阅连接:用于订阅主服务器的—sentinel—:hello 频道。
获取主服务器信息
Sentinel 默认每 10s 一次,向被监控的主服务器发送 info 命令,获取主服务器和其下属从服务器的信息。
127.0.0.1:6382> info
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6381,state=online,offset=630363,lag=0
slave1:ip=127.0.0.1,port=6379,state=online,offset=630230,lag=1
master_replid:48a0b03ad4717e90283a7761d8324ab4b1c640ee
master_replid2:1ad13cc44397824972b9de8429aadab2a90f6e05
master_repl_offset:630363
second_repl_offset:226608
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:
获取从服务器信息
当 Sentinel 发现主服务器有新的从服务器出现时,Sentinel 还会向从服务器建立命令连接和订阅连接。 在命令连接建立之后,Sentinel 还是默认 10s 一次,向从服务器发送 info 命令,并记录从服务器的信息。
127.0.0.1:6381> info
# Replication
role:slave
master_host:127.0.0.1
master_port:6382
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:621529
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:48a0b03ad4717e90283a7761d8324ab4b1c640ee
master_replid2:1ad13cc44397824972b9de8429aadab2a90f6e05
master_repl_offset:621529
second_repl_offset:226608
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:621529
向主服务器和从服务器发送消息(以订阅的方式)
默认情况下,Sentinel 每 2s 一次,向所有被监视的主服务器和从服务器所订阅的—sentinel—:hello 频道 上发送消息,消息中会携带 Sentinel 自身的信息和主服务器的信息。
PUBLISH _sentinel_:hello
"< s_ip > < s_port >< s_runid >< s_epoch > < m_name > < m_ip >< m_port ><m_epoch>"
接收来自主服务器和从服务器的频道信息
当 Sentinel 与主服务器或者从服务器建立起订阅连接之后,Sentinel 就会通过订阅连接,向服务器发送 以下命令:
subscribe —sentinel—:hello
Sentinel 彼此之间只创建命令连接,而不创建订阅连接,因为 Sentinel 通过订阅主服务器或从服务器, 就可以感知到新的 Sentinel 的加入,而一旦新 Sentinel 加入后,相互感知的 Sentinel 通过命令连接来通信 就可以了。
检测主观下线状态
Sentinel 每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他 Sentinel)发送 PING 命令
实例在 down-after-milliseconds 毫秒内返回无效回复(除了 +PONG、-LOADING、-MASTERDOWN 外)
实例在 down-after-milliseconds 毫秒内无回复(超时)
Sentinel 就会认为该实例主观下线(SDown)
检查客观下线状态
当一个 Sentinel 将一个主服务器判断为主观下线后
Sentinel 会向同时监控这个主服务器的所有其他 Sentinel 发送查询命令
主机的
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
其他 Sentinel 回复
<down_state>< leader_runid >< leader_epoch >
判断它们是否也认为主服务器下线。
如果达到 Sentinel 配置中的 quorum 数量的 Sentinel 实例都判断主服 务器为主观下线,则该主服务器就会被判定为客观下线
选举 Leader Sentinel
当一个主服务器被判定为客观下线后,监视这个主服务器的所有 Sentinel 会通过选举算法(raft),选 出一个 Leader Sentinel 去执行 failover(故障转移)操作。
2.3 哨兵 leader 选举
Raft 协议
Raft 协议是用来解决分布式系统一致性问题的协议。
Raft 协议描述的节点共有三种状态:Leader, Follower, Candidate。
term:Raft 协议将时间切分为一个个的 Term(任期),可以认为是一种“逻辑时间”。
选举流程:
Raft 采用心跳机制触发 Leader 选举
系统启动后,全部节点初始化为 Follower,term 为 0。
节点如果收到了 RequestVote 或者 AppendEntries,就会保持自己的 Follower 身份
节点如果一段时间内没收到 AppendEntries 消息,在该节点的超时时间内还没发现 Leader,Follower 就 会转换成 Candidate,自己开始竞选 Leader。
一旦转化为 Candidate,该节点立即开始下面几件事情:
- 增加自己的 term。
- 启动一个新的定时器。
- 给自己投一票。
- 向所有其他节点发送 RequestVote,并等待其他节点的回复。
如果在计时器超时前,节点收到多数节点的同意投票,就转换成 Leader。同时向所有其他节点发送 AppendEntries,告知自己成为了 Leader。
每个节点在一个 term 内只能投一票,采取先到先得的策略,Candidate 前面说到已经投给了自己, Follower 会投给第一个收到 RequestVote 的节点。
Raft 协议的定时器采取随机超时时间,这是选举 Leader 的关键。
在同一个 term 内,先转为 Candidate 的节点会先发起投票,从而获得多数票。
Sentinel 的 leader 选举流程
1、某 Sentinel 认定 master 客观下线后,该 Sentinel 会先看看自己有没有投过票,如果自己已经投过票 给其他 Sentinel 了,在一定时间内自己就不会成为 Leader。
2、如果该 Sentinel 还没投过票,那么它就成为 Candidate。
3、Sentinel 需要完成几件事情:
- 更新故障转移状态为 start
- 当前 epoch 加 1,相当于进入一个新 term,在 Sentinel 中 epoch 就是 Raft 协议中的 term。
- 向其他节点发送 is-master-down-by-addr 命令请求投票。命令会带上自己的 epoch。
- 给自己投一票(leader、leader_epoch)
4、当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;(通过判断 epoch)
5、Candidate 会不断的统计自己的票数,直到他发现认同他成为 Leader 的票数超过一半而且超过它配 置的 quorum,这时它就成为了 Leader。
6、其他 Sentinel 等待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下 线的标识。
2.4 故障转移
当选举出 Leader Sentinel 后,Leader Sentinel 会对下线的主服务器执行故障转移操作,主要有三个步 骤:
- 它会将失效 Master 的其中一个 Slave 升级为新的 Master, 并让失效 Master 的其他 Slave 改为复制新的 Master ;
- 当客户端试图连接失效的 Master 时,集群也会向客户端返回新 Master 的地址,使得集群可以使 用现在的 Master 替换失效 Master 。
- Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即, Master 主服务器的 redis.conf 配置文件中会多一行 replicaof 的配置, sentinel.conf 的监控目标会随之调换。
2.5 主服务器的选择
哨兵 leader 根据以下规则从客观下线的主服务器的从服务器中选择出新的主服务器。
- 过滤掉主观下线的节点
- 选择 slave-priority 最高的节点,如果有则返回没有就继续选择
- 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整,如果有就返回了,没 有就继续
- 选择 run_id 最小的节点,因为 run_id 越小说明重启次数越少
第三节 集群与分区
分区是将数据分布在多个 Redis 实例(Redis 主机)上,以至于每个实例只包含一部分数据。
3.1 分区的意义
性能的提升
单机 Redis 的网络 I/O 能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力 可网络带宽,有助于提高 Redis 总体的服务能力。
存储能力的横向扩展
即使 Redis 的服务能力能够满足应用需求,但是随着存储数据的增加,单台机器受限于机器本身的存储 容量,将数据分散到多台机器上存储使得 Redis 服务可以横向扩展。
3.2 分区的方式
根据分区键(id)进行分区:
范围分区
根据 id 数字的范围比如 1–10000、100001–20000…90001-100000,每个范围分到不同的 Redis 实例 中
id 范围 | Redis 实例 |
---|---|
1–10000 | Redis01 |
100001–20000 | Redis02 |
… | |
90001-100000 | Redis10 |
好处:
实现简单,方便迁移和扩展
缺陷:
热点数据分布不均,性能损失
非数字型 key,比如 uuid 无法使用(可采用雪花算法替代)
分布式环境 主键 雪花算法 是数字 能排序
hash 分区
利用简单的 hash 算法即可: Redis 实例=hash(key)%N
key:要进行分区的键,比如 user_id N:Redis 实例个数(Redis 主机)
好处: 支持任何类型的 key 热点分布较均匀,性能较好
缺陷: 迁移复杂,需要重新计算,扩展较差(利用一致性 hash 环)
3.3 client 端分区
对于一个给定的 key,客户端直接选择正确的节点来进行读写。许多 Redis 客户端都实现了客户端分区 (JedisPool),也可以自行编程实现。
部署方案
客户端选择算法
hash
普通 hash
hash(key)%N
hash:可以采用 hash 算法,比如 CRC32、CRC16 等
N:是 Redis 主机个数
user_id : u001
hash(u001) : 1844213068
Redis实例=1844213068%3
余数为2,所以选择Redis3。
普通Hash的优势
普通 Hash 的优势
实现简单,热点数据分布均匀
普通 Hash 的缺陷
节点数固定,扩展的话需要重新计算
查询时必须用分片的 key 来查,一旦 key 改变,数据就查不出了,所以要使用不易改变的 key 进行分片
一致性 hash
基本概念
普通 hash 是对主机数量取模,而一致性 hash 是对 2^ 32 (4 294 967 296)
突然想到了比特币的算法也是求 hash 值不过范围更加大是 2^ 256 次方,
2 的 256 次方:1.1579208923731619542357098500869 ×10 的 77 次方
求比当前 hash 值更小的 hash 概率可想而知,没有哪台机器可以轻易计算
一台机器亿万年每年计算亿万次数据小于亿万的概率仍低于亿万分之一
想象成一个圆,就像钟表一样,钟表的圆可以理解成由 60 个点组成的圆,而此处我们把这个圆想象成由 2^32 个 点组成的圆,示意图如下:
圆环的正上方的点代表 0,0 点右侧的第一个点代表 1,以此类推,2、3、4、5、6……直到 232-1
也就 是说 0 点左侧的第一个点代表 232-1。我们把这个由 2 的 32 次方个点组成的圆环称为 hash 环。
假设我们有 3 台缓存服务器,服务器 A、服务器 B、服务器 C,那么,在生产环境中,这三台服务器肯定 有自己的 IP 地址,我们使用它们各自的 IP 地址进行哈希计算,使用哈希后的结果对 2^32 取模,可以使用 如下公式:
hash( IP 地址) % 2^32
通过上述公式算出的结果一定是一个 0 到 2^32-1 之间的一个整数,我们就用算出的这个整数,代表服务 器 A、服务器 B、服务器 C,既然这个整数肯定处于 0 到 232-1 之间,那么,上图中的 hash 环上必定有一 个点与这个整数对应,也就是服务器 A、服务器 B、服务 C 就可以映射到这个环上,如下图:
假设,我们需要使用 Redis 缓存数据,那么我们使用如下公式可以将数据映射到上图中的 hash 环上。
hash(key) % 2^32
映射后的示意图如下,下图中的橘黄色圆形表示数据
现在服务器与数据都被映射到了 hash 环上,上图中的数据将会被缓存到服务器 A 上,因为从数据的位置 开始,沿顺时针方向遇到的第一个服务器就是 A 服务器,所以,上图中的数据将会被缓存到服务器 A 上。 如图:
将缓存服务器与被缓存对象都映射到 hash 环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第 一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器 hash 后的值是固定的,所 以,在服务器不变的情况下,数据必定会被缓存到固定的服务器上,那么,当下次想要访问这个数据 时,只要再次使用相同的算法进行计算,即可算出这个数据被缓存在哪个服务器上,直接去对应的服务 器查找对应的数据即可。多条数据存储如下:
优点
添加或移除节点时,数据只需要做部分的迁移,比如上图中把 C 服务器移除,则数据 4 迁移到服务器 A 中,而其他的数据保持不变。添加效果是一样的。
hash 环偏移
在介绍一致性哈希的概念时,我们理想化的将 3 台服务器均匀的映射到了 hash 环上。也就是说数据的范 围是 2^32/N。但实际情况往往不是这样的。有可能某个服务器的数据会很多,某个服务器的数据会很 少,造成服务器性能不平均。这种现象称为 hash 环偏移。
理论上我们可以通过增加服务器的方式来减少偏移,但这样成本较高,所以我们可以采用虚拟节点的方 式,也就是虚拟服务器,如图:
“虚拟节点"是"实际节点”(实际的物理服务器)在 hash 环上的复制品,一个实际节点可以对应多个虚拟节点。
从上图可以看出,A、B、C 三台服务器分别虚拟出了一个虚拟节点,当然,如果你需要,也可以虚拟出 更多的虚拟节点。引入虚拟节点的概念后,缓存的分布就均衡多了,上图中,1 号、3 号数据被缓存在服 务器 A 中,5 号、4 号数据被缓存在服务器 B 中,6 号、2 号数据被缓存在服务器 C 中,如果你还不放心,可 以虚拟出更多的虚拟节点,以便减小 hash 环偏斜所带来的影响,虚拟节点越多,hash 环上的节点就越 多,缓存被均匀分布的概率就越大。
缺点
复杂度高
客户端需要自己处理数据路由、高可用、故障转移等问题
使用分区,数据的处理会变得复杂,不得不对付多个 redis 数据库和 AOF 文件,不得在多个实例和主机之 间持久化你的数据。
不易扩展
一旦节点的增或者删操作,都会导致 key 无法在 redis 中命中,必须重新根据节点计算,并手动迁移全部 或部分数据。
3.4 proxy 端分区
在客户端和服务器端引入一个代理或代理集群,客户端将命令发送到代理上,由代理根据算法,将命令 路由到相应的服务器上。常见的代理有 Codis(豌豆荚)和 TwemProxy(Twitter)。
部署架构
Codis 由豌豆荚于 2014 年 11 月开源,基于 Go 和 C 开发,是近期涌现的、国人开发的优秀开源软件之一。
Codis 3.x 由以下组件组成:
Codis Server:基于 redis-3.2.8 分支开发。增加了额外的数据结构,以支持 slot 有关的操作以及 数据迁移指令。
Codis Proxy:客户端连接的 Redis 代理服务, 实现了 Redis 协议。 除部分命令不支持以外,表现 的和原生的 Redis 没有区别(就像 Twemproxy)。
对于同一个业务集群而言,可以同时部署多个 codis-proxy 实例; 不同 codis-proxy 之间由 codis-dashboard 保证状态同步。
Codis Dashboard:集群管理工具
支持 codis-proxy、codis-server 的添加、删除,以及据迁移 等操作。在集群状态发生改变时,codis-dashboard 维护集群下所有 codis-proxy 的状态的一致 性。对于同一个业务集群而言,同一个时刻 codis-dashboard 只能有 0 个或者 1 个; 所有对集群的修改都必须通过 codis-dashboard 完成。
Codis Admin:集群管理的命令行工具。
可用于控制 codis-proxy、codis-dashboard 状态以及访问外部存储。
Codis FE:集群管理界面。
多个集群实例共享可以共享同一个前端展示页面; 通过配置文件管理后端 codis-dashboard 列表,配置文件可自动更新。
Storage:为集群状态提供外部存储。
提供 Namespace 概念,不同集群的会按照不同 product name 进行组织; 目前仅提供了 Zookeeper、Etcd、Fs 三种实现,但是提供了抽象的 interface 可自行扩展。
分片原理
Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算 哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽 位。
Codis 的槽位和分组的映射关系就保存在 codis proxy 当中。
优点&缺点
优点
对客户端透明,与 codis 交互方式和 redis 本身交互一样
支持在线数据迁移,迁移过程对客户端透明有简单的管理和监控界面
支持高可用,无论是 redis 数据存储还是代理节点
自动进行数据的均衡分配
最大支持 1024 个 redis 实例,存储容量海量 高性能
缺点
采用自有的 redis 分支,不能与原版的 redis 保持同步
如果 codis 的 proxy 只有一个的情况下, redis 的性能会下降 20% 左右
某些命令不支持
3.5 官方 cluster 分区
Redis3.0 之后,Redis 官方提供了完整的集群解决方案。
方案采用去中心化的方式,包括:sharding(分区)、replication(复制)、failover(故障转移)。 称为 RedisCluster。
Redis5.0 前采用 redis-trib 进行集群的创建和管理,需要 ruby 支持
Redis5.0 可以直接使用 Redis-cli 进行集群的创建和管理
部署架构
去中心化
RedisCluster 由多个 Redis 节点组构成,是一个 P2P 无中心节点的集群架构,依靠 Gossip 协议传播的集群。
Gossip 协议
Gossipt 协议是一个通信协议,一种传播消息的方式。
起源于:病毒传播
Gossip 协议基本思想就是:
一个节点周期性(每秒)随机选择一些节点,并把信息传递给这些节点。
这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。
信息会周期性的传递给 N 个目标节点。这个 N 被称为 fanout(扇出)
gossip 协议包含多种消息,包括 meet、ping、pong、fail、publish 等等。
命令 | 说明 |
---|---|
meet | sender 向 receiver 发出,请求 receiver 加入 sender 的集群 |
ping | 节点检测其他节点是否在线 |
pong | receiver 收到 meet 或 ping 后的回复信息;在 failover 后,新的 Master 也会广播 pong |
fail | 节点 A 判断节点 B 下线后,A 节点广播 B 的 fail 信息,其他收到节点会将 B 节点标记为下线 |
publish | 节点 A 收到 publish 命令,节点 A 执行该命令,并向集群广播 publish 命令,收到 publish 命令的节点都会执行相同的 publish 命令 |
通过 gossip 协议,cluster 可以提供集群间状态同步更新、选举自助 failover 等重要的集群功能。
slot
redis-cluster 把所有的物理节点映射到[0-16383]个 slot 上,基本上采用平均分配和连续分配的方式。
比如上图中有 5 个主节点,这样在 RedisCluster 创建时,slot 槽可按下表分配:
节点名称 | slot 范围 |
---|---|
Redis1 | 0-3270 |
Redis2 | 3271-6542 |
Redis3 | 6543-9814 |
Redis4 | 9815-13087 |
Redis5 | 13088-16383 |
cluster 负责维护节点和 slot 槽的对应关系 value------>slot--------> 节点
当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把 结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数 量大致均等的将哈希槽映射到不同的节点。
比如: set name zhaoyun
hash(“name”)采用 crc16 算法,得到值:1324203551%16384=15903 根据上表 15903 在 13088-16383 之间,所以 name 被存储在 Redis5 节点。
slot 槽必须在节点上连续分配,如果出现不连续的情况,则 RedisCluster 不能工作,详见容错。
RedisCluster 的优势
高性能
Redis Cluster 的性能与单节点部署是同级别的。 多主节点、负载均衡、读写分离
高可用
Redis Cluster 支持标准的 主从复制配置来保障高可用和高可靠。
failover Redis Cluster 也实现了一个类似 Raft 的共识方式,来保障整个集群的可用性。
易扩展
向 Redis Cluster 中添加新节点,或者移除节点,都是透明的,不需要停机。 水平、垂直方向都非常容易扩展。
数据分区,海量数据,数据存储
原生
部署 Redis Cluster 不需要其他的代理或者工具,而且 Redis Cluster 和单机 Redis 几乎完全兼 容。
集群搭建
{{Redis 搭建集群高可用}}
注意
修改 redis.conf 配置文件,打开 cluster-enable yes
创建 Redis 集群时 Redis 里不要有数据
命令客户端连接集群 ./redis-cli -h 127.0.0.1 -p 7001 -c
查看集群状态 cluster info
查看集群中的节点 cluster nodes
3.5 cluster 的分片
分片
不同节点分组服务于相互无交集的分片(sharding),Redis Cluster 不存在单独的 proxy 或配置服务 器,所以需要将客户端路由到目标的分片。
客户端路由
Redis Cluster 的客户端相比单机 Redis 需要具备路由语义的识别能力,且具备一定的路由缓存能力。
moved 重定向
1.每个节点通过通信都会共享 Redis Cluster 中槽和集群中对应节点的关系
2.客户端向 Redis Cluster 的任意节点发送命令,接收命令的节点会根据 CRC16 规则进行 hash 运算与 16384 取余,计算自己的槽和对应节点
3.如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
4.如果保存数据的槽不在当前节点的管理范围内,则向客户端返回 moved 重定向异常
5.客户端接收到节点返回的结果,如果是 moved 异常,则从 moved 异常中获取目标节点的信息
6.客户端向目标节点发送命令,获取命令执行结果
[root@localhost bin]# ./redis-cli -h 127.0.0.1 -p 7001 -c
127.0.0.1:7001> set name:001 zhaoyun OK
127.0.0.1:7001> get name:001 "zhaoyun" [root@localhost bin]# ./redis-cli -h 127.0.0.1 -p 7002 -c
127.0.0.1:7002> get name:001
-> Redirected to slot [4354] located at 127.0.0.1:7001 "zhaoyun"
127.0.0.1:7001> cluster keyslot name:001 (integer) 4354
1234567891011
ask 重定向
在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移 当客户端向某个节点发送命令,节点向客户端返回 moved 异常,告诉客户端数据对应的槽的节点信息 如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁 移到别的节点了,就会返回 ask,这就是 ask 重定向机制
1.客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回 ask 转 向给客户端
2.客户端向新的节点发送 Asking 命令给新的节点,然后再次向新节点发送命令
3.新节点执行命令,把命令执行结果返回给客户端
moved 和 ask 的区别
1、moved:槽已确认转移
2、ask:槽还在转移过程中
Smart 智能客户端
JedisCluster
JedisCluster 是 Jedis 根据 RedisCluster 的特性提供的集群智能客户端
JedisCluster 为每个节点创建连接池,并跟节点建立映射关系缓存( Cluster slots )
JedisCluster 将每个主节点负责的槽位一一与主节点连接池建立映射缓存
JedisCluster 启动时,已经知道 key,slot 和 node 之间的关系,可以找到目标节点
JedisCluster 对目标节点发送命令,目标节点直接响应给 jedisCluster
如果 jedisCluster 与目标节点连接出错,则 jedisCluster 会知道连接的节点是一个错误的节点此时节点返回 moved 异常给 jedisCluster
JedisCluster 会重新初始化 slot 与 node 节点的缓存关系,然后向新的目标节点发送命令,目标命令执行
命令并向 JedisCluster 响应
如果命令发送次数超过 5 次,则抛出异常"Too many cluster redirection!"
JedisPoolconfig config = new JedisPoolConfigO);
Set<HostAndPort> iedisclusterNode = new Hashset<HostAndPort>();
jedisclusterNode. add(new HostAndPort("192.168.127.128", 7001));
jedisclusterNode. add(new HostAndport("192.168.127.128", 7002));
jedisclusterNode. add(new HostAndport("192.168.127.128", 7003));
jedisclusterNode. add(new HostAndPort("192.168.127.128", 7004));
iedisclusterNode. add(new HostAndport("192.168.127.128", 7005));
iedisclusterNode. add(new HostAndport("192.168.127.128", 7006));
Jediscluster jcd = new Jediscluster(jedisclusterNode, config);
jcd. set ("name: 001","zhangfei");
string value =jcd.get("name:001");
迁移
在 RedisCluster 中每个 slot 对应的节点在初始化后就是确定的。在某些情况下,节点和分片需要变更:
新的节点作为 master 加入;
某个节点分组需要下线;
负载不均衡需要调整 slot 分布。
此时需要进行分片的迁移,迁移的触发和过程控制由外部系统完成。包含下面 2 种:
节点迁移状态设置:迁移前标记源/目标节点。
key 迁移的原子化命令:迁移的具体步骤。
1、向节点 B 发送状态变更命令,将 B 的对应 slot 状态置为 importing。
2、向节点 A 发送状态变更命令,将 A 对应的 slot 状态置为 migrating。
3、向 A 发送 migrate 命令,告知 A 将要迁移的 slot 对应的 key 迁移到 B。
4、当所有 key 迁移完成后,cluster setslot 重新设置槽位。
3.6 cluster 扩容与缩容
扩容
参考{{Redis 搭建集群高可用}}中的扩容
注意
添加完主节点需要对主节点进行 hash 槽分配,这样该主节才可以存储数据。
- 第一步:连接上集群(连接集群中任意一个可用结点都行)
- 第二步:输入要分配的槽数量
- 第三步:输入接收槽的结点 id
- 第四步:输入源结点 id ,选择输入:all
- 第五步:输入 yes 开始移动槽到目标结点 id
添加从节点与缩容
./redis-cli --cluster add-node 新节点的ip和端口 旧节点ip和端口 --cluster-slave --cluster-master-id 主节点id
如果从节点以前是集群的一员则需要先删除生成的配置文件 nodes.conf
删除后再执行./redis-cli --cluster add-node 指令
集群缩容删除已经占有 hash 槽的主结点会失败
./redis-cli --cluster del-node 192.168.127.128:7008 6be94480315ab0dd2276a7f70c82c578535d6666
3.7 容灾(failover)
故障检测
集群中的每个节点都会定期地(每秒)向集群中的其他节点发送 PING 消息 如果在一定时间内(cluster-node-timeout),发送 ping 的节点 A 没有收到某节点 B 的 pong 回应,则 A 将 B 标识为 pfail。 A 在后续发送 ping 时,会带上 B 的 pfail 信息, 通知给其他节点。 如果 B 被标记为 pfail 的个数大于集群主节点个数的一半(N/2 + 1)时,B 会被标记为 fail,A 向整个集群 广播,该节点已经下线。 其他节点收到广播,标记 B 为 fail。
从节点选举
raft
每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数 据越多)的从节点,选举时间越靠前,优先进行选举。
slave 通过向其他 master 发送 FAILVOER_AUTH_REQUEST 消息发起竞选, master 收到后回复 FAILOVER_AUTH_ACK 消息告知是否同意。 slave 发送 FAILOVER_AUTH_REQUEST 前会将 currentEpoch 自增,并将最新的 Epoch 带入到 FAILOVER_AUTH_REQUEST 消息中,如果自己未投过票,则回复同意,否则回复拒绝。 所有的 Master 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
选举判定
1、offset 越大
2、currentEpoch 越大
RedisCluster 失效的判定:
1、集群中半数以上的主节点都宕机(无法投票)
2、宕机的主节点的从节点也同时宕机了(slot 槽分配不连续) 变更通知
变更通知
当 slave 收到过半的 master 同意时,会成为新的 master。此时会以最新的 Epoch 通过 PONG 消息广播 自己成为 master,让 Cluster 的其他节点尽快的更新拓扑结构(node.conf)。
主从切换
自动切换
就是上面讲的从节点选举
手动切换
人工故障切换是预期的操作,而非发生了真正的故障,目的是以一种安全的方式(数据无丢失)将当前 master 节点和其中一个 slave 节点(执行 cluster-failover 的节点)交换角色
1、向从节点发送 cluster failover 命令(slaveof no one)
2、从节点告知其主节点要进行手动切换(CLUSTERMSG_TYPE_MFSTART)
3、主节点会阻塞所有客户端命令的执行(10s)
4、从节点从主节点的 ping 包中获得主节点的复制偏移量
5、从节点复制达到偏移量,发起选举、统计选票、赢得选举、升级为主节点并更新配置
6、切换完成后,原主节点向所有客户端发送 moved 指令重定向到新的主节点
以上是在主节点在线情况下。
如果主节点下线了,则采用 cluster failover force 或 cluster failover takeover 进行强制切换。
副本漂移
我们知道在一主一从的情况下,如果主从同时挂了,那整个集群就挂了。
为了避免这种情况我们可以做一主多从,但这样成本就增加了。
Redis 提供了一种方法叫副本漂移,这种方法既能提高集群的可靠性又不用增加 22111 太多的从机。 如图:
Master1 宕机,则 Slaver11 提升为新的 Master1
集群检测到新的 Master1 是单点的(无从机) 集群从拥有最多的从机的节点组(Master3)中,选择节点名称字母顺序最小的从机(Slaver31)漂移 到单点的主从节点组(Master1)。
具体流程如下(以上图为例):
1、将 Slaver31 的从机记录从 Master3 中删除
2、将 Slaver31 的的主机改为 Master1
3、在 Master1 中添加 Slaver31 为从节点
4、将 Slaver31 的复制源改为 Master1
5、通过 ping 包将信息同步到集群的其他节点
结语
实际上早该学完了呢,中间花了近三天的时间搞了下 live2d + 音乐播放器的思源笔记模板和笔记主题 KnowledgeBrain 的大改,纠结了很久的标签的样式。
然后又花了一天时间再次研究了下 Trello ,一天时间再次研究了下 Notion 的所有功能,虽然我搭建的一个 Notion 模板可还没使用,感觉 Notion 还是比较适合个人管理汇总的,可玩性很高。
然后又花了两天时间来研究任务和日程应该如何选择什么软件,如何构建一个任务大脑
Todo 清单、微软 TODO、番茄 todo、滴答清单、Todolist、时光序、onmifocus
todo 清单和时光序应该是知乎上比较推荐的
头大
感觉
Trello + 日历 做日程 日历可以订阅 Trello 的一个日程项目日历,自动同步日程信息
Teambition 本来以为很垃圾的,没想到手机版是 Mac 端的 lite 版本,插件功能没有,但可以通过模板添加,因为一些模板的存在,如果不想动脑还是不错的项目任务时间管理软件
Trello 同样也适合
微软 todo 记临时,且会自动同步到 iphone 的提醒事项
微软 outlook 的日历可以在原生日历上添加,完全打通而不是订阅广播
每次开始选择规划都是 O(n 方)的复杂度,算是为了以后每次的 O(1),不知是否值得呢
然后整理了万本书籍资源存入网盘和本地 Timemachine 中,以后看一些文学名著类的书籍就不需要花费时间去搜集了,自控力正在看第二遍,感觉在任务定个第三遍开始的时间提醒,一本真正的好书值得多看几遍!
心态浮躁一两天,叹气一声!我还有很多东西没学呢!不能再这样花费时间不在主要工作当中啊