缓存原理&设计
生产中遇到的缓存问题
- 系统在某个时刻访问量剧增,造成数据库压力剧增甚至崩溃,怎么办?
- 什么是缓存雪崩,缓存穿透和缓存击穿,会造成什么问题,如何解决?
- 什么是大Key和热Key,会造成什么问题,如何解决?
- 如何保证redis中的数据都是热点数据?
- 缓存和数据库数据是不一致时,会造成什么问题,如何解决?
- 什么是数据并发竞争,会造成什么问题,如何解决?
- 单线程的Redis为什么这么快?
- Redis哨兵和集群的原理及选择?
- 在多机Redis使用时,如何保证主从服务器的数据一致性?
重点
- 理解缓存的使用场景
- 理解缓存原理
- 了解常见缓存及分类
- 理解服务器端缓存
- 了解缓存的优势和代价
- 理解缓存的读写模式
- 掌握缓存的常见设计思路并能够运用到项目中
缓存基本思想
缓存的使用场景
DB缓存,减轻服务器压力
一般情况下数据存在数据库中,应用程序直接操作数据库。当访问量上万,数据库压力增大,可以采取的方案有:读写分离,分库分表
;当访问量达到10万、百万,需要引入缓存
。将已经访问过的内容或数据存储起来,当再次访问时先找缓存,缓存命中返回数据。不命中再找数据库,并回填缓存。
提高系统响应
数据库的数据是存在文件里,也就是硬盘
。与内存做交换
(swap)。在大量瞬间访问时(高并发)MySQL单机会因为频繁IO而造成无法响应。MySQL的InnoDB是有行锁’
;将数据缓存在Redis中,也就是存在了内存中。内存天然支持高并发访问。可以瞬间处理大量请求。qps到达10万读请求
做Session分离
传统的session是由tomcat自己进行维护和管理。集群或分布式环境,不同的tomcat管理各自的session。只能在各个tomcat之间,通过网络和Io进行session的复制,极大的影响了系统的性能。将登录成功后的Session信息,存放在Redis中,这样多个服务器(Tomcat)可以共享Session信息。
做分布式锁(Redis)
- 一般讲锁是多线程的锁,是在一个进程中的;
- 多个进程(JVM)在并发时也会产生问题,也要控制时序性;
- 可以采用分布式锁。使用Redis实现
sexNX
做乐观锁(Redis)
- 同步锁和数据库中的行锁、表锁都是悲观锁
- 悲观锁的性能是比较低的,响应性比较差
- 高性能、高响应(秒杀)采用乐观锁
- Redis可以实现乐观锁
watch + incr
什么是缓存
缓存原指CPU上的一种高速存储器,它先于内存与CPU交换数据,速度很快。现在泛指存储在计算机上的原始数据的复制集,便于快速访问。在互联网技术中,缓存是系统快速响应的关键技术之一
大型网站中缓存的使用
单机架构LAMP(Linux+apache+MySQL+PHP)、JavaEE(SSM);访问量越大,响应力越差,用户体验越差
在大型网站中从浏览器到网络,再到应用服务器,再到数据库,通过在各个层面应用缓存技术,大大提升了系统性能和用户体验。
缓存分类
页面缓存
传统互联网:页面缓存和浏览器缓存
移动互联网:APP缓存
页面缓存
页面缓存:页面自身对某些元素或全部元素进行存储,并保存成文件。html5:Cookie、WebStorage(SessionStorage和LocalStorage)、WebSql、indexDB、Application、Cache等
开启步骤
1、设置manifest描述文件:CACHE MANIFEST #comment js/index.js img/bg.png
2、html关联manifest属性
浏览器缓存
当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。
浏览器缓存
可分为强制缓存
和协商缓存
。
强制缓存
:直接使用浏览器的缓存数据
条件
: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)中。比如图片文件。
网络端缓存
通过代理的方式响应客户端请求,对重复的请求返回缓存中的数据资源。
Web代理缓存
可以缓存原生服务器的静态资源,比如样式、图片等。常见的反向代理服务器比如大名鼎鼎的Nginx。
边缘缓存
边缘缓存中典型的商业化服务就是CDN了。CDN的全称是Content Delivery Network,即内容分发网络。CDN通过部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。现在一般的公有云服务商都提供CDN服务。
服务端缓存
服务器端缓存是整个缓存体系的核心。包括数据库级缓存、平台级缓存和应用级缓存。
数据库级缓存
数据库是用来存储和管理数据的。MySQL在Server层使用查询缓存机制。将查询后的数据缓存起来。K-V结构,Key:select语句的hash值,Value:查询结果。InnoDB存储引擎中的buffer-pool用于缓存InnoDB索引及数据块。
平台级缓存
平台级缓存指的是带有缓存特性的应用框架。比如:GuavaCache 、EhCache、OSCache等。部署在应用服务器上,也称为服务器本地缓存。
应用级缓存(重点)
具有缓存功能的中间件:Redis、Memcached、EVCache、Tair等。采用K-V形式存储。利用集群支持高可用、高性能、高并发、高扩展。
分布式缓存
缓存的优势、代价
使用缓存的优势
提升用户体验
用户体验(User Experience):用户在使用产品过程中建立起来的一种纯主观感受。缓存的使用可以提升系统的响应能力,大大提升了用户体验。
减轻服务器压力
客户端缓存、网络端缓存减轻应用服务器压力。服务端缓存减轻数据库服务器的压力。
提升系统性能
系统性能指标:响应时间、延迟时间、吞吐量、并发用户数和资源利用率等。
缓存技术可以:
缩短系统的响应时间
减少网络传输时间和应用延迟时间
提高系统的吞吐量
增加系统的并发用户数
提高了数据库资源的利用率
使用缓存的代价
额外的硬件支出
缓存是一种软件系统中以空间换时间的技术。需要额外的磁盘空间和内存空间来存储数据;搭建缓存服务器集群需要额外的服务器
采用云服务器的缓存服务就不用额外的服务器了;阿里云,百度云,提供缓存服务
高并发缓存失效
在高并发场景下会出现缓存失效(缓存穿透、缓存雪崩、缓存击穿)。造成瞬间数据库访问量增大,甚至崩溃
缓存与数据库数据同步
缓存与数据库无法做到数据的时时同步;Redis无法做到主从时时数据同步
缓存并发竞争
多个redis的客户端同时对一个key进行set值得时候由于执行顺序引起的并发问题
缓存读写模式
Cache Aside Pattern(常用)
Cache Aside Pattern
(旁路缓存),是最经典的缓存+数据库读写模式。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存呢?
缓存的值是一个结构:hash、list,更新数据需要遍历
2、懒加载,使用的时候才更新缓存
也可以采用异步的方式填充缓存
Read/Write Through Pattern
应用程序只操作缓存,缓存操作数据库。
Read-Through
(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入缓存。
Write-Through
(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。
该种模式需要提供数据库的handler,开发较为复杂。
Write Behind Caching Pattern(推荐)
应用程序只更新缓存。
缓存通过异步的方式将数据批量或合并后更新到DB中,不能时时同步,甚至会丢数据
缓存架构的设计思路
缓存的整体设计思路包括:
- 多层次
分布式缓存宕机,本地缓存还可以使用
- 数据类型
- 简单数据类型
Value是字符串或整数
Value的值比较大(大于100K)
只进行setter和getter
可采用Memcached
Memcached纯内存缓存,多线程 - 复杂数据类型
Value是hash、set、list、zset
需要存储关系,聚合,计算
可采用Redis
- 简单数据类型
- 要做集群
分布式缓存集群方案(Redis)
哨兵+主从
RedisCluster - 缓存的数据结构设计
- 1、与数据库表一致
数据库表和缓存是一一对应的
缓存的字段会比数据库表少一些
缓存的数据是经常访问的
用户表,商品表 - 2、与数据库表不一致
需要存储关系,聚合,计算等
比如某个用户的帖子、用户的评论。
以用户评论为例,DB结构如下:
如果要取出UID为1000的用户的评论,原始的表的数据结构显然是不行的。
我们应做如下设计:- key:UID+时间戳(精确到天) 评论一般以天为计算单位
- value:Redis的Hash类型。field为 id和content
- expire:设置为一天
- 1、与数据库表一致
案例: 设计拉钩首页缓存职位列表、热门职位
1、静态文件
2、职位列表
数据特点:固定数据,一次性读取
方案:
- 在服务器开启时一次性初始化到服务器本地缓存
- 采用Guava Cache,Guava Cache用于存储频繁使用的少量数据,支持高并发访问
- 也可以使用JDK的CurrentHashMap,需要自行实现
3、热门职位
数据特点:频繁变化,不必时时同步;但一定要有数据,不能为空
方案:
- 数据从服务层读取(dubbo),然后放到本地缓存中(Guava),如果出现超时或读取为空,则返回原
- 来本地缓存的数据。
注意:不同的客户端看到的数据有可能不一样
4、数据回填
从Dubbo中读取数据时,先读取Redis集群的缓存,如果缓存命中则直接返回。如果缓存不命中则返回本地缓存,不能直接读取数据库。用异步的形式从数据库刷入到缓存中。
5、热点策略
对于热点数据我们采用本地缓存策略,而不采用服务熔断策略,因为首页数据可以不准确,但不能不响应。
高并发脏读
先更新数据库,在更新缓存
先删除缓存,在更新数据库
先更新数据库,再删除缓存(推荐)
Redis底层结构和缓存原理
本章学习目标:
- 掌握Redis五种基本数据类型的用法和常见命令的使用
- 了解bitmap、geo、stream的使用
- 理解Redis底层数据结构(Hash、跳跃表、quicklist)
- 了解RedisDB和RedisObject
- 理解LRU算法
- 理解Redis缓存淘汰策略
- 能够较正确的应用Redis缓存淘汰策略
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)
Redis中key的设计
- 用:分割
- 把表名转换为key前缀, 比如: user:
- 第二段放置主键值
- 第三段放置列名
比如:用户表user, 转换为redis的key-value存储
username 的 key: user:9:username
{userid:9,username:zhangf}
email的key user:9:email
表示明确:看key知道意思
不易被覆盖
数据类型及命令手册
参考:https://blog.csdn.net/qq_36581961/article/details/113248387
Redis客户端访问
Java程序访问Redis
采用jedis API进行访问即可
关闭RedisServer端的防火墙
systemctl stop firewalld(默认)
systemctl disable firewalld.service(设置开启不启动)
新建maven项目后导入Jedis包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
测试类
@Test
public void testConn(){
//与Redis建立连接 IP+port
Jedis redis = new Jedis("192.168.127.128", 6379);
//在Redis中写字符串 key value
redis.set("jedis:name:1","jd-zhangfei");
//获得Redis中字符串的值
System.out.println(redis.get("jedis:name:1"));
//在Redis中写list
redis.lpush("jedis:list:1","1","2","3","4","5");
//获得list的长度
System.out.println(redis.llen("jedis:list:1"));
}
Spring访问Redis
新建项目,添加依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
</dependencies>
添加Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
<!-- 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>
添加properties文件
redis.pool.maxActive=100
redis.pool.maxIdle=50
redis.pool.maxWait=1000
redis.pool.testOnBorrow=true
redis.timeout=50000
redis.server=192.168.72.128
redis.port=6379
编写测试用例
import org.junit.Test;
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.AbstractJUnit4SpringContextTests;
import java.io.Serializable;
@ContextConfiguration({ "classpath:redis.xml" })
public class RedisTest extends AbstractJUnit4SpringContextTests {
@Autowired
private RedisTemplate<Serializable, Serializable> rt;
@Test
public void testConn() {
rt.opsForValue().set("name","zhangfei");
System.out.println(rt.opsForValue().get("name"));
}
}
SpringBoot访问Redis
新建项目,导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
添加配置文件application.yml
spring:
redis:
host: 192.168.72.128
port: 6379
jedis:
pool:
min-idle: 0
max-idle: 8
max-active: 80
max-wait: 30000
timeout: 3000
添加配置类RedisConfig
package com.lagou.sbr.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;
@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.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
** 添加RedisController**
package com.lagou.sbr.controller;
import org.springframework.beans.factory.annotation.Autowired;
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;
@RestController
@RequestMapping(value = "/redis")
public class RedisController {
@Autowired
RedisTemplate redisTemplate;
@GetMapping("/put")
public String put(@RequestParam(required = true) String key,
@RequestParam(required = true) String value) {
//设置过期时间为20秒
redisTemplate.opsForValue().set(key,value,20, TimeUnit.SECONDS);
return "Success";
}
@GetMapping("/get")
public String get(@RequestParam(required = true) String key){
return (String) redisTemplate.opsForValue().get(key);
}
}
修改Application并运行
package com.lagou.sbr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SpringbootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRedisApplication.class, args);
}
}
底层数据结构
参考(未更新完整):https://blog.csdn.net/qq_36581961/category_10691600.html
缓存过期和淘汰策略
Redis性能高:
官方数据
读:110000次/s
写:81000次/s
长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满
内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降
maxmemory
不设置的场景
Redis的key是固定的,不会增加
Redis作为DB使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展
缓存淘汰策略:禁止驱逐 (默认)
设置的场景
Redis是作为缓存使用,不断增加Key
maxmemory : 默认为0 不限制
问题:达到物理内存后性能急剧下架,甚至崩溃;内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降
设置多少?与业务有关
1个Redis实例,保证系统运行 1 G ,剩下的就都可以设置Redis;物理内存的3/4
slaver : 留出一定的内存
在redis.conf中
maxmemory 1024mb
命令: 获得maxmemory数
CONFIG GET maxmemory
设置maxmemory后,当趋近maxmemory时,通过缓存淘汰策略,从内存中删除对象
不设置maxmemory 无最大内存限制 maxmemory-policy noeviction (禁止驱逐) 不淘汰
设置maxmemory maxmemory-policy 要配置
expire数据结构
在Redis中可以使用expire命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除
expire的使用
expire命令的使用方法如下:expire key ttl(单位秒)
127.0.0.1:6379> expire name 2 #2秒失效
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name zhangfei
OK
127.0.0.1:6379> ttl name #永久有效
(integer) -1
127.0.0.1:6379> expire name 30 #30秒失效
(integer) 1
127.0.0.1:6379> ttl name #还有24秒失效
(integer) 24
127.0.0.1:6379> ttl name #失效
(integer) -2
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 这个字典表中。
删除策略
Redis的数据删除有定时删除
、惰性删除
和主动删除
三种方式。Redis目前采用惰性删除
+主动删除
的方式。
定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
创建一个定时器,当key设置有过期时间 且过期时间到达 由定时器任务立即对键的删除
- 优点:节约内存 到时删除 快速释放不必要的内存
- 缺点:CPU压力很大 无论CPU此时的负载多高 均占用CPU 会影响redis服务器相应时间和指令吞吐量
总结:以时间换空间
惰性删除
数据到达过期时间 不做处理 等下次访问该数据时
-
如果过期 删除
-
如果未过期 返回数据
-
优点:节约CPU性能 发现必须删除才删除
-
缺点:内存压力大 出现长期占用内存数据
总结:用存储空间换处理器性能 以空间换时间
调用expireIfNeeded函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删除它。
int expireIfNeeded(redisDb *db, robj *key) {
//获取主键的失效时间 get当前时间-创建时间>ttl
long long when = getExpire(db,key);
//假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
if (when < 0) return 0;
//假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
if (server.loading) return 0;
//如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
//还未失效就直接返回0
if (mstime() <= when) return 0;
//如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失
//效的信息进行广播,最后将该主键从数据库中删除
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);
}
定期删除(主动删除)
周期性轮询redis库中的时效性数据 采用随机抽取的策略 利用过期数据占比的方式控制删除频度
- 特点:CPU性能占用设置有峰值 检测频度可以自定义设置
- 特点:内存占用率不是很大 ,长期占用内存的冷数据会被持续清理
逐出算法
数据逐出的相关配置
- 最大可使用配置
# 占用物理内存的比例 默认值为0 表示不限制 生产环境按照需求设定 通常设置在50%以上
maxmemory
- 每次选取待删除的个数
# 选取数据时并不会全库扫描 导致严重的性能消耗 降低读写性能 因此采用随机获取数据的方式作为待检测删除数据
maxmemory-samples
- 删除策略
# 达到最大内存之后 对被挑出来的数据进行删除的策略
maxmemory-policy
通信协议及事件处理机制
本章学习目标:
- 知道Redis的请求响应模式
- 理解请求数据格式(RESP)
- 描述命令处理流程
- 知道Redis的响应格式
- 掌握4种IO多路复用模式(epoll)
- 理解aeEventLoop
通信协议
Redis是单进程单线程的。应用系统和Redis通过Redis协议(RESP)进行交互。
请求响应模式
Redis协议位于TCP层之上,即客户端和Redis实例保持双工的连接。
串行的请求响应模式(ping-pong)
串行化是最简单模式,客户端与服务器端建立长连接
连接通过心跳机制检测(ping-pong)
ack应答
客户端发送请求,服务端响应,客户端收到响应后,再发起第二个请求,服务器端再响应。
telnet和redis-cli 发出的命令 都属于该种模式
特点:
- 有问有答
- 耗时在网络传输命令
- 性能较低
双工的请求响应模式(pipeline)
批量请求,批量响应。请求响应交叉进行,不会混淆(TCP双工)
- pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回。
- 通过pipeline,一次pipeline(n条命令)=一次网络时间 + n次命令时间
原子化的批量请求响应模式(事务)
Redis可以利用事务机制批量执行命令。后面会详细讲解。
发布订阅模式(pub/sub)
发布订阅模式是:一个客户端触发,多个客户端被动接收,通过服务器中转。后面会详细讲解。
脚本化的批量执行(lua)
客户端向服务器端提交一个lua脚本,服务器端执行该脚本。后面会详细讲解。
请求数据格式
Redis客户端与服务器交互采用序列化协议
(RESP)。请求以字符串数组的形式来表示要执行命令的参数。Redis使用命令特有(command-specific)数据类型作为回复。
Redis通信协议的主要特点有:
- 客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
- 客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。
在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。简单,高效,易读。
内联格式
可以使用telnet给Redis发送命令,首字符为Redis命令名的字符,格式为 str1 str2 str3…
[root@localhost bin]# 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
规范格式(redis-cli)
1、间隔符号,在Linux下是\r\n,在Windows下是\n
2、简单字符串 Simple Strings, 以 "+“加号 开头
3、错误 Errors, 以”-"减号 开头
4、整数型 Integer, 以 “:” 冒号开头
5、大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制512M
6、数组类型 Arrays,以 "*"星号开头
用SET命令来举例说明RESP协议的格式。
redis> SET mykey Hello
"OK"
实际发送的请求数据:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
*3
$3
SET
$5
mykey
$5
Hello
实际收到的响应数据:
+OK\r\n
命令处理流程
整个流程包括:服务器启动监听、接收命令请求并解析、执行命令请求、返回命令回复等。
Server启动时监听socket
- 启动调用 initServer方法
- 创建eventLoop(事件机制)
- 注册时间事件处理器
- 注册文件事件(socket)处理器
- 监听 socket 建立连接
建立Client
- redis-cli建立socket
- redis-server为每个连接(socket)创建一个 Client 对象
- 创建文件事件监听socket
- 指定事件处理函数
读取socket数据到输入缓冲区
从client中读取客户端的查询缓冲区内容。
解析获取命令
- 将输入缓冲区中的数据解析成对应的命令
- 判断是单条命令还是多条命令并调用相应的解析器解析
执行命令
解析成功后调用processCommand 方法执行命令,如下图:
大致分三个部分:
- 调用 lookupCommand 方法获得对应的 redisCommand
- 检测当前 Redis 是否可以执行该命令
- 调用 call 方法真正执行命令
协议响应格式
- 状态回复
对于状态,回复的第一个字节是“+”OK
- 错误回复
对于错误,回复的第一个字节是“ - ”1. -ERR unknown command 'foobar' 2. -WRONGTYPE Operation against a key holding the wrong kind of value
- 整数回复
对于整数,回复的第一个字节是“:”":6"
- 批量回复
对于批量字符串,回复的第一个字节是“$”"$6 foobar"
- 多条批量回复
对于多条批量回复(数组),回复的第一个字节是“*”"*3"
协议解析及处理
包括协议解析
、调用命令
、返回结果
。
协议解析
用户在Redis客户端键入命令后,Redis-cli会把命令转化为RESP协议格式,然后发送给服务器。服务器再对协议进行解析,分为三个步骤
- 解析命令请求参数数量
命令请求参数数量的协议格式为"*N\r\n" ,其中N就是数量,比如127.0.0.1:6379> set name:1 zhaoyun
我们打开aof文件可以看到协议内容
首字符必须是“*”,使用"\r"定位到行尾,之间的数就是参数数量了。*3(/r/n) $3(/r/n) set(/r/n) $7(/r/n) name:10(/r/n) $7(/r/n) zhaoyun(/r/n)
- 循环解析请求参数
首字符必须是" " , 使 用 " / r " 定 位 到 行 尾 , 之 间 的 数 是 参 数 的 长 度 , 从 / n 后 到 下 一 个 " ",使用"/r"定位到行尾,之间的数是参数的长度,从/n后到下一个" ",使用"/r"定位到行尾,之间的数是参数的长度,从/n后到下一个"“之间就是参数的值了,循环解析直到没有”$"。
协议执行
协议的执行包括命令的调用和返回结果,判断参数个数和取出的参数是否一致,RedisServer解析完命令后,会调用函数processCommand处理该命令请求
- quit校验,如果是“quit”命令,直接返回并关闭客户端
- 命令语法校验,执行lookupCommand,查找命令(set),如果不存在则返回:“unknowncommand”错误。
- 参数数目校验,参数数目和解析出来的参数个数要匹配,如果不匹配则返回:“wrong number of、arguments”错误。
- 此外还有权限校验,最大内存校验,集群校验,持久化校验等等。
校验成功后,会调用call函数执行命令,并记录命令执行时间和调用次数
如果执行命令时间过长还要记录慢查询日志
执行命令后返回结果的类型不同则协议格式也不同,分为5类:状态回复、错误回复、整数回复、批量回复、多条批量回复。