API网关如何实现对服务下线实时感知

上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka 有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。 一、前言在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且由于自动伸缩、故障和升级,服务实例会经常动态改变。因此,客户端代码需要使用更加复杂的服务发现机制。 目前服务发现主要有两种模式:客户端发现和服务端发现。 服务端发现:客户端通过负载均衡器向服务注册中心发起请求,负载均衡器查询服务注册中心,将每个请求路由到可用的服务实例上。客户端发现:客户端负责决定可用服务实例的网络地址,并且在集群中对请求负载均衡, 客户端访问服务登记表,也就是一个可用服务的数据库,然后客户端使用一种负载均衡算法选择一个可用的服务实例然后发起请求。客户端发现相对于服务端发现最大的区别是:客户端知道(缓存)可用服务注册表信息。如果Client端缓存没能从服务端及时更新的话,可能出现Client 与 服务端缓存数据不一致的情况。 二、网关与Eureka结合使用Netflix OSS 提供了一个客户端服务发现的好例子。Eureka Server 为注册中心,Zuul 相对于Eureka Server来说是Eureka Client,Zuul 会把 Eureka Server 端服务列表缓存到本地,并以定时任务的形式更新服务列表,同时zuul通过本地列表发现其它服务,使用Ribbon实现客户端负载均衡。 正常情况下,调用方对网关发起请求即刻能得到响应。但是当对生产者做缩容、下线、升级的情况下,由于Eureka这种多级缓存的设计结构和定时更新的机制,LoadBalance 端的服务列表B存在更新不及时的情况(由上篇文章《Eureka 缓存机制》可知,服务消费者最长感知时间将无限趋近240s),如果这时消费者对网关发起请求,LoadBalance 会对一个已经不存在的服务发起请求,请求是会超时的。 三、解决方案3.1 实现思路生产者下线后,最先得到感知的是 Eureka Server 中的 readWriteCacheMap,最后得到感知的是网关核心中的 LoadBalance。但是 loadBalance 对生产者的发现是在 loadBalance 本地维护的列表中。 所以要想达到网关对生产者下线的实时感知,可以这样做:首先生产者或者部署平台主动通知 Eureka Server, 然后跳过 Eureka 多级缓存之间的更新时间,直接通知 Zuul 中的 Eureka Client,最后将 Eureka Client 中的服务列表更新到 Ribbon 中。 但是如果下线通知的逻辑代码放在生产者中,会造成代码污染、语言差异等问题。 借用一句名言: “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决” Gateway-SynchSpeed 相当于一个代理服务,它对外提供REST API来负责响应调用方的下线请求,同时会将生产者的状态同步到 Eureka Server 和 网关核心,起着 状态同步 和 软事物 的作用。 ...

June 10, 2019 · 2 min · jiezi

Ubuntu1604下安装和配置Redis

声明:文章内容转载至【Ubuntu16.04下安装和配置Redis】 一、前提条件需要连接互联网,然后执行sudo apt-get update更新软件包 二、执行安装命令sudo apt-get install redis-server 执行后如下图所示,我们输入y 确认安装并使用空间 接下来会执行完成,我们可以看到包括redis的版本信息等,执行service redis status 可以查看redis服务的状态为running,说明安装完成系统自动启动了服务 三、配置redis服务3.1:开启远程连接找到/et/redis/redis.conf文件修改如下 ,注释掉 127.0.0.1 #bind 127.0.0.1,如果不需要远程连接redis则不需要这个操作 3.2:设置密码找到/et/redis/redis.conf文件修改如下 ,添加 requirepass kingredis(密码设置为kingredis) 最后重启redis服务 service redis restart 四、测试redis服务4.1:测试密码设置成功执行redis-cli命令打开redis客户端 set操作的时候要求我输入密码,说明密码设置成功,执行auth 密码验证密码后,可以执行set操作 4.2:测试远程登录在本地打开一个客户端 ,cd到redis安装的目录,主要是要有redis-cli.exe的目录输入 redis-cli -h redis服务器IP -p redis服务端口号(默认6379) 如上图所示,访问远程redis服务成功,操作redis成功,说明我们远程的redis安装切配置安全密码成功了 【注意】如果是阿里云服务器,切记要在安全组、安全策略里面加入服务的端口号,允许所有地址访问,如下图,才可以,即在阿里云服务器的所有服务都需要把端口映射出来才可以,如果没有做这一步,上面的redis-cli命令就会处于一直等待的状态

June 6, 2019 · 1 min · jiezi

面试官你是如何使用JDK来实现自己的缓存支持高并发

我自己总结的Java学习的系统知识点以及面试问题,已经开源,目前已经 41k+ Star。会一直完善下去,欢迎建议和指导,同时也欢迎Star: https://github.com/Snailclimb...本文转载自:https://dwz.cn/HGarfiB9需求分析项目中经常会遇到这种场景:一份数据需要在多处共享,有些数据还有时效性,过期自动失效。比如手机验证码,发送之后需要缓存起来,然后处于安全性考虑,一般还要设置有效期,到期自动失效。我们怎么实现这样的功能呢? 解决方案使用现有的缓存技术框架,比如redis,ehcache。优点:成熟,稳定,功能强大;缺点,项目需要引入对应的框架,不够轻量。如果不考虑分布式,只是在单线程或者多线程间作数据缓存,其实完全可以自己手写一个缓存工具。下面就来简单实现一个这样的工具。先上代码: import java.util.HashMap;import java.util.Map;import java.util.concurrent.*;/** * @Author: lixk * @Date: 2018/5/9 15:03 * @Description: 简单的内存缓存工具类 */public class Cache { //键值对集合 private final static Map<String, Entity> map = new HashMap<>(); //定时器线程池,用于清除过期缓存 private final static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); /** * 添加缓存 * * @param key 键 * @param data 值 */ public synchronized static void put(String key, Object data) { Cache.put(key, data, 0); } /** * 添加缓存 * * @param key 键 * @param data 值 * @param expire 过期时间,单位:毫秒, 0表示无限长 */ public synchronized static void put(String key, Object data, long expire) { //清除原键值对 Cache.remove(key); //设置过期时间 if (expire > 0) { Future future = executor.schedule(new Runnable() { @Override public void run() { //过期后清除该键值对 synchronized (Cache.class) { map.remove(key); } } }, expire, TimeUnit.MILLISECONDS); map.put(key, new Entity(data, future)); } else { //不设置过期时间 map.put(key, new Entity(data, null)); } } /** * 读取缓存 * * @param key 键 * @return */ public synchronized static Object get(String key) { Entity entity = map.get(key); return entity == null ? null : entity.getValue(); } /** * 读取缓存 * * @param key 键 * * @param clazz 值类型 * @return */ public synchronized static <T> T get(String key, Class<T> clazz) { return clazz.cast(Cache.get(key)); } /** * 清除缓存 * * @param key * @return */ public synchronized static Object remove(String key) { //清除原缓存数据 Entity entity = map.remove(key); if (entity == null) return null; //清除原键值对定时器 Future future = entity.getFuture(); if (future != null) future.cancel(true); return entity.getValue(); } /** * 查询当前缓存的键值对数量 * * @return */ public synchronized static int size() { return map.size(); } /** * 缓存实体类 */ private static class Entity { //键值对的value private Object value; //定时器Future private Future future; public Entity(Object value, Future future) { this.value = value; this.future = future; } /** * 获取值 * * @return */ public Object getValue() { return value; } /** * 获取Future对象 * * @return */ public Future getFuture() { return future; } }}本工具类主要采用 HashMap+定时器线程池 实现,map 用于存储键值对数据,map 的value是 Cache 的内部类对象 Entity,Entity 包含 value 和该键值对的生命周期定时器 Future。Cache 类对外只提供了 put(key, value), put(key, value, expire), get(key), get(key, class), remove(key), size()几个同步方法。 ...

June 5, 2019 · 3 min · jiezi

浏览器缓存那些事

浏览器读取资源的流程浏览器在加载资源时,根据请求头的expires和cache-control判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过last-modified或者etag验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源如果前面两者都没有命中,直接从服务器加载资源强制缓存(不发送请求)如何设置 通常我们会同时设置expires和cache-control两种,保证无论在http1还是1.1的情况下都有效 expires过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求cache-control http1.1新标准,包括这些属性: (1)max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;(2)s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;(3)public:指示响应可被任何缓存区缓存;(4)private:只能针对个人用户,而不能被代理服务器缓存;(5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。这个很容易让人产生误解,使人误以为是响应不被缓存。实际上Cache-Control:no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。(6)no-store:禁止一切缓存(这个才是响应不被缓存的意思)。缓存的两种表现形式 memory cache来自于内存的数据,会随着进程的结束而清除,读取速度相对快(0ms)一般存放脚本,图片,字体等文件 disk cache来自于硬盘的数据,不会随着进程的结束而清除,读取速度慢于memory cache(2-10ms 硬盘读写的IO操作)一般存放css文件 根据经验情况来看:浏览器的实际处理逻辑是这样的 首次加载资源 -> 200 -> 关闭标签页再次进入 -> 200 from disk cache -> 刷新 -> 200 from memory cache(不过好像css都是from disk cache, base64都是from memory cache) 协商缓存(发送请求)客户端向服务端发送请求时候(没有命中强制缓存),服务端会检查是否有对应的标识,没有则返回200并生成一个新的标识带到header,下次在请求的时候服务端检查到对应的这个标识并做相应的校验,通过则返回304,读取缓存。 Last-modify / If-modify-since浏览器首次请求资源的时候,服务器会返回一个last-Modify到header中. Last-Modify 含义是最后的修改时间。 当浏览器再次请求的时候,request header会带上 if-Modify-Since,该值为之前返回的 Last-Modify。服务器收到if-Modify-Since后,根据资源的最后修改时间(last-Modify)和该值(if-Modify-Since)进行比较,如果相等的话,则命中缓存,返回304,否则, 则会给出200响应,并且更新Last-Modify为新的值 Etag / If-none-match(http1.1规范) ETag的原理和上面的last-modified是类似的。ETag对当前请求的资源做一个唯一的标识。该标识可以是一个字符串,文件的size,hash等。只要能够合理标识资源的唯一性并能验证是否修改过就可以了。ETag在服务器响应请求的时候,返回当前资源的唯一标识(它是由服务器生成的)。但是只要资源有变化,ETag会重新生成的。浏览器再下一次加载的时候会向服务器发送请求,会将上一次返回的ETag值放到request header 里的 if-None-Match里面去,服务器端只要比较客户端传来的if-None-Match值是否和自己服务器上的ETag是否一致,如果一致说明资源未修改过,因此返回304,如果不一致,说明修改过,因此返回200。并且把新的Etag赋值给if-None-Match来更新该值。协商缓存两种方式对比在精度上,ETag要优先于 last-modified。last-modified这种方式精度差在哪里: a. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了 b. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒); 在性能上,Etag要逊于Last-Modified,Last-Modified需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。在优先级上,服务器校验优先考虑Etag。

May 31, 2019 · 1 min · jiezi

从实现角度看redis-lazy-free的使用和注意事项

众所周知,redis对外提供的服务是由单线程支撑,通过事件(event)驱动各种内部逻辑,比如网络IO、命令处理、过期key处理、超时等逻辑。在执行耗时命令(如范围扫描类的keys, 超大hash下的hgetall等)、瞬时大量key过期/驱逐等情况下,会造成redis的QPS下降,阻塞其他请求。近期就遇到过大容量并且大量key的场景,由于各种原因引发的redis内存耗尽,导致有6位数的key几乎同时被驱逐,短期内redis hang住的情况 耗时命令是客户端行为,服务端不可控,优化余地有限,作者antirez在4.0这个大版本中增加了针对大量key过期/驱逐的lazy free功能,服务端的事情还是可控的,甚至提供了异步删除的命令unlink(前因后果和作者的思路变迁,见作者博客:Lazy Redis is better Redis - <antirez>) lazy free的功能在使用中有几个注意事项(以下为个人观点,有误的地方请评论区交流): lazy free不是在遇到快OOM的时候直接执行命令,放后台释放内存,而是也需要block一段时间去获得足够的内存来执行命令lazy free不适合kv的平均大小太小或太大的场景,大小均衡的场景下性价比比较高(当然,可以根据业务场景调整源码里的宏,重新编译一个版本)redis短期内其实是可以略微超出一点内存上限的,因为前一条命令没检测到内存超标(其实快超了)的情况下,是可以写入一个很大的kv的,当后续命令进来之后会发现内存不够了,交给后续命令执行释放内存操作如果业务能预估到可能会有集中的大量key过期,那么最好ttl上加个随机数,匀开来,避免集中expire造成的blocking,这点不管开不开lazy free都一样具体分析请见下文 参数redis 4.0新加了4个参数,用来控制这种lazy free的行为 lazyfree-lazy-eviction:是否异步驱逐key,当内存达到上限,分配失败后lazyfree-lazy-expire:是否异步进行key过期事件的处理lazyfree-lazy-server-del:del命令是否异步执行删除操作,类似unlinkreplica-lazy-flush:replica client做全同步的时候,是否异步flush本地db以上参数默认都是no,按需开启,下面以lazyfree-lazy-eviction为例,看看redis怎么处理lazy free逻辑,其他参数的逻辑类似 源码分析命令处理逻辑int processCommand(client *c)是redis处理命令的主方法,在真正执行命令前,会有各种检查,包括对OOM情况下的处理 int processCommand(client *c) { // ... if (server.maxmemory && !server.lua_timedout) { // 设置了maxmemory时,如果有必要,尝试释放内存(evict) int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR; // ... // 如果释放内存失败,并且当前将要执行的命令不允许OOM(一般是写入类命令) if (out_of_memory && (c->cmd->flags & CMD_DENYOOM || (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) { flagTransaction(c); // 向客户端返回OOM addReply(c, shared.oomerr); return C_OK; } } // ... /* Exec the command */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && c->cmd->proc != discardCommand && c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) { queueMultiCommand(c); addReply(c,shared.queued); } else { call(c,CMD_CALL_FULL); c->woff = server.master_repl_offset; if (listLength(server.ready_keys)) handleClientsBlockedOnKeys(); } return C_OK;内存释放(淘汰)逻辑内存的释放主要在freeMemoryIfNeededAndSafe()内进行,如果释放不成功,会返回C_ERR。freeMemoryIfNeededAndSafe()包装了底下的实现函数freeMemoryIfNeeded() ...

May 18, 2019 · 2 min · jiezi

Nginx-内容缓存及常见参数配置

原文链接:何晓东 博客 使用场景:项目的页面需要加载很多数据,也不是经常变化的,不涉及个性化定制,为每次请求去动态生成数据,性能比不上根据请求路由和参数缓存一下结果,使用 Nginx 缓存将大幅度提升请求速度。基础只需要配置 proxy_cache_path 和 proxy_cache 就可以开启内容缓存,前者用来设置缓存的路径和配置,后者用来启用缓存。 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; } }}对应参数说明:1.用于缓存的本地磁盘目录是 /path/to/cache/ 2.levels 在 /path/to/cache/ 设置了一个两级层次结构的目录。将大量的文件放置在单个目录中会导致文件访问缓慢,所以针对大多数部署,我们推荐使用两级目录层次结构。如果 levels 参数没有配置,则 NGINX 会将所有的文件放到同一个目录中。 3.keys_zone 设置一个共享内存区,该内存区用于存储缓存键和元数据,有些类似计时器的用途。将键的拷贝放入内存可以使 NGINX 在不检索磁盘的情况下快速决定一个请求是 HIT 还是 MISS,这样大大提高了检索速度。一个 1MB 的内存空间可以存储大约 8000 个 key,那么上面配置的 10MB 内存空间可以存储差不多 80000 个key。 4.max_size 设置了缓存的上限(在上面的例子中是 10G)。这是一个可选项;如果不指定具体值,那就是允许缓存不断增长,占用所有可用的磁盘空间。当缓存达到这个上线,处理器便调用 cache manager 来移除最近最少被使用的文件,这样把缓存的空间降低至这个限制之下。 5.inactive 指定了项目在不被访问的情况下能够在内存中保持的时间。在上面的例子中,如果一个文件在 60 分钟之内没有被请求,则缓存管理将会自动将其在内存中删除,不管该文件是否过期。该参数默认值为 10 分钟(10m)。注意,非活动内容有别于过期内容。NGINX 不会自动删除由缓存控制头部指定的过期内容(本例中Cache-Control:max-age=120)。过期内容只有在 inactive 指定时间内没有被访问的情况下才会被删除。如果过期内容被访问了,那么 NGINX 就会将其从原服务器上刷新,并更新对应的 inactive 计时器。 ...

May 14, 2019 · 2 min · jiezi

分布式数据缓存中的一致性哈希算法

一致性哈希算法在分布式缓存领域的 MemCached,负载均衡领域的 Nginx 以及各类 RPC 框架中都有广泛的应用,它主要是为了解决传统哈希函数添加哈希表槽位数后要将关键字重新映射的问题。 本文会介绍一致性哈希算法的原理及其实现,并给出其不同哈希函数实现的性能数据对比,探讨Redis 集群的数据分片实现等,文末会给出实现的具体 github 地址。 Memcached 与客户端分布式缓存Memcached 是一个高性能的分布式缓存系统,然而服务端没有分布式功能,各个服务器不会相互通信。它的分布式实现依赖于客户端的程序库,这也是 Memcached 的一大特点。比如第三方的 spymemcached 客户端就基于一致性哈希算法实现了其分布式缓存的功能。 其具体步骤如下: 向 Memcached 添加数据,首先客户端的算法根据 key 值计算出该 key 对应的服务器。服务器选定后,保存缓存数据。获取数据时,对于相同的 key ,客户端的算法可以定位到相同的服务器,从而获取数据。在这个过程中,客户端的算法首先要保证缓存的数据尽量均匀地分布在各个服务器上,其次是当个别服务器下线或者上线时,会出现数据迁移,应该尽量减少需要迁移的数据量。 客户端算法是客户端分布式缓存性能优劣的关键。 普通的哈希表算法一般都是计算出哈希值后,通过取余操作将 key 值映射到不同的服务器上,但是当服务器数量发生变化时,取余操作的除数发生变化,所有 key 所映射的服务器几乎都会改变,这对分布式缓存系统来说是不可以接收的。 一致性哈希算法能尽可能减少了服务器数量变化所导致的缓存迁移。 哈希算法首先,一致性哈希算法依赖于普通的哈希算法。大多数同学对哈希算法的理解可能都停留在 JDK 的 hashCode 函数上。其实哈希算法有很多种实现,它们在不同方面都各有优劣,针对不同的场景可以使用不同的哈希算法实现。 下面,我们会介绍一下几款比较常见的哈希算法,并且了解一下它们在分布均匀程度,哈希碰撞概率和性能等方面的优劣。 MD5 算法:全称为 Message-Digest Algorithm 5,用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一,主流编程语言普遍已有 MD5 实现。MD5 的作用是把大容量信息压缩成一种保密的格式(就是把一个任意长度的字节串变换成定长的16进制数字串)。常见的文件完整性校验就是使用 MD5。 CRC 算法:全称为 CyclicRedundancyCheck,中文名称为循环冗余校验。它是一类重要的,编码和解码方法简单,检错和纠错能力强的哈希算法,在通信领域广泛地用于实现差错控制。 MurmurHash 算法:高运算性能,低碰撞率,由 Austin Appleby 创建于 2008 年,现已应用到 Hadoop、libstdc++、nginx、libmemcached 等开源系统。Java 界中 Redis,Memcached,Cassandra,HBase,Lucene和Guava 都在使用它。 ...

May 13, 2019 · 3 min · jiezi

系统的讲解-PHP-缓存技术

概述缓存已经成了项目中是必不可少的一部分,它是提高性能最好的方式,例如减少网络I/O、减少磁盘I/O 等,使项目加载速度变的更快。 缓存可以是CPU缓存、内存缓存、硬盘缓存,不同的缓存查询速度也不一样(CPU缓存 > 内存缓存 > 硬盘缓存)。 接下来,给大家逐一进行介绍。 浏览器缓存浏览器将请求过的页面存储在客户端缓存中,当访问者再次访问这个页面时,浏览器就可以直接从客户端缓存中读取数据,减少了对服务器的访问,加快了网页的加载速度。 强缓存用户发送的请求,直接从客户端缓存中获取,不请求服务器。 根据 Expires 和 Cache-Control 判断是否命中强缓存。 代码如下: header('Expires: '. gmdate('D, d M Y H:i:s', time() + 3600). ' GMT');header("Cache-Control: max-age=3600"); //有效期3600秒Cache-Control 还可以设置以下参数: public:可以被所有的用户缓存(终端用户的浏览器/CDN服务器)private:只能被终端用户的浏览器缓存no-cache:不使用本地缓存no-store:禁止缓存数据协商缓存用户发送的请求,发送给服务器,由服务器判定是否使用客户端缓存。 代码如下: $last_modify = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);if (time() - $last_modify < 3600) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', $last_modify).' GMT'); header('HTTP/1.1 304'); //Not Modified exit;}header('Last-Modified: '. gmdate('D, d M Y H:i:s').' GMT');用户操作行为对缓存的影响操作行为ExpiresLast-Modified地址栏回车有效有效页面跳转有效有效新开窗口有效有效前进/后退有效有效F5刷新无效有效Ctrl+F5刷新无效无效文件缓存数据文件缓存将更新频率低,读取频率高的数据,缓存成文件。 比如,项目中多个地方用到城市数据做三级联动,我们就可以将城市数据缓存成一个文件(city_data.json),JS 可以直接读取这个文件,无需请求后端服务器。 全站静态化CMS(内容管理系统),也许大家都比较熟悉,比如早期的 DEDE、PHPCMS,后台都可以设置静态化HTML,用户在访问网站的时候读取的都是静态HTML,不用请求后端的数据库,也不用Ajax请求数据接口,加快了网站的加载速度。 ...

May 10, 2019 · 2 min · jiezi

Laravel-的缓存源码解析

Last-Modified: 2019年5月10日14:17:34 前言Laravel 支持多种缓存系统, 并提供了统一的api接口. (Laravel 5.5)默认支持的存储驱动包括如下: file (默认使用)apcarray (数组, 测试用)database (关系型数据库)memcachedredis默认的缓存配置文件在 config/cache.php 参考链接: https://learnku.com/docs/lara...https://www.jianshu.com/p/46a...使用直接使用Laravel为我们提供的Facade use Illuminate\Support\Facades\Cache;$cache = Cache::get('key');支持的大部分方法: Cache::put('key', 'value', $minutes);Cache::add('key', 'value', $minutes);Cache::forever('key', 'value');Cache::remember('key', $minutes, function(){ return 'value' });Cache::rememberForever('key', function(){ return 'value' });Cache::forget('key');Cache::has('key');Cache::get('key');Cache::get('key', 'default');Cache::get('key', function(){ return 'default'; });Cache::tags('my-tag')->put('key','value', $minutes);Cache::tags('my-tag')->has('key');Cache::tags('my-tag')->get('key');Cache::tags('my-tag')->forget('key');Cache::tags('my-tag')->flush();Cache::increment('key');Cache::increment('key', $amount);Cache::decrement('key');Cache::decrement('key', $amount);Cache::tags('group')->put('key', $value);Cache::tags('group')->get('key');Cache::tags('group')->flush();其他使用方法请参照官方翻译(中文)文档: https://learnku.com/docs/lara... 源码Laravel 中常用 Cache Facade 来操作缓存, 对应的实际类是 Illuminate\Cache\CacheManager 缓存管理类(工厂). Cache::xxx()我们通过 CacheManager 类获取持有不同存储驱动的 Illuminate\Cache\Repository 类 CacheManager::store($name = null)Repository 仓库类代理了实现存储驱动接口 Illuminate\Contracts\Cache\Store 的类实例. Cache Facade首先从 Cache Facade 开始分析, 先看一下其源码: ...

May 10, 2019 · 4 min · jiezi

service-worker的基本知识

前言: 看到一篇讲解service worker的文章, 基础讲的还不错, 所以转了以后作为自己的参考 Service Worker是什么service worker 是独立于当前页面的一段运行在浏览器后台进程里的脚本。它的特性将包括推送消息,背景后台同步, geofencing(地理围栏定位),拦截和处理网络请求。 这个 API 会让人兴奋的原因是,它可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。 在 service worker 之前,另一个叫做 APP Cache 的 api 也可以提供离线体验。APP Cache 的的主要问题是坑比较多,而且其被设计为只适合于单页 web 应用程序,对于传统的多页网站则不适合。service worker 的设计规避了这些痛点。 关于 service worker 的一些注意点: service worker 是一个JavaScript worker ,所以它不能直接访问 DOM 。但 service worker 可以通过postMessage 接口与跟其相关的页面进行通信,发送消息,从而让这些页面在有需要的时候去操纵 DOM 。Service worker 是一个可编程的网络代理,允许你去控制如何处理页面的网络请求, 可以处理fetch请求。Service worker 在不使用时将被终止,并会在需要的时候重新启动,因此你不能把onfetch 和onmessage事件来作为全局依赖处理程序。如果你需要持久话一些信息并在重新启动Service worker后使用他,可以使用 IndexedDBAPI ,service worker 支持。Service Worker 的缓存机制是依赖 Cache API 实现的 Service worker 广泛使用了 promise。Service worker依赖 HTML5 fetch APIService Workers 要求必须在 HTTPS 下才能运行Service Worker生命周期 ...

May 8, 2019 · 4 min · jiezi

程序员笔记详解Eureka-缓存机制

引言Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下线后状态没有及时更新,服务消费者调用到已下线的服务导致请求失败。本文基于Spring Cloud Eureka 1.4.4.RELEASE,在默认region和zone的前提下,介绍Eureka的缓存机制。 一、AP特性从CAP理论看,Eureka是一个AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C),只保证最终一致性,因此在架构中设计了较多缓存。 二、服务状态Eureka服务状态enum类:com.netflix.appinfo.InstanceInfo.InstanceStatus 状态说明状态说明UP在线OUT_OF_SERVICE失效DOWN下线UNKNOWN未知STARTING正在启动三、Eureka Server在Eureka高可用架构中,Eureka Server也可以作为Client向其他server注册,多节点相互注册组成Eureka集群,集群间相互视为peer。Eureka Client向Server注册、续约、更新状态时,接受节点更新自己的服务注册信息后,逐个同步至其他peer节点。 【注意】如果server-A向server-B节点单向注册,则server-A视server-B为peer节点,server-A接受的数据会同步给server-B,但server-B接受的数据不会同步给server-A。 3.1 缓存机制Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而UI则从registry更新服务注册信息。 三级缓存 缓存类型说明registryConcurrentHashMap实时更新,类AbstractInstanceRegistry成员变量,UI端请求的是这里的服务注册信息readWriteCacheMapGuava Cache/LoadingCache实时更新,类ResponseCacheImpl成员变量,缓存时间180秒readOnlyCacheMapConcurrentHashMap周期更新,类ResponseCacheImpl成员变量,默认每30s从readWriteCacheMap更新,Eureka client默认从这里更新服务注册信息,可配置直接从readWriteCacheMap更新缓存相关配置 配置默认说明eureka.server.useReadOnlyResponseCachetrueClient从readOnlyCacheMap更新数据,false则跳过readOnlyCacheMap直接从readWriteCacheMap更新eureka.server.responsecCacheUpdateIntervalMs30000readWriteCacheMap更新至readOnlyCacheMap周期,默认30seureka.server.evictionIntervalTimerInMs60000清理未续约节点(evict)周期,默认60seureka.instance.leaseExpirationDurationInSeconds90清理未续约节点超时时间,默认90s关键类 类名说明com.netflix.eureka.registry.AbstractInstanceRegistry保存服务注册信息,持有registry和responseCache成员变量com.netflix.eureka.registry.ResponseCacheImpl持有readWriteCacheMap和readOnlyCacheMap成员变量四、Eureka ClientEureka Client存在两种角色:服务提供者和服务消费者,作为服务消费者一般配合Ribbon或Feign(Feign内部使用Ribbon)使用。Eureka Client启动后,作为服务提供者立即向Server注册,默认情况下每30s续约(renew);作为服务消费者立即向Server全量更新服务注册信息,默认情况下每30s增量更新服务注册信息;Ribbon延时1s向Client获取使用的服务注册信息,默认每30s更新使用的服务注册信息,只保存状态为UP的服务。 二级缓存 缓存类型说明localRegionAppsAtomicReference周期更新,类DiscoveryClient成员变量,Eureka Client保存服务注册信息,启动后立即向Server全量更新,默认每30s增量更新upServerListZoneMapConcurrentHashMap周期更新,类LoadBalancerStats成员变量,Ribbon保存使用且状态为UP的服务注册信息,启动后延时1s向Client更新,默认每30s更新缓存相关配置 配置默认说明eureka.instance.leaseRenewalIntervalInSeconds30Eureka Client 续约周期,默认30seureka.client.registryFetchIntervalSeconds30Eureka Client 增量更新周期,默认30s(正常情况下增量更新,超时或与Server端不一致等情况则全量更新)ribbon.ServerListRefreshInterval30000Ribbon 更新周期,默认30s关键类 类名说明com.netflix.discovery.DiscoveryClientEureka Client 负责注册、续约和更新,方法initScheduledTasks()分别初始化续约和更新定时任务com.netflix.loadbalancer.PollingServerListUpdaterRibbon 更新使用的服务注册信息,start初始化更新定时任务com.netflix.loadbalancer.LoadBalancerStatsRibbon,保存使用且状态为UP的服务注册信息五、默认配置下服务消费者最长感知时间Eureka Client时间说明上线30(readOnly)+30(Client)+30(Ribbon)=90sreadWrite -> readOnly -> Client -> Ribbon 各30s正常下线30(readonly)+30(Client)+30(Ribbon)=90s服务正常下线(kill或kill -15杀死进程)会给进程善后机会,DiscoveryClient.shutdown()将向Server更新自身状态为DOWN,然后发送DELETE请求注销自己,registry和readWriteCacheMap实时更新,故UI将不再显示该服务实例非正常下线30+60(evict)*2+30+30+30= 240s服务非正常下线(kill -9杀死进程或进程崩溃)不会触发DiscoveryClient.shutdown()方法,Eureka Server将依赖每60s清理超过90s未续约服务从registry和readWriteCacheMap中删除该服务实例考虑如下情况 0s时服务未通知Eureka Client直接下线;29s时第一次过期检查evict未超过90s;89s时第二次过期检查evict未超过90s;149s时第三次过期检查evict未续约时间超过了90s,故将该服务实例从registry和readWriteCacheMap中删除;179s时定时任务从readWriteCacheMap更新至readOnlyCacheMap;209s时Eureka Client从Eureka Server的readOnlyCacheMap更新;239s时Ribbon从Eureka Client更新。因此,极限情况下服务消费者最长感知时间将无限趋近240s。 六、应对措施服务注册中心在选择使用Eureka时说明已经接受了其优先保证可用性(A)和分区容错性(P)、不保证强一致性(C)的特点。如果需要优先保证强一致性(C),则应该考虑使用ZooKeeper等CP系统作为服务注册中心。分布式系统中一般配置多节点,单个节点服务上线的状态更新滞后并没有什么影响,这里主要考虑服务下线后状态更新滞后的应对措施。 6.1 Eureka Server1.缩短readOnlyCacheMap更新周期。缩短该定时任务周期可减少滞后时间。 eureka.server.responsecCacheUpdateIntervalMs: 10000 # Eureka Server readOnlyCacheMap更新周期2.关闭readOnlyCacheMap。中小型系统可以考虑该方案,Eureka Client直接从readWriteCacheMap更新服务注册信息。 ...

May 7, 2019 · 1 min · jiezi

Redis闲谈1构建知识图谱

场景:Redis面试 (图片来源于网络) 面试官: 我看到你的简历上说你熟练使用Redis,那么你讲一下Redis是干嘛用的?小明: (心中窃喜,Redis不就是缓存吗?)Redis主要用作缓存,通过内存高效地存储非持久化数据。 面试官: Redis可以用作持久化的存储吗? 小明 :嗯...应该可以吧... 面试官: 那Redis怎么进行持久化操作呢? 小明:嗯...不是太清楚。 面试官: Redis的内存淘汰机制有哪些? 小明:嗯...没了解过 面试官:我们还可以用Redis做哪些事情?分别利用了Redis的哪个指令? 小明:我只知道Redis还可以做分布式锁、消息队列... 面试官:好了,我们进入下一个话题... 思考:很明显,小明同学在面试过程中关于Redis的表现和回答肯定是比较失败的。Redis是我们工作中每天都会使用到的东西,为什么一到面试却变成了丢分项呢? 作为开发者,我们习惯了使用大神们已经封装好的东西,以此保障我们能够更专注于业务开发,却不知道这些常用工具的底层实现是什么,因此尽管平时应用起来得心应手,但一到面试还是无法让面试官眼前一亮。 本文总结了一些Redis的知识点,有原理有应用,希望可以帮助到大家。 一、Redis是什么REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI 、C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 这里我引用了Redis教程里对Redis的描述,很官方,但是很标准。 **可基于内存亦可持久化的日志型、Key-Value数据库。**我认为这个描述很贴切很全面。 1.1 Redis的行业地位Redis是互联网技术领域使用最为广泛的存储中间件,因超高的性能、完美的文档、多方面的应用能力以及丰富完善的客户端支持在存储方面独当一面,广受好评,尤其以其性能和读取速度而成为了领域中最受青睐的中间件。基本上每一个软件公司都会使用Redis,其中包括很多大型互联网公司,比如京东、阿里、腾讯、github等。因此,Redis也成为了后端开发人员必不可少的技能。 1.2 知识图谱在我看来,学习每一项技术,都需要有一个清晰的脉络和结构,不然你也不知道自己会了哪些、还有多少没学会。就像一本书,如果没有目录章节,也就失去了灵魂。 因此我试图总结出Redis的知识图谱,也称为脑图,如下图所示,可能知识点不是很全,后续会不断更新补充。 本系列文章的知识点也会和这个脑图基本一致,本文先介绍Redis的基本知识,后续文章会详细介绍Redis的数据结构、应用、持久化等多个方面。 二、Redis优点2.1 速度快作为缓存工具,Redis最广为人知的特点就是快,到底有多快呢?Redis单机qps(每秒的并发)可以达到110000次/s,写的速度是81000次/s。那么,Redis为什么这么快呢? 绝大部分请求是纯粹的内存操作,非常快速;使用了很多查找操作都特别快的数据结构进行数据存储,Redis中的数据结构是专门设计的。如HashMap,查找、插入的时间复杂度都是O(1);采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁、释放锁操作,没有因为可能出现死锁而导致的性能消耗;用到了非阻塞I/O多路复用机制。2.2 丰富的数据类型Redis有5种常用的数据类型:String、List、Hash、set、zset,每种数据类型都有自己的用处。 2.3 原子性,支持事务Redis支持事务,并且它的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。 2.4 丰富的特性Redis具有丰富的特性,比如可以用作分布式锁;可以持久化数据;可以用作消息队列、排行榜、计数器;还支持publish/subscribe、通知、key过期等等。当我们要用中间件来解决实际问题的时候,Redis总能发挥出自己的用处。 三、Redis和Memcache对比Memcache和Redis都是优秀的、高性能的内存数据库,一般我们说到Redis的时候,都会拿Memcache来和Redis做对比。(为什么要做对比呢?当然是要衬托出Redis有多好,没有对比,就没有伤害~)对比的方面包括: 3.1 存储方式Memcache把数据全部存在内存之中,断电后会挂掉,无法做到数据的持久化,且数据不能超过内存大小。Redis有一部分数据存在硬盘上,可以做到数据的持久性。3.2 数据支持类型Memcache对数据类型支持相对简单,只支持String类型的数据结构。Redis有丰富的数据类型,包括:String、List、Hash、Set、Zset。3.3 使用的底层模型它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM机制 ,因为一般的系统调用系统函数,会浪费一定的时间去移动和请求。3.4 存储值大小Redis最大可以存储1GB,而memcache只有1MB。看到这里,会不会觉得Redis特别好,全是优点,完美无缺?其实Redis还是有很多缺点的,这些缺点平常我们该如何克服呢? 四、Redis存在的问题及解决方案4.1 缓存数据库的双写一致性的问题问题:一致性的问题是分布式系统中很常见的问题。一致性一般分为两种:强一致性和最终一致性,当我们要满足强一致性的时候,Redis也无法做到完美无瑕,因为数据库和缓存双写,肯定会出现不一致的情况,Redis只能保证最终一致性。 解决:我们如何保证最终一致性呢? 第一种方式是给缓存设置一定的过期时间,在缓存过期之后会自动查询数据库,保证数据库和缓存的一致性。如果不设置过期时间的话,我们首先要选取正确的更新策略:先更新数据库再删除缓存。但我们删除缓存的时候也可能出现某些问题,所以需要将要删除的缓存的key放到消息队列中去,不断重试,直到删除成功为止。4.2 缓存雪崩问题问题: 我们应该都在电影里看到过雪崩,开始很平静,然后一瞬间就开始崩塌,具有很强的毁灭性。这里也是一样的,我们执行代码的时候将很多缓存的实效时间设定成一样,接着这些缓存在同一时间都会实效,然后都会重新访问数据库更新数据,这样会导致数据库连接数过多、压力过大而崩溃。 解决: 设置缓存过期时间的时候加一个随机值。设置双缓存,缓存1设置缓存时间,缓存2不设置,1过期后直接返回缓存2,并且启动一个进程去更新缓存1和2。4.3 缓存穿透问题问题: 缓存穿透是指一些非正常用户(黑客)故意去请求缓存中不存在的数据,导致所有的请求都集中到到数据库上,从而导致数据库连接异常。 解决: 利用互斥锁。缓存失效的时候,不能直接访问数据库,而是要先获取到锁,才能去请求数据库。没得到锁,则休眠一段时间后重试。采用异步更新策略。无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。提供一个能迅速判断请求是否有效的拦截机制。比如利用布隆过滤器,内部维护一系列合法有效的key,迅速判断出请求所携带的Key是否合法有效。如果不合法,则直接返回。4.4 缓存的并发竞争问题问题: 缓存并发竞争的问题,主要发生在多线程对某个key进行set的时候,这时会出现数据不一致的情况。 ...

May 5, 2019 · 1 min · jiezi

数据库Redis基础篇

欢迎关注公众号:【爱编码】如果有需要后台回复2019赠送1T的学习资料哦!! 简介Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存和消息代理。它支持诸如字符串、散列、列表、集、带范围查询的排序集、位图、hyperloglog、带半径查询和流的地理空间索引等数据结构。 Redis具有内置的复制、Lua脚本、LRU清除、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis集群的自动分区提供高可用性。 原理与架构Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。 单线程模型 因为Redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行。所有命令都会进入一个队列中,然后逐个被执行,因此不会产生并发问题。 为什么单线程还能这么快1.纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。 2.非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如下图所示 单线程避免了线程切换和竞态产生的消耗。注:阻塞的操作是会非常影响Redis性能,这个下次再总结 API使用场景命令语法可以到下面地址查,本节仅仅说使用场景。https://www.runoob.com/redis/... 字符串1.缓存功能Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。 类似下面这样子的伪代码 // 从MySQL获取用户信息userInfo = mysql.get(id);// 将userInfo序列化,并存入Redisredis.setex(userRedisKey, 3600, serialize(userInfo));// 返回结果return userInfo2.计数例如使用Redis作为文章点赞数计数的基础组件,用户每一次点赞,相应的点赞数就会自增1 long incrLikeCounter(long id) { key = "article:like:" + id; return redis.incr(key);}3.共享Session使用Redis将用户的Session进行集中管理,每次用户更新或者查询登录信息都直接从Redis中集中获取。 4.限速很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。类似如下伪代码 phoneNum = "138xxxxxxxx";key = "shortMsg:limit:" + phoneNum;// SET key value EX 60 NXisExists = redis.set(key,1,"EX 60","NX");if(isExists != null || redis.incr(key) <=5){// 通过}else{// 限速}哈希关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。 相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对fieldvalue对应每个用户的属性。类似如下伪代码: UserInfo getUserInfo(long id){// 用户id作为key后缀userRedisKey = "user:info:" + id;// 使用hgetall获取所有用户信息映射关系userInfoMap = redis.hgetAll(userRedisKey);UserInfo userInfo;if (userInfoMap != null) {// 将映射关系转换为UserInfouserInfo = transferMapToUserInfo(userInfoMap);} else {// 从MySQL中获取用户信息userInfo = mysql.get(id);// 将userInfo变为映射关系使用hmset保存到Redis中redis.hmset(userRedisKey, transferUserInfoToMap(userInfo));// 添加过期时间redis.expire(userRedisKey, 3600);}return userInfo;}列表列表是一种比较灵活的数据结构,它可以充当栈和队列。 ...

April 28, 2019 · 1 min · jiezi

分布式系统关注点18缓存穿透和缓存雪崩到底啥区别

如果第二次看到我的文章,欢迎文末扫码订阅我个人的公众号(跨界架构师)哟~ 本文长度为2805字,建议阅读8分钟。坚持原创,每一篇都是用心之作~ 有句话说得好,欲要使其毁灭,先要使其疯狂。当你沉浸在缓存所带来的系统tps飙升的喜悦中时,使你系统毁灭的种子也已经埋在其中。 而且,你所承载的tps越高,它所带来的毁灭性更大。 在前两篇《360°全方位解读「缓存」》和《先写DB还是「缓存」?》中,我们已经对缓存有了一定的认识,并且知道了关于缓存相关的「一致性」问题的最佳实践。 这次,我们就来聊聊隐藏在缓存中的毁灭性种子是什么? 我们从前一篇文章《先写DB还是「缓存」?》中多次提到的「cache miss」说起。 缓存雪崩在前一篇文章《先写DB还是「缓存」?》中,我们多次提到了「cache miss」这个词,利用「cache miss」来更好的保障DB和缓存之间的数据一致性。 然而,任何事物都是有两面性的,「cache miss」在提供便利的同时,也带来了一个潜在风险。 这个风险就是「缓存雪崩」。 在图中的第二步,大量的请求并发进入,这里的一次「cache miss」就有可能导致产生「缓存雪崩」。 不过,虽然「cache miss」会产生「缓存雪崩」,但「缓存雪崩」并不仅仅产生于「cache miss」。 雪崩一词源于「雪崩效应」,是指像「多米勒骨牌」这样的级联反应。前面没顶住,导致影响后面,如此蔓延。(关于对应雪崩的方式参考之前的文章,文末放链接) 所以「缓存雪崩」的根本问题是:缓存由于某些原因未起到预期的缓冲效果,导致请求全部流转到数据库,造成数据库压力过重。 因此,流量激增、高并发下的缓存过期、甚至缓存系统宕机都有可能产生「缓存雪崩」问题。 怎么解决这个问题呢?宕机可以通过做高可用来解决(可以参考之前的文章,文末放链接)。而在“流量激增”、“高并发下的缓存过期”这两种场景下,也有两种方式可以来解决。 加锁排队通过加锁或者排队机制来限制读数据库写缓存的线程数量。比如,下面的伪代码就是对某个key只允许一个线程进入的效果。 key = "aaa";var cacheValue = cache.read(key);if (cacheValue != null) { return cacheValue;}else { lock(key) { cacheValue = cache.read(key); if (cacheValue != null) { return cacheValue; } else { cacheValue = db.read(key); cache.set(key,cacheValue); } } return cacheValue;} 这个比较好理解,就不废话了。 ...

April 25, 2019 · 1 min · jiezi

如何才能将一首歌的背景音乐提取出来

我们经常会在一些音乐播放器上听歌,有时候听到好听的歌曲时候都会将它下载到自己的手机中,自己没事的时候就会打开来听一听,有些人将自己的手机歌单中会设置很多的分组,把自己喜欢的歌曲都放在一起,但是也有用户想将自己喜欢的歌曲部分音乐提取出来制作成一首串烧的歌曲,这样就可以一直听了,那么如何才能将音乐提取出来呢?接下来就一起来看看吧! 软件介绍: 迅捷音频转换器是一款多功能的音频编辑处理软件,软件具有功能齐全,操作简单等特点,支持音频剪切、音频提取、音频转换,可以多种分割方式进行音频剪切,而且软件不仅支持单个文件操作,还支持文件批量操作! 操作步骤: 1、先把提取音乐的工具准备好,准备好之后我们再将它打开就可以了。 2、打开后,当然就是添加音乐文件进行提取了,点击音频提取,接着会显示添加文件和添加文件夹这两种方式,大家选择一种添加就可以了。 3、之后,需要我们操作提取音频的部分了,点击添加片段指南,我们可以拖拽住进度条进行调整,这个时候下方的当前时间点就会显示提取音频的时间,提取音频的片段设置好后,如果没有需要删除的片段,大家就可以点击确定。 4、待我们都设置好之后,这个时候就可以设置它的保存路径了,找到文件输出目录,将保存路径设置好之后,点击开始提取。 5、开始提取之后,会看到一些小对号,只要显示的话就说明已经提取成功了。 以上就是如何才能将一首歌曲提取出来的所有操作步骤,你们学会了吗?由衷的感谢你们的阅读。

April 24, 2019 · 1 min · jiezi

三大缓存问题及解决方案

在我们的实际开发应用中,缓存机制的广泛存在,大大的提高了系统对数据库的请求承受阈值,但是在一些特定的场景下,需要去了解它可能出现的问题和对应的解决方案,才能更好的增加我们系统的健壮性 1.缓存穿透 问题场景在一般的查询场景下,当一次查询从缓存中查询不到对应的信息,那么会继续去访问数据库进行查询,这种过程被称为缓存穿透。看似正常的操作,实际上,当有人恶意的去使用根本不存在的数据去频繁访问服务器,可能就会造成系统的瘫痪。 解决方案bloom filter(布隆过滤器):是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。它是一个判断元素是否存在集合的快速的概率算法。在某些查询中,可以将所有可能的查询条件放入这个集合,在查询之前使用这个集合对查询条件进行过滤,就可以避免缓存穿透的问题。空值缓存:在第一次查询为空值之后,将这个查询条件key存入缓存中,缓存时间设置为较短时间,这样可以应对一些短时间内大量重复查询的情况。 2.缓存雪崩 问题场景我们在使用例如Redis来进行缓存操作的时候,一般会给缓存设置一个过期时间,但是对于大量缓存过期时间相同的系统来说,可能会因为某个时间段缓存同时失效而造成所有本应该由缓存来接受的请求直接请求到数据库,造成数据库崩溃。当发生雪崩的时候,没有一片雪花觉得是自己的责任。 解决方案交叉失效时间:在设置缓存时间的时候,我们可以在一段合理的范围时间内,随机的去设置这些缓存的过期时间,避免同一过期时间 3.缓存击穿 问题场景缓存击穿是缓存雪崩的一个特例,某个单独的热点的缓存因为过期时间导致失效,那么同样会有大量的请求去访问数据库导致崩溃。与缓存雪崩不同的是,缓存击穿更像是某一热度点的缓存雪崩。 解决方案二级缓存:对于那些热度高的数据设置二级缓存,并且错开和一级缓存的失效时间,使请求不会同时穿透两层缓存去访问数据库

April 23, 2019 · 1 min · jiezi

我眼中的 Redis

引言打开Microsoft To-Do,发现Redis的学习计划还躺在那里。其实我对Redis的理解,仅仅停留在我认识这个单词的层面上。学习简介本来对这个Redis没什么兴趣的,不就是一个缓存的数据库而已吗?直到上次配置spring-redis的时候,发现这个东西没有用户名。spring: redis: host: 127.0.0.1 port: 6379 password:配置如上所示,只有主机、端口和密码,和普通的MySQL或其他数据库不同。Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。我们熟知的就是Redis的缓存,Redis采用C编写,运行异常的快。是有磁盘存储支持的内存数据库!适用于数据变化快且数据库大小可遇见(适合内存容量)的应用程序。使用场景:股票价格、数据分析、实时数据搜集、实时通讯。NoSQLRedis属于NoSQL,NoSQL = Not Only SQL。如今数据量越来越大,传统的关系型数据库已经无法应付如此大数据的需求了。举个例子:假设我们使用关系型数据库存储朋友圈,那一天会产生多少数据?再查询的时候数据库不就死了吗?这让我想起了我之前关注的一个帖子:腾讯微信的后台数据库到底是怎么设计的?无知的人相谈甚欢,最后好像是官方的哥们是在看不下去了,回复:谁告诉你们微信用的是关系型数据库?普通关系型数据库,如果只查询的话效率很高?如果算上读写的话?那可能传统的数据库都承受不住高并发。重要的是关系型数据库没法扩展,大家想一想,因为数据之间是有关系的,所以数据库扩展绝对不像扩展后台服务规模一样再拎个服务器出来那么简单。由此可见,在数据量日渐增长的今天,为了解决大数据量与高并发的难题,NoSQL应运而生。NoSQL产品主要有四类:类型特点代表适用场景键值对存储能实现快速查询,但存储的数据缺少结构化Redis内容缓存,主要用于处理大数据的高访问负载列存储数据库查找速度快,可扩展性强,更容易进行分布式扩展,但功能相对局限HBase分布式的文件系统文档数据库数据结构要求不严格,查询性能不高,而且缺乏统一的查询语法MongoDBWeb应用(相较于普通的Key-Value,其Value是结构化的)图形数据库利用图结构相关算法,但需要对整个图做计算才能得出结果,不容易分布式Neo4j社交网络,推荐系统,专注于构建关系图谱这些数据库的名称大家或多或少应该听说过吧?今天才真正知道它们的作用,各有其特长,我们需根据业务场景动态选择。Facebook的消息存储采用的就是HBase数据库,支持大数据进行随机、实时访问。NoSQL因为数据之间都是没有关系的,所以易扩展,同时具有很高的读写性能,很适合高并发场景。历史2008年,意大利的一家创业公司Merzia推出了一款基于MySQL的网站实时统计系统LLOOGG。没过多久,创始人Salvatore Sanfilippo对MySQL的性能大失所望,于是他决定自己写一个数据库。牛人就是牛人,我们就算质疑MySQL的性能,想写也写不出来啊?!2009年,Salvatore Sanfilippo完成了数据库的编写,这就是Redis。Salvatore Sanfilippo将Redis开源,并一直进行着Redis的开发,直到今天。我们熟知的Github、StackOverflow、新浪微博等公司都是Redis的用户。缓存传统的缓存代码需要这样写,很冗长,都是重复的代码。ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();logger.info(“判断Redis中是否有Key”);if (redisTemplate.hasKey(url)) { logger.info(“Redis命中,从Redis中获取”); return valueOperations.get(url);}logger.info(“发起Get请求”);ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);logger.info(“存入缓存”);valueOperations.set(url, response.getBody(), TIME_OUT, TimeUnit.MINUTES);感谢Spring AOP,我们可以使用注解实现缓存功能。@Cacheable(“cacheName”)public List<Student> findAll() { return studentRepository.findAll();}使用注解实现缓存很简单,同时@Cacheable还有许多的高级用法,以后与大家详述。操作Redis有三种动作:GET:根据键查找值。SET:给定键存储值。DEL:删除键中的值。然后就是Redis的数据结构,这个觉得暂时还不需要知道,毕竟现在使用的是现成的@Cacheable注解,还不需要我们手动去操作Redis。一如代码深似海,软件之路很广很远。生命有限的我们不能把所有东西都精通,我们要在学习成本与能力提升之间进行权衡。总结故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步,驽马十驾,功在不舍。

April 20, 2019 · 1 min · jiezi

处理高并发的一般思路

前言今天看见有人聊目前系统有2亿的PV,该如何优化?当我看到这个话题的时候,突然在想自己工作中也遇到了不少高并发的场景了,所以即兴发挥,在这里简单总结和分享下,欢迎指正和补充。正文读操作关于读,我们一般遵循如下优先级:优先级技术方案说明示例最高尽可能静态化对实时性要去不高的数据,尽可能全走CDN例如获取基础商品信息高就近使用内存优先级服务器内存、远程内存服务例如秒杀、抢购库存(优先分配库存到服务器内存,其次远程内存服务<又涉及额外网络IO>)极低数据库(能不读就不要读)连接池、sql优化常见业务写操作关于写,我们一般会按照数据的一致性要求级别来看:数据一致性要求技术方案不高先写内存(优先级从服务器内存到远程内存服务) 再异步储存高同步完成最关键的任务 异步保证其他任务最终成功削峰限流从简单到复杂:简单程度技术方案最简单百分比流量拒绝(随机、没有先到先得不够公平)简单原子操作限流(优先级使用服务器内存、其次远程内存服务)稍麻烦队列限流(先到先得,公平)服务稳定性在高并发的场景,有时候为了保证核心业务的正常进行,我们需要对一些次要的业务进行服务降级。简单的降级方案如下:配置开关降级:手动进行配置开关降级定时开关降级:自动定时降级系统架构关于系统架构,不用想的太复杂,简单的拆离此业务即可。运维架构部署层面,尽可能的把此类服务单独部署。武器"工欲善其事,必先利其器",处理高并发我们当然少不了好的武器。以下是高并发“三剑客”:技术名词说明异步异步回调,层层回调似灾难(Promise也是很臃肿的链式代码)epollIO多路复用,nginx/redis方案协程轻量,用户态调度高并发能力

April 19, 2019 · 1 min · jiezi

Redis的KEYS命令引起宕机事件

摘要: 使用 Redis 的开发者必看,吸取教训啊!原文:Redis 的 KEYS 命令引起 RDS 数据库雪崩,RDS 发生两次宕机,造成几百万的资金损失作者:陈浩翔Fundebug经授权转载,版权归原作者所有。最近的互联网线上事故发生比较频繁,2018 年 9 月 19 号顺丰发生了一起线上删库事件,在这里就不介绍了。在这里讲述一下最近发生在我公司的事故,以及如何避免,并且如何处理优化。间接原因还有很多,技术跟不上业务的发展,由每日百万量到千万级是一个大的跨进,公司对于系统优化的处理优先级不高,技术开发人手的短缺第一次宕机2018 年 9 月 13 号某个点,公司某服务化项目的 RDS 实例连接飙升,CPU 升到 100%,拒绝了其他应用的所有请求服务整个过程如下:监控报警,显示 RDS 的 CPU 使用率达到 80%以上,DBA 介入,准备 KILL 慢 SQL1 分钟内,没有发现明显阻塞的 SQL,CPU 持续上升到 99%5 分钟内,大量应用报警,并且拒绝服务,RDS 的监控显示出现大量慢 SQL,联系服务器数据库提供商进行协助8 分钟内,进行数据库主备切换(业务会受损,但是也没办法,没有定位到问题)9 分钟内,部分业务恢复,但是一些业务订单的回调消息堆积超过 20w,备库的 CPU 使用率也持续上升15 分钟内,备库 CPU 使用率超过 97%,业务再次中断,进行切回主库,并进行限流20 分钟内,关闭一些次要应用的流量入口25 分钟内,主库 CPU 使用率恢复正常30 分钟内,逐步开启关闭的限流应用35 分钟内,所有应用恢复正常接下来就是与服务器数据库提供商成立应急小组紧急优化可能出现的慢 SQL,虽然说可能解决了一些慢 SQL,但此次并没有定位到具体的问题,也就为几天后再次发生宕机事件埋下了伏笔事故影响某服务化项目服务不可用几十分钟,造成订单数减少几十万笔,损失百万资金。原因分析当时是没有定位到具体的原因的,但是下面的原因也是一部分可能引起宕机的情况。某服务化项目的业务增速非常快,在高峰期,数据库 QPS 突破 35000,系统处于高负荷状态。在高峰期如果同时执行几个全表扫描的 SQL,会造成数据库压力急剧上升,应用超时增多,前端应用超时,用户重试,流量飙升,形成了雪崩效应。主要原因在与一些老项目的 SQL 查询性能较差,并且使用的主库,对数据库影响较大。数据库 QPS 太高,但是缓存方案因为人手原因一直没有落地,慢 SQL 的问题处理优先级应该提升改进方案针对每个应用建一个数据库账号,严格按照规范使用缓存优化方案即时落地,慢 SQL 问题优先处理,集中处理目前已经发现的慢 SQL(查询时间超过 1S)升级数据库配置迁移非核心业务到新的 RDS 实例中去第二次宕机由于上一次的宕机原因未找到,所以此次的宕机是可以预见的。2018 年 9 月 19 号,还是一样的"配方",还是原来的"味道"。同一个 RDS,CPU 飙升至 100%,接下来就是拒绝服务,宕机。当然,有了第一次的经验,直接主从切换,在几十秒左右就恢复了所有业务,但还是严重影响了公司的业务和形象。原因分析恢复业务后,公司紧急召开了紧急事故研究会议,当然,我的级别是参与不了的。公司的高管,高层技术架构、DBA、各个项目的主负责人一起进行了会议。在此次会议中,经过查看各个项目的日志,后台的监控数据,发现在那台 RDS 数据库 CPU 飙升时,有一台 Redis 数据库内存将近 100%,然后急剧下降。联系第一次的宕机情况,也是类似的。接下来就是联系服务器数据库提供商,将那台 Redis 最近一周的命令全部调用出来,最后发现,在那个时间点运行了一条keys *…*命令。公司的一个工程师执行 keys 模糊的匹配命令是为了清理没用的键,但是没有考虑到keys *进行模糊匹配引发 Redis 锁,造成 Redis 锁住,CPU 飙升,引起了所有调用链路的超时并且卡住,等 Redis 锁的那几秒结束,所有的请求流量全部请求到 RDS 数据库中,使数据库产生了雪崩,使数据库宕机。改进方案所有线上操作,全部要经过运维通过后方可执行,运维部门逐步快速收回各项权限新增 Redis 实例,进行分离如果有使用类似 keys 正则命令需求,使用 scan 命令代替总结该事件中出现的两次事故,完全是由于人为操作引起的,如果那位工程师,看过 Redis 的开发规范,会发现是建议禁用 keys 命令的。另外,有线上的命令操作,一定要经过运维评估后方可进行操作,估计那个工程师是老员工吧,有权限,然后直接就进行操作了。另外,公司的业务发展确实很快,技术跟不上,这是非常非常危险的,极大的增加了宕机的概率。在业务量不大的情况下,那位工程师的操作是完全没什么问题的,毕竟并发也不大,但是现在,随着公司的发展,业务量的成倍成倍增加,技术的扩展却没有随着增长那么快。公司的技术人手不足也是一方面,绝大多数人都是边维护老项目边做新功能,但是对于项目的重构优化,人手却少了很多,项目优化的优先级不高,这也是很大的一个原因,极有可能出现类似的情况,新服务化构建迫在眉睫。最后的最后,线上操作的任何一条命令,再小心也不为过,因为由于你的一个符号而引起的事故可能是你所承担不起的。Redis 开发建议最后附上 Redis 的一些开发规范和建议1. 冷热数据分离,不要将所有数据全部都放到 Redis 中虽然 Redis 支持持久化,但是 Redis 的数据存储全部都是在内存中的,成本昂贵。建议根据业务只将高频热数据存储到 Redis 中【QPS 大于 5000】,对于低频冷数据可以使用 MySQL/ElasticSearch/MongoDB 等基于磁盘的存储方式,不仅节省内存成本,而且数据量小在操作时速度更快、效率更高!2. 不同的业务数据要分开存储不要将不相关的业务数据都放到一个 Redis 实例中,建议新业务申请新的单独实例。因为 Redis 为单线程处理,独立存储会减少不同业务相互操作的影响,提高请求响应速度;同时也避免单个实例内存数据量膨胀过大,在出现异常情况时可以更快恢复服务! 在实际的使用过程中,redis 最大的瓶颈一般是 CPU,由于它是单线程作业所以很容易跑满一个逻辑 CPU,可以使用 redis 代理或者是分布式方案来提升 redis 的 CPU 使用率。3. 存储的 Key 一定要设置超时时间如果应用将 Redis 定位为缓存 Cache 使用,对于存放的 Key 一定要设置超时时间!因为若不设置,这些 Key 会一直占用内存不释放,造成极大的浪费,而且随着时间的推移会导致内存占用越来越大,直到达到服务器内存上限!另外 Key 的超时长短要根据业务综合评估,而不是越长越好!4. 对于必须要存储的大文本数据一定要压缩后存储对于大文本【+超过 500 字节】写入到 Redis 时,一定要压缩后存储!大文本数据存入 Redis,除了带来极大的内存占用外,在访问量高时,很容易就会将网卡流量占满,进而造成整个服务器上的所有服务不可用,并引发雪崩效应,造成各个系统瘫痪!5. 线上 Redis 禁止使用 Keys 正则匹配操作Redis 是单线程处理,在线上 KEY 数量较多时,操作效率极低【时间复杂度为 O(N)】,该命令一旦执行会严重阻塞线上其它命令的正常请求,而且在高 QPS 情况下会直接造成 Redis 服务崩溃!如果有类似需求,请使用 scan 命令代替!6. 可靠的消息队列服务Redis List 经常被用于消息队列服务。假设消费者程序在从队列中取出消息后立刻崩溃,但由于该消息已经被取出且没有被正常处理,那么可以认为该消息已经丢失,由此可能会导致业务数据丢失,或业务状态不一致等现象发生。为了避免这种情况,Redis 提供了 RPOPLPUSH 命令,消费者程序会原子性的从主消息队列中取出消息并将其插入到备份队列中,直到消费者程序完成正常的处理逻辑后再将该消息从备份队列中删除。同时还可以提供一个守护进程,当发现备份队列中的消息过期时,可以重新将其再放回到主消息队列中,以便其它的消费者程序继续处理。7. 谨慎全量操作 Hash、Set 等集合结构在使用 HASH 结构存储对象属性时,开始只有有限的十几个 field,往往使用 HGETALL 获取所有成员,效率也很高,但是随着业务发展,会将 field 扩张到上百个甚至几百个,此时还使用 HGETALL 会出现效率急剧下降、网卡频繁打满等问题【时间复杂度 O(N)】,此时建议根据业务拆分为多个 Hash 结构;或者如果大部分都是获取所有属性的操作,可以将所有属性序列化为一个 STRING 类型存储!同样在使用 SMEMBERS 操作 SET 结构类型时也是相同的情况!8. 根据业务场景合理使用不同的数据结构类型目前 Redis 支持的数据库结构类型较多:字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Sorted Set), Bitmap, HyperLogLog 和地理空间索引(geospatial)等,需要根据业务场景选择合适的类型。常见的如:String 可以用作普通的 K-V、计数类;Hash 可以用作对象如商品、经纪人等,包含较多属性的信息;List 可以用作消息队列、粉丝/关注列表等;Set 可以用于推荐;Sorted Set 可以用于排行榜等!9. 命名规范虽然说 Redis 支持多个数据库(默认 32 个,可以配置更多),但是除了默认的 0 号库以外,其它的都需要通过一个额外请求才能使用。所以用前缀作为命名空间可能会更明智一点。另外,在使用前缀作为命名空间区隔不同 key 的时候,最好在程序中使用全局配置来实现,直接在代码里写前缀的做法要严格避免,这样可维护性实在太差了。如:系统名:业务名:业务数据:其他但是注意,key 的名称不要过长,尽量清晰明了,容易理解,需要自己衡量10. 线上禁止使用 monitor 命令禁止生产环境使用 monitor 命令,monitor 命令在高并发条件下,会存在内存暴增和影响 Redis 性能的隐患11. 禁止大 string核心集群禁用 1mb 的 string 大 key(虽然 redis 支持 512MB 大小的 string),如果 1mb 的 key 每秒重复写入 10 次,就会导致写入网络 IO 达 10MB;12. redis 容量单实例的内存大小不建议过大,建议在 10~20GB 以内。redis 实例包含的键个数建议控制在 1kw 内,单实例键个数过大,可能导致过期键的回收不及时。13. 可靠性需要定时监控 redis 的健康情况:使用各种 redis 健康监控工具,实在不行可以定时返回 redis 的 info 信息。客户端连接尽量使用连接池(长链接和自动重连)。 ...

April 19, 2019 · 2 min · jiezi

http 缓存小结

为了优化性能,使用缓存是一种比较常见的手段。那么如何实现缓存以及如何避免缓存呢,都是要探讨的话题。可以从三个部分:http 缓存、cookie、localStorage&sessionStorage 来重点讲述缓存实现的原理、过程以及实现的方式。由于篇幅原因,本篇重点讲述 http 缓存。1.基本概念缓存命中: 使用已有的副本为到达的请求提供资源而不用从服务器中获取资源。 缓存未命中: 达缓存的请求没有副本可用,而被转发给原始服务器,与缓存命中相反。 CHP: Cache Hit Percentage,缓存提供服务请求所占有的比例,缓存的文件个数/请求资源个数。 下图是打开百度资源后所请求的资源(部分)情况:可以看到其中有两个资源是从服务器中获取的,其余是从缓存中获取,那么其CHP值为:80%。2.强缓存所谓的强缓存是指请求资源的时候不需要发送 http 向服务器发送请求,直接从客户端获取资源。实现的方式是有 http 的 Expires,Cache-Control两个response header实现的。2.1 ExpiresExpires是 http 1.0 提出的一个 header,其值是一个资源有效期的绝对时间,实现缓存的过程如下: 浏览器发送请求,服务器在response 的header中加入Expires,并将资源返回给浏览器; 浏览器获取到资源后,会存储一个资源的副本,并将EXpires中的时间进行保存; 当再次进行资源请求的时候,浏览器会将当前的请求时间与之前存储的时间进行比较; 如果请求的时间在有效期之内,则直接使用副本,反之会向服务器发送资源并且重新更新资源的有效期Expires的值为一个绝对时间,当客户端改变了时间或者时区问题,会导致缓存失效;因此在http 1.1 中引入了Cache-Control。Cache-Control与Expires同时存在的时候Cache-Control的优先级大于Expires2.2 Cache-ControlCache-Control 实现的过程与 Expires 类似,其常见的有三个值: max-age: 以秒为单位,表示资源缓存的相对时间。浏览器再次进行请求的时候,会将上次请求的时间与max-age相对时间求和,在与现在请求的时间相比较,如果在计算的时间范围内,则缓存可用; no-cache: 表示资源可以进行缓存,但是在下次进行使用的时候,必须去服务器端进行再验证。如果返回的response status 是304,那么就是从缓存中获取资源,如果是200 则从服务器端获取资源; no-store: 资源不能进行本地缓存,再次请求资源的时候,需要去服务器重新获取资源。Cache-Control:max-ag=0 与 Cache-Control:no-cache 在使用效果上没有多大的区别,区别在于服务器挂了是否可以使用之前的缓存的response 即 max-age=0 可以看到之前缓存的内容,页面正常显示之前的内容而 no-cache 则返回错误5xx状态码2.3 如何加入Expires与Cache-Control头部上面讲述利用Expires与Cache-Control如何实现缓存的,那怎么样在请求资源的时候加入这两个header呢,有以下两种方式: (1) 通过代码的方式,在web服务器返回的响应中添加响应头部,如在 Express 框架中使用 setHeader 加入,代码如下:const picMap = { ’logo.png’: ’no-store’, ‘avatar.png’: ’no-cache’, ‘background.png’: ‘max-age: 36000’}app.use(express.static(publicPath, setHeaders(res, filePath, stat) { let baseName = path.basename(filePath); picMap[baseName] && res.set(‘Cache-Control’, picMap[baseName])}));(2) 通过配置web服务器的方式,在服务器配置文件中加入 Expires 与 Cache-Control,进行统一配置。3.协商缓存协商缓存是在用户强缓存失败的情况下,向服务器端进行再验证。它与强缓存的区别在于会向服务器发送请求,但是不会获取资源,浏览器端请求的资源还是从缓存中获取。其实现有两对首部:【Last-Modified,If-Modified-Since】、【ETag、If-None-Match】其中以【Last-Modified,If-Modified-Since】为例,讲解实现的过程。浏览器第一次请求一个资源,服务器会将资源以及资源的最后修改时间(时间放入Last-Modified)发送给浏览器;浏览器获取资源,将资源以及修改时间进行保存;浏览器再次请求相同资源的同时,在请求的头部加入 If-Modified-Since,其值为上一次请求保存的时间;服务器会将资源最后修改的时间与 If-Modified-Since 相比较,如果时间一致就返回 304 状态码,反之则发送新的资源给服务器。4.缓存避免开发侧(1) 给资源加上一个动态的参数,如css/index.css?v=0.1 (2) 如果缓存问题出现在 ajax 请求中,可以给请求地址添加随机数;用户侧(1) F5:cache-control:max-age=0 (2) Ctrl+F5:请求的时候不带上任何缓存头参考文献[1] 《http权威指南》。 [2] https://www.cnblogs.com/lyzg/… ...

April 16, 2019 · 1 min · jiezi

分布式系统关注点——先写DB还是「缓存」?

如果第二次看到我的文章,欢迎文末扫码订阅我个人的公众号(跨界架构师)哟~ 本文长度为4209字,建议阅读12分钟。坚持原创,每一篇都是用心之作~在前一篇《360°全方位解读「缓存」》中,我们聊了运用缓存的三种思路,以及在一个完整的系统中可以设立缓存的几个位置,并且分享了关于浏览器缓存、CDN缓存、网关(代理)缓存的一些使用经验。这次Z哥将深入到实际场景中,来看一下「进程内缓存」、「进程外缓存」运用时的一些最佳实践。由于篇幅原因,这次先聊三个问题。首当其冲的就是“先写DB还是缓存?”。我想,只要你开始运用缓存,这会是你第一个要好好思考的问题,否则在前方等待你的就是灾难。。。先写DB还是缓存?一个程序可以没有缓存,但是一定要有数据库。这是大家的普遍观点,所以数据库的重要性在你的潜意识里总是被放在了第一位。先DB再缓存如果不细想的话你可能会觉得,数据库操作失败了,自然缓存也不用操作了;数据库操作成功了,再操作缓存,没毛病。但是数据库操作成功,缓存操作的失败的情况该怎么解?(主要在用到redis,memcached这种进程外缓存的时候,由于网络因素,失败的可能性大增)办法也是有的,在操作数据库的时候带一个事务,如果缓存操作失败则事务回滚。大致的代码意思如下:begin trans var isDbSuccess = write db; if(isDbSuccess){ var isCacheSuccess = write cache; if(isCacheSuccess){ return success; } else{ rollback db; return fail; } } else{ return fail; } catch(Exception ex){ rollback db; }end trans如此一来就万无一失了吗?并不是。除了由于事务的引入,增加了数据库的压力之外,在极端场景下可能会出现rollback db失败的情况。是不是很头疼?解决这个问题的方式就是write cache的时候做delete操作,而不是set操作。如此一来,用多一次cache miss的代价来换rollback db失败的问题。就像图上所示,哪怕rollback失败了,通过一次cache miss重新从db中载入旧值。题外话:其实这种做法有一种专业的叫法——Cache Aside Pattern。为了便于记忆,你可以和分布式系统的CAP定理同时记忆,叫「缓存的CAP模式」。是不是看上去妥了?可以开始潇洒了?▲图片来源于网络,版权归原作者所有如果你的数据库没有做高可用的话,的确可以妥了。但是如果数据库做了高可用,就会涉及到主从数据库的数据同步,这就有新问题了。题外话:所以大家不要过度追求技术的酷炫,可能会得不偿失,自找麻烦。什么问题呢?就是如果在数据还未同步到「从库」的时候,由于cache miss去「从库」取到了未同步前的旧值。解决它的第一个方式很简单,也很粗暴。就是定时去「从库」读数据,发现数据和缓存不一样了就set到缓存里去。但是这个方式有点“治标不治本”。不断的从数据库定时读取,对资源的消耗大不说,这个间隔频率也不好定义一个比较合适的统一标准,太短吧,会导致重复读取的次数加大,太长吧,又会导致缓存和数据库不一致的时间变长。所以这个方案仅适用于项目中只有2、3处需要做这种处理的场景,并且还不能是数据会频繁修改的情况。因为在数据修改频次较高的场景,甚至可能还会出现这个定时机制所消耗的资源反而大于主程序的情况。一般情况下,另一种更普适性的方案是采用接下去聊的这种更底层的方式进行,就是“哪里有问题处理哪里”,当「从库」完成同步的时候再额外做一次delete cache或者set cache的操作。如此,虽说也没有100%解决短暂的数据不一致问题,但是已经将脏数据所存在的时长降到了最低(最终由主从同步的耗时决定),并且大大减少了无谓的资源消耗。可能你会说,“不行,这么一点时间也不能忍”怎么办?办法是有,但是会增加「主库」的压力。就是在产生数据库写入动作后的一小段时间内强制读「主库」来加载缓存。怎么实现呢?先得依赖一个共享存储,可以借助数据库或者也可以是我们现在正在聊的分布式缓存。然后,你在事务提交之后往共享存储中临时存一个{ key = dbname + tablename + id,value = null,expire = 3s }这样的数据,并且再做一次delete cache的操作。begin trans var isDbSuccess = write db; if(isDbSuccess){ var isCacheSuccess = delete cache; if(isCacheSuccess){ return success; } else{ rollback db; return fail; } } else{ return fail; } catch(Exception ex){ rollback db; }end trans//在这里做这个临时存储,{key,value,expire}。delete cache;如此一来,当「读数据」的时候发生cache miss,先判断是否存在这个临时数据,只要在3秒内就会强制走「主库」取数据。可以看到,不同的方案各有利弊,需要根据具体的场景仔细权衡。先缓存再DB你工作中的大部分场景对数据准确性肯定是低容忍的,所以一般不建议选择「先缓存再DB」的方案,因为内存是易失性的。一旦遇到操作缓存成功,操作DB失败的情况,问题就来了。在这个时候最新的数据只有缓存里有,怎么办?单独起个线程不断的重试往数据库写?这个方案在一定程度上可行,但不适合用于对数据准确性有高要求的场景,因为缓存一旦挂了,数据就丢了!题外话:哪怕选择了这个方案,重试线程应确保只有1个,否则会存在“ABBA”的「并发写」问题。可能你会说用delete cache不就没问题了?可以是可以,但是要有个前提条件,访问缓存的程序不会产生并发。因为只要你的程序是多线程运行的,一旦出现并发就有可能出现「读」的线程由于cache miss从数据库取的时候,「写」的线程还没将数据写到数据库的情况。所以,哪怕用delete cache的方式,要么带lock(多客户端情况下还得上分布式锁),要么必然出现数据不一致。值得注意的是,如果数据库同样做了高可用,哪怕带了lock,也还需要考虑和上面提到的「先DB再缓存」中一样的由于主从同步的时间差可能会产生的问题。当然了,「先缓存再DB」也不是一文不值。当对写入速度有极致要求,而对数据准确性没那么高要求的场景下就非常好使,其实就是前一篇(《360°全方位解读「缓存」》)提到的「延迟写」机制。小结一下,相比缓存来说,数据库的「高可用」一般会在系统发展的后期才会引入,所以在没有引入数据库「高可用」的情况下,Z哥建议你使用「先DB再缓存」的方式,并且缓存操作用delete而不是set,这样基本就可以高枕无忧了。但是如果数据库做了「高可用」,那么团队必然也形成一定规模了,这个时候就老老实实的做数据库变更记录(binlog)的订阅吧。到这里可能有的小伙伴要问了,“如果上了分布式缓存,还需要本地缓存吗?”。本地缓存还要不要?在解答这个问题之前我们先来思考一个问题,一个分布式系统最重要的价值是什么?是「无限扩展」,只要堆硬件就能应对业务增长。要达到这点的背后需要满足一个特性,就是程序要「无状态」。那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。所以,能用分布式缓存解决的问题就尽量不要引入本地缓存。否则引入分布式缓存的作用就小了很多。但是在少数场景下,本地缓存还是可以发挥其价值的,但是我们需要仔细识别出来。主要是三个场景:不经常变更的数据。(比如一天甚至好几天更新一次的那种)需要支撑非常高的并发。(比如秒杀)对数据准确性能容忍的场景。(比如浏览量,评论数等)不过,我还是建议你,除了第二种场景,否则还是尽量不要引入本地缓存。原因我们下面来说说。其实这个原因的根本问题就是在引入了本地缓存后,本地缓存(进程内缓存)、分布式缓存(进程外缓存)、数据库这三者之间的数据一致性该怎么进行呢?本地缓存、分布式缓存、db之间的数据一致性如果是个单点应用程序的话,很简单,将本地缓存的操作放在最后就好了。可能你会说本地缓存修改失败怎么办?比如重复key啊什么的异常。那你可以反思一下为这种数据为什么可以成功的写进数据库。。。但是,本地缓存带来的一个巨大问题就是:虽然一个节点没问题,但是多个本地缓存节点之间的数据如何同步?解决这个问题的方式中有两种和之前我们聊过的Session问题(《做了「负载均衡」就可以随便加机器了吗?》)是类似的。要么是由接收修改的节点通知其它节点变更(通过rpc或者mq皆可),要么借助一致性hash让同一个来源的请求固定落到一个节点上。后者可以让不同节点上的本地缓存数据都不重复,从源头上避免了这个问题。但是这两个方案走的都是极端,前者变更成本太高,比如需要通知上千个节点的话,这个成本难以接受。而后者的话对资源的消耗太高,而且还容易出现压力分摊不均匀的问题。所以,一般系统规模小的时候可以考虑前者,而规模越大越会选择后者。还有一种相对中庸一些的,以降低数据的准确性来换成本的方案。就是设置缓存定时过期或者定时往下游的分布式缓存拉取最新数据。这和前面「先DB再缓存」中提到的定时机制是一样的逻辑,胜在简单,缺点就是会存在更长时间的数据不一致。小结一下,本地缓存的数据一致性解决方案,能彻底解决的是借助一致性hash的方案,但是成本比较高。所以,如非必要还是慎重决定要不要做本地缓存。总结好了,我们一起总结一下。这次呢,Z哥先花了大量的篇幅和你讨论「先写DB还是缓存」的问题,并且带你层层深入,通过一点一点的演进来阐述不同的解决方案。然后与你讨论了「本地缓存」的意义以及如何在「分布式缓存」和「数据库」的基础上做好数据一致性,这其中主要是多个本地缓存节点之间的数据同步问题。希望对你有所启发。这次的缓存实践是一个非常好的例子,从中我们可以看到一件事情的精细化所带来的复杂度需要更加的精细化去解决,但是又会带来新的复杂度。所以作为技术人的你,需要无时无刻考虑该怎么权衡,而不是人云亦云。相关文章:分布式系统关注点——360°全方位解读「缓存」分布式系统关注点——做了「负载均衡」就可以随便加机器了吗?作者:Zachary出处:https://www.cnblogs.com/Zacha…如果你喜欢这篇文章,可以点一下文末的「赞」。这样可以给我一点反馈。: )谢谢你的举手之劳。▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码~。定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。 ...

April 11, 2019 · 1 min · jiezi

你与解决“缓存污染”只差这篇文章的距离

微信公众号:IT一刻钟大型现实非严肃主义现场一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员关注可第一时间了解更多精彩内容,定期有福利相送哟。什么是缓存污染?由于缓存的读取速度比非缓存要快上很多,所以在高性能场景下,系统在读取数据时,是首先从缓存中查找需要的数据,如果找到了则直接读取结果,如果找不到的话,则从内存或者硬盘中查找,再将查找到的结果存入缓存,以备下次使用。实际上,对于一个系统来说,缓存的空间是有限且宝贵的,我们不可能将所有的数据都放入缓存中进行操作,即便可以数据安全性也得不到保证,而且,如果缓存的数据量过大大,其速度也会变得越来越慢。这个时候就需要考虑缓存的淘汰机制,但是淘汰哪些数据,又保留哪些数据,这是一个问题。如果处理不得当,就会造成“缓存污染”问题。而缓存污染,是指系统将不常用的数据从内存移到缓存,造成常用数据的挤出,降低了缓存效率的现象。解决缓存污染的算法LFU算法LFU,英文名Least Frequently Used,字面意思就是最不经常使用的淘汰掉算法,是通过数据被访问的频率来判断一个数据的热点情况。其核心理念是“历史上这个数据被访问次数越多,那么将来其被访问的次数也多”。LFU中每个数据块都有一个引用计数器,所有数据块按照引用数从大到小的排序。步骤:新数据插入到尾部,并将计数设置为1;当队列中的数据被访问后,引用计数+1,然后重新排序,保持引用次数从大到小排序;当空间不足,需要淘汰数据时,将尾部引用计数最小的数据块删除。分析:由于是根据频数进行热点判断和淘汰,所以先天具备避免偶发性、周期性批量操作导致临时非热点数据大量涌入缓存,挤出热点数据的问题。虽然具备这种先天优势,但依旧存在另一种缓存污染问题,即历史热点数据污染当前热点数据,如果系统访问模式发生了改变,新的热点数据需要计数累加超过旧热点数据,才能将旧热点数据进行淘汰,造成热点效应滞后的问题。复杂度与代价:每次操作都需要进行计数和排序,并且需要维护每个数据块计数情况,会占用较高的内存与cpu。一个小思考,根据LFU算法,如何以O(1)时间复杂度实现get和put操作缓存?LFU-Aging算法LFU-Aging是基于LFU的改进算法,目的是解决历史热点数据对当前热点数据的污染问题。有些数据在开始时使用次数很多,但以后就不再使用,这类数据将会长时间留在缓存中,所以“除了访问次数外,还要考虑访问时间”,这也是LFU-Aging的核心理念。虽然算法将时间纳入了考量范围,但LFU-Aging并不是直接记录数据的访问时间,而是增加了一个最大平均引用计数的阈值,然后通过当前平均引用计数来标识时间,换句话说,就是将当前缓存中的平均引用计数值当作当前的生命年代,当这个生命年代超过了预设的阈值,就会将当前所有计数值减半,形成指数衰变的生命年代。分析:优点是当访问模式发生改变的时候,生命年代的指数衰变会使LFU-Aging能够更快的适用新的数据访问模式,淘汰旧的热点数据。复杂度与代价:在LFU的基础上又增加平均引用次数判断和统计处理,对cpu的消耗更高,并且当平均引用次数超过指定阈值(Aging)后,还需要遍历每一个数据块的引用计数,进行指数衰变。Window-LFU算法Window-LFU顾名思义叫做窗口期LFU,区别于原义LFU中记录所有数据的访问历史,Window-LFU只记录过去一段时间内(窗口期)的访问历史,相当于给缓存设置了有效期限,过期数据不再缓存。当需要淘汰时,将这个窗口期内的数据按照LFU算法进行淘汰。分析:由于是维护一段窗口期的记录,数据量会比较少,所以内存占用和cpu消耗都比LFU要低。并且这段窗口期相当于给缓存设置了有效期,能够更快的适应新的访问模式的变化,缓存污染问题基本不严重。复杂度与代价:维护一段时期内的数据访问记录,并对其排序。LRU算法LRU算法,英文名Least Recently Used,意思是最近最少使用的淘汰算法,根据数据的历史访问记录来进行淘汰数据,核心思想是“如果数据最近被访问过1次,那么将来被访问的概率会更高”,类似于就近优先原则。步骤:新数据插入到链表头部;每当命中缓存,便将命中的缓存数据移到链表头部;当链表满的时候,将链表尾部的数据丢弃。分析:偶发性的、周期性的批量操作会使临时数据涌入缓存,挤出热点数据,导致LRU热点命中率急剧下降,缓存污染情况比较严重。复杂度与代价:数据结构复杂度较低;每次需要遍历链表,找到命中的数据块,然后将数据移到头部。LRU-K算法LRU-K是基于LRU算法的优化版,其中K代表最近访问的次数,从某种意义上,LRU可以看作是LRU-1算法,引入K的意义是为了解决上面所提到的缓存污染问题。其核心理念是从“数据最近被访问过1次”蜕变成“数据最近被访问过K次,那么将来被访问的概率会更高”。LRU-K与LRU区别是,LRU-K多了一个数据访问历史记录队列(需要注意的是,访问历史记录队列并不是缓存队列,所以是不保存数据本身的,只是保存对数据的访问记录,数据此时依旧在原始存储中),队列中维护着数据被访问的次数以及时间戳,只有当这个数据被访问的次数大于等于K值时,才会从历史记录队列中删除,然后把数据加入到缓存队列中去。步骤:数据第一次被访问时,加入到历史访问记录队列中,访问次数为1,初始化访问时间戳;如果数据访问次数没有达到K次,则访问次数+1,更新时间戳。当队列满了时,按照某种规则(LRU或者FIFO)将历史记录淘汰。为了避免历史数据污染未来数据的问题,还需要加上一个有效期限,对超过有效期的访问记录,进行重新计数。(可以使用懒处理,即每次对访问记录做处理时,先将记录中的访问时间与当前时间进行对比,如果时间间隔超过预设的值,则访问次数重置为1并更新时间戳,表示重新开始计数)当数据访问计数大于等于K次后,将数据从历史访问队列中删除,更新数据时间戳,保存到缓存队列头部中(缓存队列时间戳递减排序,越到尾部距离当前时间越长);缓存队列中数据被再次访问后,将其移到头部,并更新时间戳;缓存队列需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。分析:LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,一旦访问模式发生变化,需要大量的新数据访问才能将历史热点访问记录清除掉。复杂度与代价:LRU-K队列是一个优先级队列。由于LRU-K需要记录那些被访问过,但还没有放入缓存的对象,导致内存消耗会很多。URL-Two queues算法URL-Two queues算法类似于LRU-2,不同点在于URL-Two queues将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:URL-Two queues算法有两个缓存队列,一个是FIFO队列(First in First out,先进先出),一个是LRU队列。当数据第一次访问时,URL-Two queues算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。步骤:新访问的数据先插入到FIFO队列中;如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;如果数据在FIFO队列中被再次访问,则将数据从FIFO删除,加入到LRU队列头部;如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;LRU队列淘汰末尾的数据。分析:URL-Two queues算法和LRU-2算法命中率类似,但是URL-Two queues会减少一次从原始存储读取或计算数据的操作。命中率要高于LRU。复杂度与代价:需要维护两个队列,代价是FIFO和LRU代价之和。五三LRU算法emmmm…这个名字其实是我取的,大概是这种算法还没有被命名?当然,这是一个玩笑话。我是在mysql底层实现里发现这个算法的,mysql在处理缓存淘汰时是用的这个方法,有点像URL-Two queues的变体,只是我们只需要维护一个队列,然后将队列按照5:3的比例进行分割,5的那部分叫做young区,3的那部分叫做old区。具体是怎么样的请先看我把图画出来:步骤:第一次访问的数据从队列的3/8处位置插入;如果数据再次被访问,则移动到队列头部;如果数据没有被再访问,会逐步被热点数据驱逐向下移;淘汰尾部数据。分析:五三LRU算法算作是URL-Two queues算法的变种,原理其实是一样的,只是把两个队列合二为一个队列进行数据的处理,所以命中率和URL-Two queues算法一样。复杂度与代价:维护一个队列,代价较低,但是内存占用率和URL-Two queues一样。Multi Queue算法Multi Queue算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是“优先缓存访问次数多的数据”。Multi Queue算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级。访问优先级是根据访问次数计算出来的,例如:Q0,Q1….Qn代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数。步骤:新插入的数据放入Q0;每个队列按照LRU管理数据,再次访问的数据移动到头部;当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部;Q-history按照LRU淘汰数据的索引。分析:Multi Queue降低了“缓存污染”带来的问题,命中率比LRU要高。复杂度与代价:Multi Queue需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。Multi Queue需要记录每个数据的访问时间,需要定时扫描所有队列,代价比LRU要高。虽然Multi Queue的队列看起来数量比较多,但由于所有队列之和受限于缓存容量的大小,因此这里多个队列长度之和和一个LRU队列是一样的,因此队列扫描性能也相近。说在后面话还有哪些优秀的缓存淘汰算法,或者你有更好的想法或问题,欢迎留言给我!

April 10, 2019 · 1 min · jiezi

深入剖析浏览器缓存策略

前言在访问一个网页时,客户端会从服务器下载所需的资源。但是有些资源很少发生变动,例如 HTML、JS、CSS、图片、字体文件等。如果每次加载页面都从源服务器下载这些资源,不仅会增加获取资源的时间,也会给服务器带来一定压力。因此,重用已获取的资源十分重要。将请求的资源缓存下来,下次请求同一资源时,直接使用存储的副本,而不会再去源服务器下载。这就是我们常说的缓存技术。缓存的种类很多:浏览器缓存、网关缓存、CDN 缓存、代理服务器缓存等。这些缓存大致可以归为两类:共享缓存和私有缓存。共享缓存能够被多个用户使用,而私有缓存只能用于单个用户。浏览器缓存只存在于每个单独的客户端,因此它是私有缓存。本文主要介绍私有(浏览器)缓存。你将学习:浏览器缓存的分类如何启用和禁止缓存缓存存储的位置如何设置缓存的过期时间缓存过期之后会发生什么如何为自己的应用制定合适的缓存策略调试如何判断网站是否启用了缓存如何禁用浏览器缓存缓存存储启用缓存Cache-Control浏览器会根据 HTTP Response Headers 中的一些字段来决定是否要缓存该资源。通过设置 Response Headers 中的 Cache-Control 和 Expires 可以启用缓存,这样资源就会被缓存到客户端。Cache-Control 可以设置 private 、public 、max-age 、 no-cache 来启用缓存。Cache-Control: private/publicCache-Control: max-age=300Cache-Control: no-cacheprivate :表示该资源只能被浏览器缓存。public :表示该资源既能被浏览器缓存,也能被任何中间人(比如代理服务器、CDN 等)缓存。max-age :表示该资源能够被缓存的最大时间。如果设置 max-age=0 ,该资源仍然会被浏览器缓存,只不过立刻就过期了。no-cache :该资源会被缓存,但是立刻就过期了,因此需要先和服务器确认资源是否发生变化,只有当资源没有变化时,该缓存才会被使用,否则需要从服务器下载。相当于 max-age=0 。ExpiresExpires 标识了缓存的具体过期时间,来控制资源何时过期。通过设置 Expires 可以启用缓存。不过需要注意 Expires 的值是格林威治时间(Greenwich Mean Time, GMT),不是本地时间。Expires: Fri, 08 Mar 2029 08:05:59 GMTExpires: 0 // Expires: 0 仍然会启用缓存,只不过缓存立刻过期。优先级既然 Cache-Control 和 Expires 都能够启用缓存,那么问题来了,如果同时设置 Cache-Control: max-age=600 和 Expires: 0 ,那么浏览器应该如何缓存该资源呢?答案是只有 Cache-Control: max-age=600 生效。因为 Cache-Control 的优先级高于 Expires,如果同时设置了 Cache-Control 和 Expires,以 Cache-Control 为准。浏览器的默认行为设置 Cache-Control 之后,可以看到浏览器确实启用了缓存(from disk cache)。如下所示:Cache-Control:max-age=604800, must-revalidate, public但是我发现,即使 Response Header 中没有设置 Cache-Control 和 Expires,浏览器仍然会缓存某些资源。这是为什么呢?原来当 Response Header 中有 Last-Modified 但是没有 Cache-Control 和 Expires 时,浏览器会用一套自己的算法来决定这个资源会被缓存多长时间。这是浏览器为了提升性能进行的优化,每个浏览器的行为可能不一致,有些浏览器上甚至没有这样的优化。因此,如果要启用缓存,还是应该自己设置合适的 Cache-Control 和 Expires,不要依赖浏览器自身的缓存算法。当然,如果在调试时发现本应该更新的文件没有更新,也别忘了看看是否被浏览器缓存了。禁止缓存给 Cache-Control 设置 no-store 会禁止浏览器和中间人缓存该资源。在处理包含个人隐私数据或银行业务数据的资源时很有用。Cache-Control: no-store缓存目标对象一般来说,浏览器缓存只能存储 GET 响应,例如 HTML、JS、CSS、图片等静态资源。因为这些资源不经常发生变化,所以缓存可以帮助提升获取资源的速度。但是像一些 POST/DELETE 请求,这些请求基本上每一次都不一样,因此也没有什么缓存的价值。缓存位置浏览器可以在内存、硬盘中开辟一个空间用以保存请求资源的副本。我们经常在 Dev Tools 里面看到 Memory Cache(内存缓存)和 Disk Cache(硬盘缓存),指的就是缓存所在的位置。请求一个资源时,会按照优先级(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找缓存,如果命中则使用缓存,否则发起网络请求。这里只介绍常用的 Memory Cache 和 Disk Cache。200 from Memory Cache表示不访问服务器,直接从内存中读取缓存。因为缓存的资源保存在内存中,所以读取速度较快,但是关闭进程之后,缓存的资源也会随之销毁。一般来说,系统不会给内存分配较大的容量,因此内存缓存一般用于存储小文件。同时,内存缓存在有时效性要求的场景下也很有用(比如浏览器的隐私模式)。200 from Disk Cache表示不访问服务器,直接从硬盘中读取缓存。与内存相比,硬盘的读取速度较慢,但是硬盘缓存持续的时间更长,关闭进程之后,缓存的资源仍然存在。由于硬盘的容量较大,因此一般用于存储大文件。总的来说就是:内存缓存:读取快、持续时间短、容量小硬盘缓存:读取慢、持续时间长、容量大缓存分类浏览器缓存一般分为两类:强缓存(也称本地缓存)和协商缓存(也称弱缓存)。判定过程如下:浏览器发送请求前,会先去缓存里面查看是否命中强缓存,如果命中,则直接从缓存中读取资源,不会发送请求到服务器。否则,进入下一步。当强缓存没有命中时,浏览器一定会向服务器发起请求。服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。如果命中,服务器会返回响应,但是不会携带任何响应实体,只是告诉浏览器可以直接从缓存中获取这个资源。否则,进入下一步。如果前两步都没有命中,则直接从服务器加载资源。强缓存和协商缓存的共同点在于,如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源。而不同点在于,强缓存不发送请求到服务器,而协商缓存会发送请求到服务器以验证资源是否过期。普通刷新会启用协商缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存。缓存过期策略当缓存过期之后,浏览器会向服务器发起 HTTP 请求,以确定资源是否发生了变化。如果资源未改变,那么浏览器会继续使用本地的缓存资源;如果该资源已经发生变化了,那么浏览器会删除旧的缓存资源,并将新的资源缓存到本地。过期时间Http Response Header 里面的 Cache-Control: max-age=xxx 和 Expires 都可以设置缓存的过期时间,但是它们有一些区别:Expires :标识该资源过期的时间点,它是一个绝对值,即在这个时间点之后,缓存的资源过期。max-age :标识该资源能够被缓存的最大的时间。它是一个相对值,相对于第一次请求该文档时服务器记录的「请求发起时间」。虽然 Cache-Control 是 HTTP 1.1 提出来的新特性,但并不是说 max-age 优于 Expires。它们都有各自的使用场景,我们应该根据业务需求去决定使用哪一个。比如当某个资源需要在特定的时间点过期时应该使用 Expires 。如果只是为了开启缓存,使用 max-age 可能会更好些,因为 Cache-Control 的优先级高于 Expires。针对应用中几乎不会改变的文件,通常可以设置一个较长的过期时间,以保证缓存的有效。例如图片、CSS、JS 等静态资源。缓存验证上一小节已经提到,当浏览器请求一个资源时,如果发现缓存中有该资源,但是已经过期了,那么浏览器就会向服务器发起 HTTP 请求,以验证缓存的资源是否发生变化。缓存验证时机什么时候会进行缓存验证?刷新页面。一般来说,为了确保用户获取到最新的数据,在刷新页面时大部分浏览器都不会再使用缓存中的数据,而是发起一个请求去服务器验证。Response Header 中设置了 Cache-control: must-revalidate。当缓存的资源过期之后,必须到源服务器去验证,只有确认该资源没有过期,才能继续使用缓存。缓存验证器服务器是怎么判断资源改变与否的呢?服务端在返回响应内容的同时,还会在 Response Header 中设置一些验证标识,当缓存的资源过期之后,浏览器就会携带验证标识向服务器发起请求,服务器通过对比这些标识,就能知道缓存的资源是否发生了改变。Header 中的验证标识字段主要有两组:Etag 和 If-None-Match 、Last-Modified 和 If-Modified-Since 。其中,形如 If-xxx 这样的请求首部字段,可以称之为条件请求。比如只在满足某个条件的情况下返回或上传文件,这样可以节省带宽。 Last-ModifiedLast-Modified 就是一个验证器。服务器在将资源返回给客户端的同时,会将资源的最后修改时间 Last-Modified 加在 Response Header 中一起返回。浏览器会为资源标记上该信息,当缓存过期之后,浏览器会把该信息设置到 Request Header 中的 If-Modified-Since 中向服务器发起请求。如果 If-Modified-Since 中的值和服务器上该资源最终的修改时间一致,就说明该资源没有被修改过,服务器会直接返回 304 状态码,无响应实体,这样就可以节省传输的数据量。如果不一致,服务器会返回 200 状态码,同时和第一次 HTTP 请求一样,返回响应实体和验证器。Last-Modified:Fri, 04 Jan 2019 14:00:21 GMTEtag服务器会通过某种算法,为资源计算出一个唯一标识符,在把响应返回给客户端的时候,会在 Response Header 中加上 Etag: 唯一标识符 一起返回给客户端。Etag:“952d03d8561454120b550f0a5679a172c4822ce8"客户端会将 Etag 保存下来,后续请求时会将 Etag 作为 Request Header 中 If-None-Match 的值发给服务器。通过比对客户端发过来的 Etag 和服务器上保存的 Etag 是否一致,就能够知道资源是否发生了变化。如果资源没有发生变化,返回 304,客户端继续使用缓存。如果资源已经修改,则返回 200。制定缓存策略缓存真的可以说让我们又爱又恨。在开发时,我们经常遇到这样的问题:明明已经修改了这个文件,为什么没有生效?好吧,文件被缓存了。。。但是在上线时,我们又希望文件尽可能地被浏览器缓存,来提高性能。因此为自己的应用制定合适的缓存策略非常重要。为静态资源设置较长缓存时间。有些资源很长时间都不会改变,比如一些三方库,图片,字体文件等。可以为它们设置一个很长的过期时间,例如设定「一年」。通过给文件名的唯一标识来确保文件修改生效。有些时候为了解决 bug,我们可能会修改一些文件,比如应用的 CSS、JS 等。如果这些文件已经被缓存,那么除非用户强制刷新页面,否则用户只有在缓存过期之后才有可能获取新的文件。如何让浏览器不使用缓存,而是重新下载新的文件呢?有一个办法就是给文件名加上唯一标识,比如 Hash 或版本信息。当文件修改之后,这个唯一标识也会随之改变。浏览器发现文件改变之后,就不会使用缓存了。明确是否有资源不能被缓存。比如一些敏感数据,如果不应该被浏览器缓存,需要在 Response Header 中设置 Cache-Control: no-store。调试如何知道请求的资源是否被缓存了?打开 Chrome 的开发者工具,我们可以看到 Size 这一栏下面,如果显示文件真实的大小,则说明该文件未被缓存。如果显示 from xxx cache,则说明该请求使用的是已被缓存的文件。如下:调试时,如果想禁用浏览器缓存,可以在开发者工具上勾选 Disabel cache。最后最后,大家可以通过下面这张图再回顾一下我们刚刚讲过的内容。参考HTTP 缓存奇怪的缓存策略 ...

April 9, 2019 · 2 min · jiezi

浏览器缓存策略

为了提高站点的访问速度,使用缓存来优化。缓存主要分为 强缓存和协商缓存。协商缓存主要分为last-modified、etag。下面我主要通过代码修改来表现各个缓存之间的区别。先讨论协商缓存。last-modified表示文件的修改日期,如果文件做了修改那就应该重新获取文件。last-modified是文件修改后根据服务器的时间生成。如果我们修改了文件则会重新获取,status就为200再次刷新就会返回304表示缓存已经是最新不需要再更新。请求中会询问相关文件修改时间(If-Modified-Since)请求响应ETag:是一个可以与Web资源关联的记号(token)如果文件被替换,就会生成唯一的etag。替换前的文件替换后的文件PS: 如果是使用了多台服务器做负载均衡的话,会出现etag不一致问题。Apache 的默认ETag的值总是由文件的索引节点(Inode)、大小(Size)、最后修改时间(MTime)决定,我们只需要去掉Inode即可强缓存强缓存相比协商缓存更为彻底,在协商缓存下浏览器不会对服务器发起请求。强缓存:主要分为expires和cache-controlExpires: 表示存在时间,允许客户端在这个时间之前不去检查(发请求),等同max-age的 效果。但是如果同时存在,则被Cache-Control的max-age覆盖。 格式: Expires :时间,后面跟一个时间或者日期,超过这个时间后缓存失效。也就是浏览器发出请求之前,会检查这个时间是否失效,若失效,则浏览器会重新发出请求。开启apache expires_mod之后,浏览器在第一次将资源请求之后会缓存。Cache-ControlCache-Control 在 HTTP 响应头中,用于指示代理和 UA 使用何种缓存策略。比如:no-cache 为本次响应不可直接用于后续请求(在没有向服务器进行校验的情况下)no-store 为禁止缓存(不得存储到非易失性介质,如果有的话尽量移除,用于敏感信息)public为大家都可以缓存。private为仅 UA 可缓存cache-control中设置max-age 为最长的缓存时间。在该时间内则使用缓存。设置为no-cache之后则不会再进行缓存。题外话在使用apache对浏览器缓存进行测试过程中发现。在不设置 cache-control的情况下,浏览器会根据自身的情况去取舍相关的缓存,可以从这查看。如果大家在服务器配置过程中发现,自己没有配置任何的缓存信息但是浏览器却缓存了资源就不用惊讶。

April 6, 2019 · 1 min · jiezi

缓存的三个问题

缓存的作用是在内存中临时存储来自外部系统(如数据库)的数据,以便让请求更快的得到响应。如果请求数据在缓存中不存在,或者已经超时失效,那么也要从外部系统查询,然后放入缓存中,这个过程叫刷新缓存。这是缓存的基本使用逻辑,但是实际当中可能出现三种异常情况,它们会导致缓存起不到预期的使用效果,以至于系统性能明显下降。缓存命中率过低缓存命中率指的是从缓存中找到数据的请求占所有请求的比重。例如 100 个请求当中有 90 个请求的结果可以直接从缓存中获得,那么命中率就是 90%。剩下 10% 的请求就要从外部系统查询数据,填入缓存,然后再返回。什么情况下缓存命中率高呢?请求的数据比较集中的时候,例如 80% 的请求集中在 20% 的数据上,这部分数据也被称作热点之类的。热点越热,缓存命中率越高。因此之所以出现缓存命中率过低,自然就是因为热点不够热,请求的数据非常分散。命中率过低的后果就是很多请求的数据仍需从外部系统查询,假如是数据库的话,数据库的压力就会非常大,同时系统的响应也明显变慢。有时候系统正常情况下是存在热点数据的,但突然有一天出现大量的分散请求,导致缓存命中率直线下降。这些异常的请求可以看作是有意的攻击行为,目的就是让系统无法响应。要缓解缓存命中率过低的问题,最直接的办法当然是加大缓存。本地缓存不够,就用分布式缓存,多台机器分开存储。而遇到攻击行为的话,加大缓存可能是徒劳的,这时候需要去识别请求,对于被归类为攻击的请求主动延长响应时间,甚至拒绝返回结果。比如说一个论坛,突然遇到大量请求,均匀的访问五年内的帖子内容,导致数据库负载很大,此时可以将访问老帖子的请求(帖子ID通常是递增的,ID越小表示发帖时间越久)返回时间适当延长,比如延长到五分钟。不过使用这种做法时千万不要简单的暂停线程,这会导致没有多余的线程来处理正常的请求。大量缓存项同时刷新缓存通常都是存在失效时间的,需要避免的一种情况就是大量缓存项在同一个时间点失效,如果此时对这些数据的请求量大,那么这些请求就会同时去刷新各自的缓存,这就将压力传递到了外部系统上。避免这种情况的办法就是在预定的失效时间基础上加上一个随机值,以错开缓存项的失效时间。大量请求刷新同一个缓存项一个请求遇到缓存失效,于是去刷新缓存,而在这个过程中又有大量请求来访问正在刷新的缓存项,导致该缓存项完成本次刷新后,又立刻被另一个线程刷新,实质上每个请求都因为缓存未命中而去访问了外部系统。出现这个现象的原因是设计上的不合理。当一个缓存正在刷新时,访问该缓存项的其他线程应该等待刷新完毕,这样它们就可以直接从缓存获得结果了。线程同步当然是用锁。如果是分布式系统,那就用分布式锁。

April 2, 2019 · 1 min · jiezi

浏览器缓存看这一篇就够了

浏览器缓存作为性能优化的重要一环,对于前端而言,重要性不言而喻。以前总是一知半解的,所以这次好好整理总结了一下。1、缓存机制首先我们来总体感知一下它的匹配流程,如下:浏览器发送请求前,根据请求头的expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。如果前两步都没有命中,则直接从服务端获取资源。2、强缓存强缓存:不会向服务器发送请求,直接从缓存中读取资源。2.1 强缓存原理强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:第一次请求,不存在缓存结果和缓存标识,直接向服务器发送请求存在缓存标识和缓存结果,但是已经失效,强制缓存是啊比,则使用协商缓存(暂不分析)存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果那么强制缓存的缓存规则是什么?当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高。2.1.1、 Expires缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。2.1.2、 Cache-Control在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:public:所有内容都将被缓存(客户端和代理服务器都可缓存)private:所有内容只有客户端可以缓存,Cache-Control的默认取值no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致,也就是协商缓存。而no-store才表示不会被缓存,即不使用强制缓存,也不使用协商缓存2.1.3、设置强缓存需要服务端设置expires和cache-control。nginx代码参考,设置了一年的缓存时间:location ~ ..(ico|svg|ttf|eot|woff)(.) { proxy_cache pnc; proxy_cache_valid 200 304 1y; proxy_cache_valid any 1m; proxy_cache_lock on; proxy_cache_lock_timeout 5s; proxy_cache_use_stale updating error timeout invalid_header http_500 http_502; expires 1y;}浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?这就是下面我们要讲到的from disk cache和from memory cache。2.2、from disk cache和from memory cache细心地同学在开发的时候应该注意到了Chrome的网络请求的Size会出现三种情况from disk cache(磁盘缓存)、from memory cache(内存缓存)、以及资源大小数值。状态类型说明200form memory cache不请求网络资源,资源在内存当中,一般脚本、字体、图片会存在内存当中200form disk ceche不请求网络资源,在磁盘当中,一般非脚本会存在内存当中,如css等200资源大小数值从服务器下载最新资源304报文大小请求服务端发现资源没有更新,使用本地资源浏览器读取缓存的顺序为memory –> disk。以访问https://github.com/xiangxingchen/blog为例我们第一次访问时https://github.com/xiangxingchen/blog关闭标签页,再此打开https://github.com/xiangxingchen/blog时F5刷新时简单的对比一下3、协商缓存协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:协商缓存生效,返回304和Not Modified协商缓存失效,返回200和请求结果3.1、Last-Modified和If-Modified-Since浏览器首先发送一个请求,让服务端在response header中返回请求的资源上次更新时间,就是last-modified,浏览器会缓存下这个时间。然后浏览器再下次请求中,request header中带上if-modified-since:[保存的last-modified的值]。根据浏览器发送的修改时间和服务端的修改时间进行比对,一致的话代表资源没有改变,服务端返回正文为空的响应,让浏览器中缓存中读取资源,这就大大减小了请求的消耗。由于last-modified依赖的是保存的绝对时间,还是会出现误差的情况:保存的时间是以秒为单位的,1秒内多次修改是无法捕捉到的;各机器读取到的时间不一致,就有出现误差的可能性。为了改善这个问题,提出了使用etag。3.2、ETag和If-None-Matchetag是http协议提供的若干机制中的一种Web缓存验证机制,并且允许客户端进行缓存协商。生成etag常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。 和last-modified一样.浏览器会先发送一个请求得到etag的值,然后再下一次请求在request header中带上if-none-match:[保存的etag的值]。通过发送的etag的值和服务端重新生成的etag的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。etag能够解决last-modified的一些缺点,但是etag每次服务端生成都需要进行读写操作,而last-modified只需要读取操作,从这方面来看,etag的消耗是更大的。二者对比精确度上:Etag要优于Last-Modified。优先级上:服务器校验优先考虑Etag。性能上:Etag要逊于Last-Modified4、用户行为对浏览器缓存的影响打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control:no-cache(为了兼容,还带了 Pragma:no-cache),服务器直接返回 200 和最新内容。5、总结 ...

March 31, 2019 · 1 min · jiezi

MySQL 执行过程与查询缓存

MySQL执行一个查询过程:当我们向MySQL发送一个请求的时候,MySQL到底做了什么:1.客户端发送一条查询给服务器2.服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。3.服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。4.MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询5.将结果返回给客户端。mysql 主要是由 server 层和存储层两部分构成的。server 层主要包括连接器、查询缓存,分析器、优化器、执行器。存储层主要是用来存储和查询数据的,常用的存储引擎有 InnoDB、MyISAM,(1) MySQL客户端/服务器通信协议MySQL客户端和服务器之的通信协议是“半双工”的,这就意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。所以我们无法也无须将一个消息切成小块独立来发送。优缺点:这种协议让MySQL通信简单快速,但是也从很多地方限制了 MySQL。一个明显的限制是,这意味着没法进行流量控制。一旦一端开始发送消息,另一端要接收完整个消息才能响应它。这就像采回抛球的游戏:在任何时刻,只有一个人能控制球,而且只有控制球的人才能将球抛回去(发送消息)。(2).连接器 MySQL客户端和服务端建立连接,获取当前连接用户的权限(3)查询缓存在解析一个查询语句之前,如果查询缓存是打开的,MySQL会检查这个缓存,是否命中查询缓存中的数据。这个检查是通过一个大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,那也不会匹配缓存结果,这种情况下查询就会进入下一阶段的处理。如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前 MySQL会检查一次用户权限。这仍然是无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题, MySQL会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行.ps:注意在 mysql8 后已经没有查询缓存这个功能了,因为这个缓存非常容易被清空掉,命中率比较低。(3).分析器既然没有查到缓存,就需要开始执行 sql 语句了,在执行之前肯定需要先对 sql 语句进行解析。分析器主要对 sql 语句进行语法和语义分析,检查单词是否拼写错误,还有检查要查询的表或字段是否存在(4)查询优化查询的生命周期的下一步是将一个SQL转换成一个执行计划, MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQ执行计划。这个过程中任何错误(例如语法错误)都可能终止查询。2.关于查询缓存(1)MySQL 判断缓存命中的方法很简单:缓存存放在一个引用表中,通过一个哈希值引用。MySOL查询缓存保存查询返回的完整结果。当查询命中该缓存, MySQL会立刻返回结果跳过了 解析,优化和执行阶段查询缓存系统会跟踪查迫中涉及的每个表,如果这些表发生变化,那么和这个表相关的的存数据都将失效。这种机制效率看起来比较低,因为数据表变化时很有可能对查询结果并没有变更,但是这种简单实现代价很小,而这点对于一个非常繁忙的系统来说非常重要。查询缓存系统对应用程序是完全透明的。应用程序无须关心 MySQL是通过查询缓存返回的结果还是实际执行返回的结果。事实上,这两种方式执行的结果是完全相同的。换句话说,查询缓存无须使用任何语法。无论是 MYSQL开启成关闭查询缓在,对应用程序都是透明的。(2)判断缓存命中当判断缓存是否命中时, MySQL不会解析、“正规化”或者参数化查询语句,而是直接使用SQL语句和客户端发送过来的其他原始信息,在字符上不同,例如空格、注释,在何的不同,都会导致缓存的不中。当查询语句中有一些不确定的数据时,则不会被缓存,例如包含函数NOW()或者 CURRENT_DATE()的查询不会被缓存.误区:我们常听到:“如果查询中包含一个不确定的函数, MySQL则不会检查查询缓存”。这个说法是不正确的。因为在检查查询缓存的时候,还没有解析SQL语句,所以MySQL并不知道查询语句中是否包含这类函数。在检查查询缓存之前, MySQL只做一件事情,就是通过一个大小写不敏感的检查看看SQL语句是不是以5EL开头。准确的说法应该是:“如果查询语句中包含任何的不确定函数,那么在查询缓存中是不可能找到缓存结果的”。注意点:MySQL的查询缓存在很多时候可以提升查询性能,在使用的时候,有一些问题需要特别注意。首先,打开查询缓存对读和写操作都会带来额外的消耗:1.读查询在开始之前必须先检查是否命中缓存2.如果这个读查询可以被缓存,那么当完成执行后, MySQL若发现查询缓存中没有这个查询,会将其结果存入查询缓存,这会带来额外的系统消耗。3.这对写操作也会有影响,因为当向某个表写入数据的时候, MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片很多,这个操作就可能会带来大系统消耗(设置了很多的内存给查询缓存用的时候)如果查询缓存使用了很大量的内存,缓存失效操作就可能成为一个非常严重的问题瓶颈如果缓存中存放了大量的查询结果,那么缓存失效操作时整个系统都可能会僵死一会因为这个操作是靠一个全局锁操作保护的,所有需要做该操作的查询都要等待这个锁,而且无论是检测是否命中缓存、还是缓存失效检测都需要等待这个全局锁。(3)什么情况下查询缓存能发挥作用理论上,可以通过观察打开或者关闭查询缓存时候的系统效率来决定是否需要开启查询。对手那些需要消耗大量资源的查询通常都是非常适合缓存的。例如一些汇总计算查询具体的如 COUNT()等。总地来说,对于复杂的 SELECT语句都可以使用查询缓存,例如多表JOIN后还需要做排序和分页,这类查询每次执行消耗都很大,但是返回的结果集却很小,非常适合查询缓存。不过需要注意的是,涉及的表上 UPDATE、 DELETE和 INSERT操作相比 SELECT来说要非常少才行。判断查询缓存是否有效的直接数据是命中率。就是使用查询缓存返回结果占总查询的比率不过缓存中率是一个很难判断的数值。命中率多大才是好的命中率。具体情况,具体分析。只要查询缓存带来的效率提升大于查询缓存带来的额外消耗,即使30%命中率对系统性能提升也有很大好处。另外,缓存了哪些查询也很重要,例如,被缓存的查询本身消耗非常巨大,那么即使缓存命中率非常低,也仍然会对系统性能提升有好处缓存未命中可能有如下几种原因:1.查询语句无法被缓存,可能是因为查询中包含一个不确定的函数(如 CURREN_DATE)或者查询结果太大而无法缓存。这都会导致状态值 Cache not cached增加。2.MySQL从未处理这个查询,所以结果也从不曾被缓存过。3.还有一种情况是虽然之前缓存了查询结果,但是由于查询缓存的内存用完了,MySQL需要将某些缓存“逐出”,或者由于数据表被修改导致缓存失效。如果你的服务器上有大量缓存未命中,但是实际上绝大数查询都被缓存了,那么一定是有如下情况发生:1.查询缓存还没有完成预热。也就是说, MySQL还没有机会将查询结果都缓存起来。2.查询语句之前从未执行过。如果你的应用程序不会重复执行一条查询语句,那么即使完成预热仍然会有很多缓存未命中3.缓存失效操作太多了。(4)如何配置 和维护查询缓存query_cache_type是否打开查询缓存。可以设置成0FN或 DEMAND。 DEMAND表示只有在查询语句中明确写明SQL_ CACHE的语句才放入查询缓存。这个变量可以是会话级别的也可以是全局级别的query_cache_size查询缓存使用的总内存空间,单位是字节。这个值必须是1024的整数倍,否则 MySQL实际分配的数据会和你指定的略有不同。query_cahce_min_res_unit在查询缓存中分配内存块时的最小单位。query_chache_limitMySQL能够缓存的最大査询结果。如果查询结果大于这个值,则不会被缓存。因为査询缓存在数据生成的时候就开始尝试缓存数据,所以只有当结果全部返回后,才知道查询结果是否超出限制如果超出, MySQL则增加状态值 Cache_not_cached,并将结果从查询缓存中删除如果你事先知道有很多这样的情况发生,那么建议在查询语句中加入(5)替代方案MySQL查询缓存工作的原则是:执行查询最快的方式就是不去执行,但是查询仍然需要发送到服务器端,服务器也还需要做一点点工作。如果对于某些查询完全不需要与服务器通信效果会如何呢?这时客户端的缓存可以很大程度上帮你分担 MySQL服务器的压力总结:完全相同的查询在重复执行的时候,查询缓存可以立即返回结果,而无须在数据库中重新执行一次。根据我们的经验,在高并发压力环境中在询缓存会导致系统性能的下降,甚至僵死。如果一定要使用查询缓存,那么不要设置太大内存,而且只有在确收益的时候才使用。那该如何判断是否应该使用查询缓存呢?建议使Percona server.,观察更细致的日志,并做一些简单的计算。还可以查看缓存命中率(并不总是有用)、“ NSERTS和 SELECT比率”(这个参数也并不直观)、或者“命中和写入比率”(这个参考意义较大)。查询缓存是一个非常方便的缓存,对应用程序完全透明,无须任何额外的编码,但是、如果希望有更高的缓存效率,我们建议使cache 或者其他类似的解决方案。ps:文章参考《高性能MySQL》一书

March 30, 2019 · 1 min · jiezi

浅谈移动端 View 的显示过程

作者:个推安卓开发工程师 一七随着科技的发展,各种移动端早已成为人们日常生活中不可或缺的部分,人们使用移动端产品工作、社交、娱乐……移动端界面的流畅性已经成为影响用户体验的重要因素之一。那么你是否思考过移动端所展现的流畅画面是如何实现的呢?本文通过对移动端View显示过程的简略分析,帮助开发者了解View渲染的逻辑,更好地优化自己的APP。上图展示的是一个完整的页面渲染过程。通过上图,我们可以初步了解每一帧页面从代码布局的编写到展示给使用者,其背后的逻辑是如何一步一步执行的。屏幕如何呈像像素点在电子屏幕中显示的图片,其实都是由一个个“小点”所组成的,这些“小点”被称为“像素点”。每一个像素点都有自己的颜色,每一张完整的图片都是由它们相连拼接形成的。每个像素点一般都有 3 个子像素:红、绿、蓝,根据这三种原色,我们能够调制出各种各样的颜色。大电视机与现在的平板电视不同的是,以前的黑白电视机或者大背投彩电,总是带着大大的“后背”。“大后背”电视其实就是阴极射线管电视机,俗称显像管电视。其成像原理是电子枪发射出的电子束(阴极射线)通过聚焦系统和偏转系统,射向屏幕上涂有荧光层的指定位置。被电子束轰击的每个位置,荧光层都会产生一个小亮点,最终小亮点们将会组成一幅幅影像,显示在电视屏幕上。这也是以前大电视机的屏幕都呈圆弧形的原因。因为越接近圆形,边长到中心的距离越相近,呈像越均匀。那为什么当磁铁贴近电视机时,会让电视机的成像出现问题呢?那是因为磁铁会干扰电子束的正常轨迹,并且在贴近屏幕的时候,也可能使得屏幕的荧光层磁化,出现一个个不正常的光斑。下图展示的是摄像机慢放后,电子束的绘制过程。LCD 和 OLED随着科技的不断进步,电视、手机、电脑的体积越来越薄,射线管显像方式也逐渐被淘汰。目前在手机市场上占据主流地位的是 LCD 和 OLED 两种屏幕。LCD 全称为 Liquid Crystal Display ,即液晶显示器。OLED 全称为 Organic Light-Emitting Diode ,即有机发光二极管。这两者之间存在显著的差别:1. 两者成像原理不同LCD 是靠白色的背光穿透彩色薄膜显色的,而 OLED 则是靠每个像素点自行发光。2. 在耗电量方面LCD的耗电量较高,即使只显示一个亮点,LCD 的背光源也需要一直发光,而且容易出现漏光现象。而OLED的每个像素都能独立工作,而且 可以自行发光,因此采用OLED的设备可以制作得更薄,甚至可以弯曲。3.在制作方面 LCD使用的是无机材料, OLED 则需要使用有机材料,因此 OLED的制作费用更高,并且使用寿命不如 LCD 。图形显示核心 GPU与CPU相对比,GPU的计算单元更多,更擅长大规模并发计算,例如密码破解、图像处理等。CPU 则是遵循冯诺依曼架构存储程序顺序执行,在大规模并行计算能力上,受到的限制更大,因此更擅长逻辑控制。应用程序编程接口 API (OpenGL)在没有统一的 API 之前,开发者需要在各式各样的图形硬件上编写各种自定义接口和驱动程序,工作量极大。1990 年 SGI(硅谷图形公司)成为了工作站 3D 图形领域的领导者,并将其 API 转变为一项开放标准,即 OpenGL。后来,SGI还促成了 OpenGL 架构审查委员会(OpenGL ARB)的创建。垂直同步 Vertical Synchronization当我们在使用手机 APP 的过程中,发现页面出现卡顿现象,那么极有可能是页面没有在 16ms 内更新导致的。实际上,人眼与大脑之间的协作无法感知超过 60fps 的画面更新。60fps 相当于是每秒 60 帧,那么每个页面需要在 1000/60 = 16ms 内更新为其他页面,才不会让我们感受到页面的卡顿。而在没有 VSync 的情况下可能会出现以下情况:如上图所示,在没有 VSync 的情况下,会出现需要显示第二帧时,其尚未处理完成的情况,因此Display 中显示的仍是第一帧。这会造成该帧显示时长超过16ms,从而导致页面卡顿的现象。为了使 CPU、GPU 生成帧的速度与 Display 保持一致,Android 系统每 16ms 就会发出一次 VSYNC 信号,触发 UI 渲染更新。从上图中我们可以看出,每隔 16ms ,安卓会发出一个 VSync 信号,收到信号后 CPU 开始处理下一帧的的内容,GPU 在 CPU 处理结束之后,将会进行光栅化,此时屏幕上显示的是上一帧已经处理完成的页面。如此反复,就可以在页面中展示一幅幅的指定画面。而确保画面流畅的前提是CPU 和 GPU 处理一帧所花费的时间不能超过 16 ms,否则就会出现以下情况:当CPU 和 GPU 处理一帧的时间超过了16 ms时,在第一个 Display 中,由于 GPU 处理 B 画面的时间过长,导致系统发出 VSync 信号时, Display不能及时地显示出 B 画面,而重复显示A页面,造成卡顿。此外,在第二个 Display 中,由于 A Buffer 还在被 Display 所使用,不能在收到 VSync 信号后开始处理下一帧的页面,导致该时间段内 CPU 的闲置。为了避免这种时间的浪费,三缓存机制由此出现:如上图所示,在三缓存机制中,当 A 缓存被 Display 使用、B 缓存被 GPU 处理时,系统会发出 Vsync 信号,并加入新的缓存 C ,用来缓存下一帧的内容。这种方式虽然不能完全避免 A页面的重复显示,但是能够让后面页面的显示更加平滑。View 的绘制流程View 的绘制是从 ViewRootImpl 的 performTraversals() 方法开始的,其整体流程大致分为三步,如下图所示:measure控件测量过程从 performMeasure() 方法开始。在该方法中childWidthMeasureSpec 和 childHeightMeasureSpec,分别是用来确定宽度和高度的。MeasureSpec 是一个 int 值,它存储着两个信息:低 30 位是 View 的 specSize,高 2 位是 View 的 specMode。specMode 有三种类型:1.UNSPECIFIED父视图对子视图没有任何限制,可以将视图按照开发者的意愿设置成任意的大小,在一般开发过程中不会用到。2.EXACTLY父视图为子视图指定一个确切的尺寸,该尺寸由 specSize 的值来决定。3.AT_MOST父视图为子视图指定一个最大的尺寸,该尺寸的最大值是 specSize。观察 View 的 measure() 方法,可以发现该方法是被 final 修饰的,因此 View 的子类只能够通过重载 onMeasure() 方法来完成自己的测量逻辑。在 onMeasure() 方法中:调用 getDefaultSize() 方法来获取视图的大小:该方法中的第二个参数 measureSpec 是从 measure() 方法中传递过来的:通过 getMode() 和 getSize() 解析获取其中对应的值,再根据 specMode 给最终的 size 赋值。不过以上只是一个简单控件的一次 measure 过程,在真正测量的过程中,由于一个页面往往包含多个子 View ,所以需要循环遍历测量,在 ViewGroup 中有一个 measureChildren() 方法,就是用来测量子视图的:measure 整体流程的方法调用链如下:layout在performTraversals() 方法的测量过程结束后,进入 layout 布局过程:performLayout(lp,desiredWindowWidth,desiredWindowHeight);该过程的主要作用即根据子视图的大小以及布局参数,将相应的 View 放到合适的位置上。host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());如上,layout() 方法接收了四个参数,按照顺时针,分别是左上右下。该坐标针对的是父视图,以左上为起始点,传入了之前测量出的宽度和高度。之后,让我们进入到 layout() 方法中观察:我们通过 setFrame() 方法给四个变量赋值,判断 View 的位置是否变化以及是否需要重新进行 layout,而且其中还调用了 onLayout() 方法。在进入该方法后,我们可以发现里面是空的,这是因为子视图的具体位置是相对于父视图而言的,所以 View 的 onLayout 为空实现。再进入 ViewGroup 类中查看,我们可以发现,这其实是一个抽象的方法,在这样的情况下, ViewGroup 的子类便需要重写该方法:draw绘制的流程主要如下图所示,该流程也是存在遍历子 View 绘制的过程:需要注意的是,View 的 onDraw() 方法是空的,这是因为每个视图的内容都不相同,这个部分交由子类根据自身的需要来处理,才更加合理:安卓渲染机制的整体流程1.APP 在 UI 线程构建 OpenGL 渲染需要的命令及数据;2.CPU 将数据上传(共享或者拷贝)给 GPU 。(PC 上一般有显存,但是 ARM 这种嵌入式设备内存一般是 GPU 、 CPU 共享内存);3.通知 GPU 渲染。一般而言,真机不会阻塞等待 GPU 渲染结束,通知结束后就返回执行其他任务;4.通知 SurfaceFlinger 图层合成;5.SurfaceFlinger 开始合成图层。总结移动端技术发展很快,而画面显示优化是一个持续发展的实践课题,贯穿于每个开发者的日常工作中。未来,个推技术团队将继续关注移动端的性能优化,为大家分享相关的技术干货。 ...

March 29, 2019 · 2 min · jiezi

如何辨别优劣高防服务器

说句实在话,目前市面上的高防服务器满目琳琅、良莠不齐,机房和防御存在一定的差异,甚至会有些不知名品牌滥竽充数以低价吸引客户,把根本不具防御能力或劣质的服务器以“高防服务器”的名义进行售卖,为避免不必要的损失,群英在这里教大家几招如何辨别优劣之分的高防服务器!①看机房的带宽大小,要足够大;②看机房防火墙的防御能力,一般设备至少要在100G以上;③看机房服务器的品牌,知名品牌服务器更有保障;④看服务器的线路,选择电信线路的高防服务器为最佳;⑤亲自测试高防IP段,便可知是否真实防御和其防御能力达到自身的要求.以上是如何辨别优劣高防服务器的方法,希望对你有帮助。

March 26, 2019 · 1 min · jiezi

架构设计脑图总结

xmind源件下载

March 20, 2019 · 1 min · jiezi

【译】缓存的最佳实践以及max-age的陷阱

本文翻译自:https://jakearchibald.com/201…这是一篇2016年的老文章。作者是Chrome浏览器的开发成员。本文首发于公众号:符合预期的CoyPan使用正确的缓存可以带来巨大的页面性能上的收益,节省带宽,减少服务器成本。但是许多网站并没有解决好他们的缓存问题,创造了一个race conditions,导致相互依赖的资源之间失去了同步。绝大多数缓存的最佳实践,都属于下面两种模式:模式一:不可变的内容 ,长时间的max-ageCache-Control: max-age = 31536000同一个URL对应的内容永不改变浏览器/CDN 可以缓存这个资源长达一年的时间被缓存资源的存储时间小于max-age指定的秒数时,该资源可以直接被使用而无需经过服务器。在这种模式下,你不会去改变特定url下的文件内容,你直接改变url:<script src="/script-f93bca2c.js"></script><link rel=“stylesheet” href="/styles-a837cb1e.css"><img src="/cats-0e9a2ef4.jpg" alt="…">每一个URL都包含一个跟随文件内容变换的部分。这个部分可以是版本号,修改日期,或者文件内容的hash值。大多数服务端框架都有工具可以简单的实现这个需求。Node.js下还有更轻量级的工具能够做到同样的事情,比如gulp-rev.但是,这种模式不适合诸如文章、博客这样的场景。文章和博客的URL是不会有版本号的,而且他们的内容能够随时修改。说真的,如果我在文章中犯了拼写或者语法错误,那么我需要能够快速、频繁的修改文章内容。模式二:可变的内容,总是向服务器发起校验Cache-Control: no-cache同一个url对应的内容会改变任何本地缓存的版本都是不可信的,除非服务器校验通过注意:no-cache并不意味着不缓存,而是使用缓存前必须请求服务端进行检查(或者说叫重新校验)。no-store告诉浏览器,根本不要缓存这个文件。同时,must-revalidate也不是说就『must-revalidate』,而是如果本地资源的缓存时间还没有超过设置的max-age的值,就可以直接使用本地资源,否则必须重新校验。在这种模式下,你可以在响应头里添加一个ETag(你选择的版本ID)或者Last-Modified。客户端下一次请求资源时,会分别带上If-None-Match和If-Modified-Since,服务端会判断说:直接使用你已有的本地资源吧,他们是最新的。这就是最常见的:HTTP 304如果没有带上ETag/Last-Modified,服务端会再次返回完成的内容。这种模式总是会发起一个网络请求,而模式一是可以不用通过网络的。使用模式一时,因为网络基础建设而导致的延时是很常见的,使用模式二时,也很容易遇到网络环境带来的延迟。取而代之的是中间的东西:一个短时间的max-age设置和可变的内容。这是一种十分糟糕的妥协。对可变内容使用max-age通常是一个错误的选择不幸的是,这种做法并非不常见。比如,Github pages就是这样的。想象一下有以下三个url:/article//styles.css/scripts.js服务端都是返回的:Cache-Control: must-revalidate, max-age=600url对应的内容是变了如果浏览器缓存了一个资源版本,但是没有到10分钟,会不经过服务器直接使用这个缓存的资源。否则发起一个网络请求,带上If-Modified-Since或者If-None-Match(如果可用)这种模式在测试的时候看起来是可以的,但在现实中,会出问题,并且很难追踪。在上面的例子中,服务端确实已经更新了HTML, CSS 和JS,但是页面最终使用了缓存里的HTML,JS,CSS却是从服务端获取的最新的版本。资源版本不匹配导致了页面出错。通常情况下,当我们对HTML进行重大更改时,我们还可能更改HTML对应的CSS结构,并更新JS以适应样式和内容的更改。这些资源是相互依赖的,但是缓存的header是无法描述这种依赖的。用户最终看到的,可能是一两个新版本的资源,和其他老的资源。max-age和响应时间有关,因此,如果上述所有的资源都是在同一次访问中请求的,他们大概会在同一时间到期,但是仍然有很小的可能发生竞争。如果你的某些页面并不包含JS或者包含了不同的CSS,那么过期时间可能就不同步了。更糟糕的是,更糟糕的是,浏览器总是从缓存中删除东西,它不知道HTML、CSS和JS是相互依赖的,所以它会很高兴地删除一个而不是其他的。上述的情况,都可能会导致页面资源的版本不匹配。对用户来说,他们最终会看到错误的页面布局和错误的页面功能,从细微的错误到完全不可用的内容。谢天谢地,对用户来说还是有补救措施的。刷新可能会修复这个问题如果页面作为刷新的一部分加载,浏览器会忽略max-age,向服务器进行验证。因此,如果用户遭遇了因为max-age而造成的错误,刷新是可以解决问题的。当然,强迫用户这样做会降低信任度,因为这会让你感觉到你的网站是不靠谱的。service worker可能会延长这些bug的寿命假设你有以下的service worker:const version = ‘2’;self.addEventListener(‘install’, event => { event.waitUntil( caches.open(static-${version}) .then(cache => cache.addAll([ ‘/styles.css’, ‘/script.js’ ])) );});self.addEventListener(‘activate’, event => { // …delete old caches…});self.addEventListener(‘fetch’, event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) );});这个service-worker缓存了script和style如果命中了缓存,就从缓存中取,否则发起网络请求如果我们更改了CSS/JS,我们会修改service-worker中的版本号,触发service-worker的更新。但是,假如addAll发出的请求经过了HTTP缓存(和其他大多数缓存一样),我们也会进入到max-age的race condition,缓存不匹配的CSS、JS版本。一旦他们被缓存了,我们将会一直看到不匹配的CSS和JS,直到我们下一次更新service-worker。而在下一次更新时,我们可能还会陷入另一个race condition。你可以在service worker中跳过缓存:self.addEventListener(‘install’, event => { event.waitUntil( caches.open(static-${version}) .then(cache => cache.addAll([ new Request(’/styles.css’, { cache: ’no-cache’ }), new Request(’/script.js’, { cache: ’no-cache’ }) ])) );});不幸的是,这个缓存的设置在Chrome/Opera中还不支持,Firefox也是刚刚支持。你可以自己来实现类似的功能:self.addEventListener(‘install’, event => { event.waitUntil( caches.open(static-${version}) .then(cache => Promise.all( [ ‘/styles.css’, ‘/script.js’ ].map(url => { // cache-bust using a random query string return fetch(${url}?${Math.random()}).then(response => { // fail on 404, 500 etc if (!response.ok) throw Error(‘Not ok’); return cache.put(url, response); }) }) )) );});在上述代码中,我用随机数来避免缓存,但是你可以更进一步,在构建的时候为内容增加一个hash值(和sw-precache做的事差不多)。这是一种在js层面的对模式一的实现,但是仅仅对service worker的使用者是有效的,而不是对所有的浏览器和你的CDN都有效。service worker & http缓存可以同时使用,不要让他们冲突正如你所见,你可以绕过service worker中糟糕的缓存,但是你最好解决根源的问题。正确的设置缓存能够让你在使用service worker的时候更加轻松,并且对那些不支持service worker的浏览器也是有好处的,还能让你充分的使用你的CDN。正确的缓存头还意味着你可以大量简化server worker的更新:const version = ‘23’;self.addEventListener(‘install’, event => { event.waitUntil( caches.open(static-${version}) .then(cache => cache.addAll([ ‘/’, ‘/script-f93bca2c.js’, ‘/styles-a837cb1e.css’, ‘/cats-0e9a2ef4.jpg’ ])) );});在这里,我将使用模式2(服务器重新验证)缓存根页面,其余资源使用模式1(不可变内容)。每次service worker更新都将触发对根页面的请求,但只有当资源的URL发生更改时,才会下载其余资源。这很好,因为无论你是从以前的版本还是第10个版本更新,它都可以节省带宽并提高性能。相对于本地应用来说,这是一个巨大的优势。在本地应用中,不管二进制内容有细微和巨大的改变,整个二进制内容都会被下载。而在这里,我们只需要一个小小的下载,就能更新巨大的web app.service worker的工作最好是作为一个增强方案,而不是变通方案。所以预期与缓存抗争,不如好好利用缓存。谨慎使用,max-age & 可变内容 也可以很有效对于可变内容使用max-age一般情况下是一个错误的选择,但也不总是这样。比如,这个页面设置了一个3分钟的max-age. race condition在这个页面是不会成为问题的,因为这个页面没有任何遵循这一种模式的依赖(我的css,js,图片等都遵循模式1-不可变内容),依赖于此页的任何内容都不会遵循相同的模式。这种模式意味着,如果我有幸写了一篇热门文章,我的cdn可以让我的服务器散热,而我能忍受用户需要花三分钟时间才看到文章更新。这种模式不能随便使用。如果我在文章中添加了一个新的部分,并且将这个部分链接到一篇新的文章,那么我就创造了一个会争用的依赖项。用户可以单击链接,并在没有引用部分的情况下获取文章的副本。如果我想避免这种情况,我就得更新第一篇文章,刷新cdn, 等待3分钟,然后在另一篇文章中添加指向他的链接。是的…..你必须非常小心这种模式。正确使用,缓存能极大的提高性能并且较少带宽消耗。对于任何容易更改的URL,都支持不可变的内容,否则在服务器重新验证时会使其安全。只有当你足够勇敢,并且你确信你没有可能会失去同步的依赖项时,再使用max-age和可变内容的模式。 ...

March 19, 2019 · 1 min · jiezi

浏览器缓存解析

浏览器缓存浏览器缓存分为几个阶段:浏览器缓存阶段一.强制缓存阶段1.cache-control: 决定了浏览器端和服务器端缓存的策略,可以出现在响应头response header中,或者 请求头 request header中max-age:指定缓存的最大有效时间,eg:cache-control:max-age=315360000,注意与expires做区分(与cache-control平级),max-age优先级高于 expires,这个属性时HTTP1.1中新增的属性s-maxage:指定public的缓存,缓存设备有很多,不仅仅浏览器是缓存设备,在整个网络中,可能会存在代理服务器,CDN属于public缓存设备,因为可以多用户访问并读取信息;什么是private缓存呢,指的是只是你个人访问的设备,浏览器就属于private缓存设备,eg:s-maxage=31536000;他的优先级高于max-age,只能设定public的缓存设备privatepublicno-cache:错误理解:不使用缓存;no-cache指的是不管本地是否设置了max-age(即忽略本地浏览器端的缓存策略),都要向服务器端发送请求,由服务器端来判断缓存情况no-store:完全不使用任何的缓存策略,不管是服务器端还是浏览器端的2.expires:Thu, 14 Mar 2019 17:29:17 GMT,这个属性时HTTP1.0中配置,缓存过期时间,用来指定资源到期时间,是服务器端具体的时间点。告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。二.协商缓存阶段基于客户端和服务器端的协商缓存机制1.last-modifiedlast-modified - response headerif-modified-since - request header需要与cache-control共同使用,如果配置了max-age 且没有过期,就不会使用last-modified;过期了之后,才会使用last-modified。last-modified 缺点:服务器端不能精确获取 文件变更时间时文件修改时间改了,文件内容没有变以秒为单位,如果是ms内修改了文件,就体现不出来2.ETag文档内容的hash值ETag —- response headerif-None-Match —-request headeretag优先级高于last-modified状态码解析200(from cache): 浏览器端缓存,cache-control:max-age=315360000 或者expires起作用304: 服务器端缓存,last-modified 或者 etag 起作用200:浏览器端没有缓存,或者服务器端缓存失效,或者用户点击了ctrl+F5 浏览器直接从服务器端下载最新的数据注意:Chrome浏览器,手动点击刷新按钮都会 在请求头中,添加 chche-control:max-age=0,这样就肯定不会使用浏览器端的缓存!更加详细,请参考:缓存详解

March 19, 2019 · 1 min · jiezi

Nginx(2)-创建具有缓存功能的反向代理服务器

承接上一篇文章,在本文中,将上文中的静态资源服务器作为上游服务器,另外搭建一台 Nginx 服务器,作为反向代理服务器。配置反向代理服务器上游服务器处理的业务逻辑相对复杂,而且强调开发效率,所以它的性能并不优秀,使用 nginx 作为反向代理后,可以将请求将根据负载均衡算法,分散到多台上游(后端)服务器,这样就实现了架构上的水平扩展,让用户无感知的情况下,添加更多的服务器,来提升性能,即使后端的服务器出现问题,nginx反向代理服务器会转交给正常工作的服务器。一般情况下,上游服务器不对外提供访问,修改的方法是,将 server 配置块中的 listen 配置项修改为内部网络地址,修改配置文件后,重启nginx 进程,目的是防止之前打开的端口仍然可以使用。Nginx实现反向代理的功能由 ngx_http_proxy_module 实现,下面是配置示例:location / { proxy_pass http://localhost:8080; proxy_set_header Host $host; #当后端还有虚拟主机时,应该返回正确的网页,而不是用户请求不用的 host 都返回相同的内容 proxy_set_header X-Real-IP $remote_addr;}当用户请求"/“的所有 URL请求,都转交配置文件中proxy_pass指定的后端服务器,同时还设置了向后端生成请求报文时新的 header,如定义Host 将用户请求的 host 定义在 header 中,定义 X-Real-IP客户端的 IP 地址。…upstream webdlib{ #定义上游服务器群组,并自定义名称为 webdlib server 172.16.240.140:8080; #上游服务器群组的服务器列表,多台服务器可以选择负载均衡算法 }server { listen 80 server_name _; … … location / { proxy_pass http://172.16.240.140:8080; #设置上游服务器地址 proxy_set_header Host $host; #添加请求首部 host 名称,由上游服务器处理 host 请求 proxy_set_header X-Real-IP $remote_addr; #添加客户端真实 IP 地址 proxy_set_header X-Forward-For $proxy_add_x_forwarded_for; }配置缓存服务器通常只有动态请求,也就是不同的用户访问同一个 url内容不相同时,请求才会交由上游处理,在页面中,一部分内容在一段时间不会发生变化,为了减轻上游服务器的压力,将上游服务器返回的内容,缓存在反向代理服务器中保存一段时间,如几个小时或一天,在缓存时间内,即使上游服务器内容发生变化,也会被忽视,将缓存的内容向浏览器发送。使用缓存会提供站点的响应性能。首先要在 http 配置块下,使用proxy_cache_path定义缓存文件的路径、文件命名方式、命名共享内存及共享内存的空间大小等信息,如proxy_cache_path /tmp/nginxcache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;缓存的使用方法则是,在需要进行缓存url 路径下,添加 proxy_cache、proxy_cache_key、proxy_cache_valid。proxy_cache my_cache:指定缓存共享内存的命名proxy_cahce_key $host$uri$is_args$args:在共享内存中设置的 key 的值,这里将 host,uri 等作为 key 值Proxy_cache_valid 200 304 302 1d :指定的响应不返回缓存下面是关于缓存的配置文件节选:…http { … proxy_cache_path /tmp/nginxcache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off; … server { … location / { proxy_pass http://172.16.240.140:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forward-For $proxy_add_x_forwarded_for; proxy_cache my_cache; proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 304 302 1d; } }}总结首先配置反向代理服务器,需要使用 proxy_pass设置上游服务地址、使用 proxy_set_header设置向后端发送请求的 header诸如客户端的 IP 地址、请求的 host。配置缓存服务器,首先要设置缓存的名称,内存空间名称等信息,然后在需要进行缓存的 URL 路径下,启用缓存,进行缓存的设置诸如缓存的名称、缓存的 key 等。 ...

March 18, 2019 · 1 min · jiezi

SpringBoot 填坑 | Shiro 与 Redis 多级缓存问题

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言来自不愿意透露姓名的小师弟的投稿。这篇主要讲了,项目中配置了多缓存遇到的坑,以及解决办法。发现问题在一次项目实践中有实现多级缓存其中有已经包括了 Shiro 的 Cache ,本以为开启 redis 的缓存是一件很简单的事情只需要在启动类上加上 @EnableCaching 注解就会启动缓存管理了,但是问题出现了。重要错误日志截图java.lang.IllegalStateException: @Bean method ShiroConfig.cacheManager called as a bean reference for type [org.apache.shiro.cache.ehcache.EhCacheManager] but overridden by non-compatible bean instance of type [org.springframework.data.redis.cache.RedisCacheManager]. Overriding bean of same name declared in: class path resource [org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.class]错误日志分析看日志大概就发现一个非法状态异常,我们继续查看接下来的日志有一段非常的重要日志 Overriding bean of same name 翻译过来的意思是帮你重写了一个名字一样的 Bean,我再看看日志里有提到 RedisCacheManager 与我自己实现的 cacheManager 到这里我已经感觉到问题所在了,以下图一为 RedisCacheManager 部分实现代码。图二为我自己的 Shiro 的 cacheManager 实现方法。解决问题有 Spring 基础的大家都应该还记得 Spring 不允许有相同的 Bean 出现。现在问题就在于 Redis 缓存管理器和 Shiro 的缓存管理器重名了,而这二者又是通过 Spring 管理,所以 Spring 读取这二者的时候,产生冲突了。解决问题的方法很简单:在自己实现 EhCacheManager 时把 @Bean 指定一个名字可以像这样 @Bean(name =“ehCacheManager” ),还有其他办法大家可以在想办法实现一下嘿嘿。结语虽然我们都知道 Spring 的报错是非常多的,但是在 Spring 的报错日志中查找问题所在是非常有用的,大部分的错误,日志都会给你反馈。如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 16, 2019 · 1 min · jiezi

golang防缓存击穿利器--singleflight

缓存击穿 给缓存加一个过期时间,下次未命中缓存时再去从数据源获取结果写入新的缓存,这个是后端开发人员再熟悉不过的基操。本人之前在做直播平台活动业务的时候,当时带着这份再熟练不过的自信,把复杂的数据库链表语句写好,各种微服务之间调用捞数据最后算好的结果,丢进了缓存然后设了一个过期时间,当时噼里啪啦两下写完代码觉得稳如铁蛋,结果在活动快结束之前,数据库很友好的挂掉了。当时回去查看监控后发现,是在活动快结束前,大量用户都在疯狂的刷活动页,导致缓存过期的瞬间有大量未命中缓存的请求直接打到数据库上所导致的,所以这个经典的问题稍不注意还是害死人 防缓存击穿的方式有很多种,比如通过计划任务来跟新缓存使得从前端过来的所有请求都是从缓存读取等等。之前读过 groupCache的源码,发现里面有一个很有意思的库,叫singleFlight, 因为groupCache从节点上获取缓存如果未命中,则会去其他节点寻找,其他节点还没有的话再从数据源获取,所以这个步骤对于防击穿非常有必要。singleFlight使得groupCache在多个并发请求对一个失效的key进行源数据获取时,只让其中一个得到执行,其余阻塞等待到执行的那个请求完成后,将结果传递给阻塞的其他请求达到防止击穿的效果。SingleFlight 使用Demo本文模拟一个数据源是从调用rpc获取的场景然后再模拟一百个并发请求在缓存失效的瞬间同时调用rpc访问源数据效果可以看到100个并发请求从源数据获取时,rpcServer端只收到了来自client 17的请求,而其余99个最后也都得到了正确的返回值。SingleFlight 源码剖析在看完singleFlight的实际效果后,欣喜若狂,想必其实现应该相当复杂吧, 结果翻看源码一看, 100行不到的代码就解决了这么个业务痛点, 不得不佩服。package singlefilghtimport “sync"type Group struct { mu sync.Mutex m map[string]*Call // 对于每一个需要获取的key有一个对应的call}// call代表需要被执行的函数type Call struct { wg sync.WaitGroup // 用于阻塞这个调用call的其他请求 val interface{} // 函数执行后的结果 err error // 函数执行后的error}func (g *Group) Do(key string, fn func()(interface{}, error)) (interface{}, error) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*Call) } // 如果获取当前key的函数正在被执行,则阻塞等待执行中的,等待其执行完毕后获取它的执行结果 if c, ok := g.m[key]; ok { g.mu.Unlock() c.wg.Wait() return c.val, c.err } // 初始化一个call,往map中写后就解 c := new(Call) c.wg.Add(1) g.m[key] = c g.mu.Unlock() // 执行获取key的函数,并将结果赋值给这个Call c.val, c.err = fn() c.wg.Done() // 重新上锁删除key g.mu.Lock() delete(g.m, key) g.mu.Unlock() return c.val, c.err} 对的没看错, 就这么100行不到的代码就能解决缓存击穿的问题,这算是我写过最愉快的一篇博了,同时也推荐大家去读一读groupCache这个项目的源码,会有更多惊喜的发现 ...

March 11, 2019 · 1 min · jiezi

你应该知道的缓存进化史

1.背景本文是上周去技术沙龙听了一下爱奇艺的Java缓存之路有感写出来的。先简单介绍一下爱奇艺的java缓存道路的发展吧。可以看见图中分为几个阶段:第一阶段:数据同步加redis通过消息队列进行数据同步至redis,然后Java应用直接去取缓存这个阶段优点是:由于是使用的分布式缓存,所以数据更新快。缺点也比较明显:依赖Redis的稳定性,一旦redis挂了,整个缓存系统不可用,造成缓存雪崩,所有请求打到DB。第二,三阶段:JavaMap到Guava cache这个阶段使用进程内缓存作为一级缓存,redis作为二级。优点:不受外部系统影响,其他系统挂了,依然能使用。缺点:进程内缓存无法像分布式缓存那样做到实时更新。由于java内存有限,必定缓存得设置大小,然后有些缓存会被淘汰,就会有命中率的问题。第四阶段: Guava Cache刷新为了解决上面的问题,利用Guava Cache可以设置写后刷新时间,进行刷新。解决了一直不更新的问题,但是依然没有解决实时刷新。第五阶段: 外部缓存异步刷新这个阶段扩展了Guava Cache,利用redis作为消息队列通知机制,通知其他java应用程序进行刷新。这里简单介绍一下爱奇艺缓存发展的五个阶段,当然还有一些其他的优化,比如GC调优,缓存穿透,缓存覆盖的一些优化等等。有兴趣的同学可以关注公众号,联系我进行交流。原始社会 - 查库上面说的是爱奇艺的一个进化线路,但是在大家的一般开发过程中,第一步一般都没有redis,而是直接查库。在流量不大的时候,查数据库或者读取文件是最为方便,也能完全满足我们的业务要求。古代社会 - HashMap当我们应用有一定流量之后或者查询数据库特别频繁,这个时候就可以祭出我们的java中自带的HashMap或者ConcurrentHashMap。我们可以在代码中这么写:public class CustomerService { private HashMap<String,String> hashMap = new HashMap<>(); private CustomerMapper customerMapper; public String getCustomer(String name){ String customer = hashMap.get(name); if ( customer == null){ customer = customerMapper.get(name); hashMap.put(name,customer); } return customer; }}但是这样做就有个问题HashMap无法进行数据淘汰,内存会无限制的增长,所以hashMap很快也被淘汰了。当然并不是说他完全就没用,就像我们古代社会也不是所有的东西都是过时的,比如我们中华名族的传统美德是永不过时的,就像这个hashMap一样的可以在某些场景下作为缓存,当不需要淘汰机制的时候,比如我们利用反射,如果我们每次都通过反射去搜索Method,field,性能必定低效,这时我们用HashMap将其缓存起来,性能能提升很多。近代社会 - LRUHashMap在古代社会中难住我们的问题无法进行数据淘汰,这样会导致我们内存无限膨胀,显然我们是不可以接受的。有人就说我把一些数据给淘汰掉呗,这样不就对了,但是怎么淘汰呢?随机淘汰吗?当然不行,试想一下你刚把A装载进缓存,下一次要访问的时候就被淘汰了,那又会访问我们的数据库了,那我们要缓存干嘛呢?所以聪明的人们就发明了几种淘汰算法,下面列举下常见的三种FIFO,LRU,LFU(还有一些ARC,MRU感兴趣的可以自行搜索):FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。LRU:最近最少使用算法。在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。但是这个依然有个问题,如果有个数据在1个小时的前59分钟访问了1万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。LFU:最近最少频率使用。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了LRU不能处理时间段的问题。上面列举了三种淘汰策略,对于这三种,实现成本是一个比一个高,同样的命中率也是一个比一个好。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的LRU,如何实现一个LRUMap呢?我们可以通过继承LinkedHashMap,重写removeEldestEntry方法,即可完成一个简单的LRUMap。class LRUMap extends LinkedHashMap { private final int max; private Object lock; public LRUMap(int max, Object lock) { //无需扩容 super((int) (max * 1.4f), 0.75f, true); this.max = max; this.lock = lock; } /** * 重写LinkedHashMap的removeEldestEntry方法即可 * 在Put的时候判断,如果为true,就会删除最老的 * @param eldest * @return / @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > max; } public Object getValue(Object key) { synchronized (lock) { return get(key); } } public void putValue(Object key, Object value) { synchronized (lock) { put(key, value); } } public boolean removeValue(Object key) { synchronized (lock) { return remove(key) != null; } } public boolean removeAll(){ clear(); return true; } }在LinkedHashMap中维护了一个entry(用来放key和value的对象)链表。在每一次get或者put的时候都会把插入的新entry,或查询到的老entry放在我们链表末尾。可以注意到我们在构造方法中,设置的大小特意设置到max1.4,在下面的removeEldestEntry方法中只需要size>max就淘汰,这样我们这个map永远也走不到扩容的逻辑了,通过重写LinkedHashMap,几个简单的方法我们实现了我们的LruMap。现代社会 - Guava cache在近代社会中已经发明出来了LRUMap,用来进行缓存数据的淘汰,但是有几个问题:锁竞争严重,可以看见我的代码中,Lock是全局锁,在方法级别上面的,当调用量较大时,性能必然会比较低。不支持过期时间不支持自动刷新所以谷歌的大佬们对于这些问题,按捺不住了,发明了Guava cache,在Guava cache中你可以如下面的代码一样,轻松使用:public static void main(String[] args) throws ExecutionException { LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) //写之后30ms过期 .expireAfterWrite(30L, TimeUnit.MILLISECONDS) //访问之后30ms过期 .expireAfterAccess(30L, TimeUnit.MILLISECONDS) //20ms之后刷新 .refreshAfterWrite(20L, TimeUnit.MILLISECONDS) //开启weakKey key 当启动垃圾回收时,该缓存也被回收 .weakKeys() .build(createCacheLoader()); System.out.println(cache.get(“hello”)); cache.put(“hello1”, “我是hello1”); System.out.println(cache.get(“hello1”)); cache.put(“hello1”, “我是hello2”); System.out.println(cache.get(“hello1”)); } public static com.google.common.cache.CacheLoader<String, String> createCacheLoader() { return new com.google.common.cache.CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return key; } }; }我将会从guava cache原理中,解释guava cache是如何解决LRUMap的几个问题的。锁竞争guava cache采用了类似ConcurrentHashMap的思想,分段加锁,在每个段里面各自负责自己的淘汰的事情。在Guava根据一定的算法进行分段,这里要说明的是,如果段太少那竞争依然很严重,如果段太多会容易出现随机淘汰,比如大小为100的,给他分100个段,那也就是让每个数据都独占一个段,而每个段会自己处理淘汰的过程,所以会出现随机淘汰。在guava cache中通过如下代码,计算出应该如何分段。 int segmentShift = 0; int segmentCount = 1; while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1; }上面segmentCount就是我们最后的分段数,其保证了每个段至少10个Entry。如果没有设置concurrencyLevel这个参数,那么默认就会是4,最后分段数也最多为4,例如我们size为100,会分为4段,每段最大的size是25。在guava cache中对于写操作直接加锁,对于读操作,如果读取的数据没有过期,且已经加载就绪,不需要进行加锁,如果没有读到会再次加锁进行二次读,如果还没有需要进行缓存加载,也就是通过我们配置的CacheLoader,我这里配置的是直接返回Key,在业务中通常配置从数据库中查询。如下图所示:过期时间相比于LRUMap多了两种过期时间,一个是写后多久过期expireAfterWrite,一个是读后多久过期expireAfterAccess。很有意思的事情是,在guava cache中对于过期的Entry并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。看下面的代码:public static void main(String[] args) throws ExecutionException, InterruptedException { Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) //写之后5s过期 .expireAfterWrite(5, TimeUnit.MILLISECONDS) .concurrencyLevel(1) .build(); cache.put(“hello1”, “我是hello1”); cache.put(“hello2”, “我是hello2”); cache.put(“hello3”, “我是hello3”); cache.put(“hello4”, “我是hello4”); //至少睡眠5ms Thread.sleep(5); System.out.println(cache.size()); cache.put(“hello5”, “我是hello5”); System.out.println(cache.size()); }输出:4 1从这个结果中我们知道,在put的时候才进行的过期处理。特别注意的是我上面concurrencyLevel(1)我这里将分段最大设置为1,不然不会出现这个实验效果的,在上面一节中已经说过,我们是以段位单位进行过期处理。在每个Segment中维护了两个队列: final Queue<ReferenceEntry<K, V>> writeQueue; final Queue<ReferenceEntry<K, V>> accessQueue;writeQueue维护了写队列,队头代表着写得早的数据,队尾代表写得晚的数据。accessQueue维护了访问队列,和LRU一样,用来我们进行访问时间的淘汰,如果当这个Segment超过最大容量,比如我们上面所说的25,超过之后,就会把accessQueue这个队列的第一个元素进行淘汰。void expireEntries(long now) { drainRecencyQueue(); ReferenceEntry<K, V> e; while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } }上面就是guava cache处理过期Entries的过程,会对两个队列一次进行peek操作,如果过期就进行删除。一般处理过期Entries可以在我们的put操作的前后,或者读取数据时发现过期了,然后进行整个Segment的过期处理,又或者进行二次读lockedGetOrLoad操作的时候调用。void evictEntries(ReferenceEntry<K, V> newest) { ///… 省略无用代码 while (totalWeight > maxSegmentWeight) { ReferenceEntry<K, V> e = getNextEvictable(); if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) { throw new AssertionError(); } } }/**返回accessQueue的entry/ReferenceEntry<K, V> getNextEvictable() { for (ReferenceEntry<K, V> e : accessQueue) { int weight = e.getValueReference().getWeight(); if (weight > 0) { return e; } } throw new AssertionError(); }上面是我们驱逐Entry的时候的代码,可以看见访问的是accessQueue对其队头进行驱逐。而驱逐策略一般是在对segment中的元素发生变化时进行调用,比如插入操作,更新操作,加载数据操作。自动刷新自动刷新操作,在guava cache中实现相对比较简单,直接通过查询,判断其是否满足刷新条件,进行刷新。其他特性在Guava cache中还有一些其他特性:虚引用在Guava cache中,key和value都能进行虚引用的设定,在Segment中的有两个引用队列: final @Nullable ReferenceQueue<K> keyReferenceQueue; final @Nullable ReferenceQueue<V> valueReferenceQueue;这两个队列用来记录被回收的引用,其中每个队列记录了每个被回收的Entry的hash,这样回收了之后通过这个队列中的hash值就能把以前的Entry进行删除。删除监听器在guava cache中,当有数据被淘汰时,但是你不知道他到底是过期,还是被驱逐,还是因为虚引用的对象被回收?这个时候你可以调用这个方法removalListener(RemovalListener listener)添加监听器进行数据淘汰的监听,可以打日志或者一些其他处理,可以用来进行数据淘汰分析。在RemovalCause记录了所有被淘汰的原因:被用户删除,被用户替代,过期,驱逐收集,由于大小淘汰。guava cache的总结细细品读guava cache的源码总结下来,其实就是一个性能不错的,api丰富的LRU Map。爱奇艺的缓存的发展也是基于此之上,通过对guava cache的二次开发,让其可以进行java应用服务之间的缓存更新。走向未来-caffeineguava cache的功能的确是很强大,满足了绝大多数的人的需求,但是其本质上还是LRU的一层封装,所以在众多其他较为优良的淘汰算法中就相形见绌了。而caffeine cache实现了W-TinyLFU(LFU+LRU算法的变种)。下面是不同算法的命中率的比较:其中Optimal是最理想的命中率,LRU和其他算法相比的确是个弟弟。而我们的W-TinyLFU 是最接近理想命中率的。当然不仅仅是命中率caffeine优于了guava cache,在读写吞吐量上面也是完爆guava cache。这个时候你肯定会好奇为啥这么caffeine这么牛逼呢?别着急下面慢慢给你道来。W-TinyLFU上面已经说过了传统的LFU是怎么一回事。在LFU中只要数据访问模式的概率分布随时间保持不变时,其命中率就能变得非常高。这里我还是拿爱奇艺举例,比如有部新剧出来了,我们使用LFU给他缓存下来,这部新剧在这几天大概访问了几亿次,这个访问频率也在我们的LFU中记录了几亿次。但是新剧总会过气的,比如一个月之后这个新剧的前几集其实已经过气了,但是他的访问量的确是太高了,其他的电视剧根本无法淘汰这个新剧,所以在这种模式下是有局限性。所以各种LFU的变种出现了,基于时间周期进行衰减,或者在最近某个时间段内的频率。同样的LFU也会使用额外空间记录每一个数据访问的频率,即使数据没有在缓存中也需要记录,所以需要维护的额外空间很大。可以试想我们对这个维护空间建立一个hashMap,每个数据项都会存在这个hashMap中,当数据量特别大的时候,这个hashMap也会特别大。再回到LRU,我们的LRU也不是那么一无是处,LRU可以很好的应对突发流量的情况,因为他不需要累计数据频率。所以W-TinyLFU结合了LRU和LFU,以及其他的算法的一些特点。频率记录首先要说到的就是频率记录的问题,我们要实现的目标是利用有限的空间可以记录随时间变化的访问频率。在W-TinyLFU中使用Count-Min Sketch记录我们的访问频率,而这个也是布隆过滤器的一种变种。如下图所示:如果需要记录一个值,那我们需要通过多种Hash算法对其进行处理hash,然后在对应的hash算法的记录中+1,为什么需要多种hash算法呢?由于这是一个压缩算法必定会出现冲突,比如我们建立一个Long的数组,通过计算出每个数据的hash的位置。比如张三和李四,他们两有可能hash值都是相同,比如都是1那Long[1]这个位置就会增加相应的频率,张三访问1万次,李四访问1次那Long[1]这个位置就是1万零1,如果取李四的访问评率的时候就会取出是1万零1,但是李四命名只访问了1次啊,为了解决这个问题,所以用了多个hash算法可以理解为long[][]二维数组的一个概念,比如在第一个算法张三和李四冲突了,但是在第二个,第三个中很大的概率不冲突,比如一个算法大概有1%的概率冲突,那四个算法一起冲突的概率是1%的四次方。通过这个模式我们取李四的访问率的时候取所有算法中,李四访问最低频率的次数。所以他的名字叫Count-Min Sketch。这里和以前的做个对比,简单的举个例子:如果一个hashMap来记录这个频率,如果我有100个数据,那这个HashMap就得存储100个这个数据的访问频率。哪怕我这个缓存的容量是1,因为Lfu的规则我必须全部记录这个100个数据的访问频率。如果有更多的数据我就有记录更多的。在Count-Min Sketch中,我这里直接说caffeine中的实现吧(在FrequencySketch这个类中),如果你的缓存大小是100,他会生成一个long数组大小是和100最接近的2的幂的数,也就是128。而这个数组将会记录我们的访问频率。在caffeine中他规则频率最大为15,15的二进制位1111,总共是4位,而Long型是64位。所以每个Long型可以放16种算法,但是caffeine并没有这么做,只用了四种hash算法,每个Long型被分为四段,每段里面保存的是四个算法的频率。这样做的好处是可以进一步减少Hash冲突,原先128大小的hash,就变成了128X4。一个Long的结构如下:我们的4个段分为A,B,C,D,在后面我也会这么叫它们。而每个段里面的四个算法我叫他s1,s2,s3,s4。下面举个例子如果要添加一个访问50的数字频率应该怎么做?我们这里用size=100来举例。首先确定50这个hash是在哪个段里面,通过hash & 3必定能获得小于4的数字,假设hash & 3=0,那就在A段。对50的hash再用其他hash算法再做一次hash,得到long数组的位置。假设用s1算法得到1,s2算法得到3,s3算法得到4,s4算法得到0。然后在long[1]的A段里面的s1位置进行+1,简称1As1加1,然后在3As2加1,在4As3加1,在0As4加1。这个时候有人会质疑频率最大为15的这个是否太小?没关系在这个算法中,比如size等于100,如果他全局提升了1000次就会全局除以2衰减,衰减之后也可以继续增加,这个算法再W-TinyLFU的论文中证明了其可以较好的适应时间段的访问频率。读写性能在guava cache中我们说过其读写操作中夹杂着过期时间的处理,也就是你在一次Put操作中有可能还会做淘汰操作,所以其读写性能会受到一定影响,可以看上面的图中,caffeine的确在读写操作上面完爆guava cache。主要是因为在caffeine,对这些事件的操作是通过异步操作,他将事件提交至队列,这里的队列的数据结构是RingBuffer,不清楚的可以看看这篇文章,你应该知道的高性能无锁队列Disruptor。然后通过会通过默认的ForkJoinPool.commonPool(),或者自己配置线程池,进行取队列操作,然后在进行后续的淘汰,过期操作。当然读写也是有不同的队列,在caffeine中认为缓存读比写多很多,所以对于写操作是所有线程共享一个Ringbuffer。对于读操作比写操作更加频繁,进一步减少竞争,其为每个线程配备了一个RingBuffer:数据淘汰策略在caffeine所有的数据都在ConcurrentHashMap中,这个和guava cache不同,guava cache是自己实现了个类似ConcurrentHashMap的结构。在caffeine中有三个记录引用的LRU队列:Eden队列:在caffeine中规定只能为缓存容量的%1,如果size=100,那这个队列的有效大小就等于1。这个队列中记录的是新到的数据,防止突发流量由于之前没有访问频率,而导致被淘汰。比如有一部新剧上线,在最开始其实是没有访问频率的,防止上线之后被其他缓存淘汰出去,而加入这个区域。伊甸区,最舒服最安逸的区域,在这里很难被其他数据淘汰。Probation队列:叫做缓刑队列,在这个队列就代表你的数据相对比较冷,马上就要被淘汰了。这个有效大小为size减去eden减去protected。Protected队列:在这个队列中,可以稍微放心一下了,你暂时不会被淘汰,但是别急,如果Probation队列没有数据了或者Protected数据满了,你也将会被面临淘汰的尴尬局面。当然想要变成这个队列,需要把Probation访问一次之后,就会提升为Protected队列。这个有效大小为(size减去eden) X 80% 如果size =100,就会是79。这三个队列关系如下:所有的新数据都会进入Eden。Eden满了,淘汰进入Probation。如果在Probation中访问了其中某个数据,则这个数据升级为Protected。如果Protected满了又会继续降级为Probation。对于发生数据淘汰的时候,会从Probation中进行淘汰,会把这个队列中的数据队头称为受害者,这个队头肯定是最早进入的,按照LRU队列的算法的话那他其实他就应该被淘汰,但是在这里只能叫他受害者,这个队列是缓刑队列,代表马上要给他行刑了。这里会取出队尾叫候选者,也叫攻击者。这里受害者会和攻击者做PK,通过我们的Count-Min Sketch中的记录的频率数据有以下几个判断:如果攻击者大于受害者,那么受害者就直接被淘汰。如果攻击者<=5,那么直接淘汰攻击者。这个逻辑在他的注释中有解释:他认为设置一个预热的门槛会让整体命中率更高。其他情况,随机淘汰。如何使用对于熟悉Guava的玩家来说如果担心有切换成本,那么你完全就多虑了,caffeine的api借鉴了Guava的api,可以发现其基本一模一样。public static void main(String[] args) { Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .expireAfterAccess(1,TimeUnit.SECONDS) .maximumSize(10) .build(); cache.put(“hello”,“hello”); }顺便一提的是,越来越多的开源框架都放弃了Guava cache,比如Spring5。在业务上我也自己曾经比较过Guava cache和caffeine最终选择了caffeine,在线上也有不错的效果。所以不用担心caffeine不成熟,没人使用。最后本文主要讲了爱奇艺的缓存之路和本地缓存的一个发展历史(从古至今到未来),以及每一种缓存的实现基本原理。当然要使用好缓存光是这些仅仅不够,比如本地缓存如何在其他地方更改了之后同步更新,分布式缓存,多级缓存等等。后面也会专门写一节介绍这个如何用好缓存。对于Guava cache和caffeine的原理后面也会专门抽出时间写这两个的源码分析,如果感兴趣的朋友可以关注公众号第一时间查阅更新文章。最后这篇文章被我收录于JGrowing,一个全面,优秀,由社区一起共建的Java学习路线,如果您想参与开源项目的维护,可以一起共建,github地址为:https://github.com/javagrowin… 麻烦给个小星星哟。如果你觉得这篇文章对你有文章,可以关注我的技术公众号,你的关注和转发是对我最大的支持,O(∩_∩)O ...

March 7, 2019 · 3 min · jiezi

用一张图总结web缓存策略

1 浏览器缓存浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:1.Memory Cache2.Service Worker Cache3.HTTP Cache4.Push Cache1.1 Memory CacheMemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。不过当页面关闭时,内存里的数据也就没有了。资源存不存内存,浏览器秉承的是“节约原则”。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。1.2 Service Worker CacheService Worker 是一种独立于主线程之外的 Javascript 线程。它可以帮我们实现离线缓存、消息推送和网络代理等功能。通常我们如果要使用 Service Worker 基本就是以下几个步骤:首先我们需要在页面的 JavaScript 主线程中注册 Service Worker。注册成功后后台开始安装步骤, 通常在安装的过程中需要缓存一些静态资源。安装成功后开始激活 Service Worker激活成功后 Service Worker 可以控制页面了(监听 fetch 和 message 事件),但是只针对在成功注册了 Service Worker 后打开的页面。在页面发起 http 请求时,service worker 可以通过 fetch 事件拦截请求,并且给出自己的响应。页面和 serviceWorker 之间可以通过 posetMessage() 方法发送消息,发送的消息可以通过 message 事件接收到。Service Worker 必须以 https 协议为前提。1.3 HTTP 缓存HTTP 缓存分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。1.3.1 强缓存强缓存指的是向浏览器缓存查找该请求的结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程强缓存是利用http响应头中的 Expires 和 Cache-Control 两个字段来控制的。1.3.1.1 Expires实现强缓存,过去我们一直用 expires。在服务器的响应头里,会将过期时间写入 expires 字段:那么,当我们试图再次向服务器请求资源时,浏览器就会先对比本地时间和 expires 的时间,如果本地时间小于 expires 设定的过期时间,就直接去缓存中取这个资源。不过expires依赖于本地时间,如果服务端和客户端的时间设置不同,那么expires 将无法达到我们的预期。1.3.1.2 Cache-Control考虑到 expires 的局限性,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。Cache-Control 包含以下几个值:(1)max-agecache-control: max-age=31536000max-age 会等于一个时间长度(以秒为单位)。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒以内都是有效的,完美地规避了时间戳带来的潜在问题。在代理服务器中,我们使用 s-maxage 来执行 max-age 的功能。(2)public 与 private如果我们为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存(也就是多个用户可以共享这个缓存);如果我们设置了 private,则该资源只能被浏览器缓存。private 为默认值。但多数情况下,public 并不需要我们手动设置,因为设置了 max-age 就表示响应是可以缓存的。(3)no-store 与 no-cache如果我们为资源设置了 no-cache,浏览器会对响应进行缓存,但是需要到服务器去确认这个缓存是否能用。即走我们下文即将讲解的协商缓存的路线。如果设置了 no-store ,所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存1.3.2 协商缓存协商缓存指的是强制缓存失效后,浏览器向服务器询问缓存的相关信息,进而判断是重新发起请求还是从本地拿缓存的过程。如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)。同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。1.3.2.1 Last-Modified / If-Modified-Since如果我们启用了协商缓存,Last-Modified 会在首次请求时随着响应头返回:Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在响应头中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,响应头不会再添加 Last-Modified 字段。1.3.2.2 Etag / If-None-MatchEtag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的。当首次请求时,我们会在响应头里获取到一个最初的标识符字符串:ETag: W/“2a3b-1602480f459"那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对:If-None-Match: W/“2a3b-1602480f459"不过 Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能。1.3.3 HTTP 缓存决策指南根据上文所说的 HTTP 缓存知识点,我们在面对一个具体的缓存需求时,可以根据下图的路线来决策:当我们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;否则考虑是否每次都需要向服务器进行缓存有效确认,如果需要,那么设 Cache-Control 的值为 no-cache;否则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为 private 还是 public;然后考虑该资源的过期时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存需要用到的 Etag、Last-Modified 等参数。1.4 Push CachePush Cache 是指 HTTP2 在 server push 阶段存在的缓存。2 服务器缓存2.1 CDNCDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。CDN 的核心点有两个,一个是缓存,一个是回源。“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。CDN 往往被用来存放静态资源,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。3 HTML5缓存3.1 Web StorageWeb Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。存储容量可以达到 5-10M 之间。它又分为 Local Storage 与 Session Storage。3.1.1 Local Storage 与 Session Storage 的区别两者的区别在于生命周期与作用域的不同。生命周期:存储在Local Storage的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,当会话结束(页面被关闭)时,存储内容也随之被释放。作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。3.1.2 Web Storage 核心 API 使用示例(1)存储数据localStorage.setItem(‘user_name’, ‘xiuyan’)sessionStorage.setItem(‘key’, ‘value’);Web Storage只能存字符串。(2)读取数据localStorage.getItem(‘user_name’)var data = sessionStorage.getItem(‘key’);(3)删除某一键名对应的数据localStorage.removeItem(‘user_name’)sessionStorage.removeItem(‘key’);(4)清除所有数据localStorage.clear()sessionStorage.clear();3.1.3 应用场景Local Storage 的特点之一是持久,有时我们更倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串,有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。Session Storage 更适合用来存储生命周期和它同步的会话级别的信息。这些信息只适用于当前会话,当你开启新的会话时,它也需要相应的更新或释放。比如微博的 Session Storage 就主要是存储你本次会话的浏览足迹。3.2 IndexDBIndexDB 是一个运行在浏览器上的非关系型数据库。4 参考文章前端性能优化原理与实践Service Worker 生命周期Service Worker初体验彻底理解浏览器的缓存机制 ...

February 11, 2019 · 2 min · jiezi

星期和工作日计算,你会了吗?

1、计算某个日期是星期几如下图,是宅男心中的大事记,要计算发生日期是周几,公式为:=WEEKDAY(B2,2)WEEKDAY函数第二参数使用2,用数字1到7表示周一到周日。这个函数在数组计算中经常用于按周统计数据。如果要显示为中文的星期几,公式为:=TEXT(B2,“aaaa”)TEXT的格式代码使用四个a,就会返回日期的中文星期,记不住的话,可以想成:啊…啊…啊…啊……2、计算n个工作日之后的日期雷雷给东东一封表白信,东东说要4个工作日之后给回复。4个工作日之后是哪天呢?=WORKDAY(A2,4,D2:D4)WORKDAY函数第一参数是开始日期,第二参数是指定的工作日,第三参数是要排除的法定节假日,如果没有,第三参数可省略。3、计算这个月有多少工作日男朋友爱上了前男友,苍老师也要订婚了。所以还是老老实实工作,看看这个月要工作多少天吧。=NETWORKDAYS(A2,B2,D2)NETWORKDAYS函数第一参数为起始日期。第二参数为结束日期。第三参数是需要排除的节假日,如果没有,第三参数可省略。4、每周单休的工作日计算东东梦想有一天能参加苍老师的婚礼,所以要多干点正事了。从现在开始,每周六休息一天,假如苍老师的婚礼在7月7日,计算一下还要工作多少天呢?=NETWORKDAYS.INTL(A2,B2,“0000010”,D2:D10)NETWORKDAYS.INTL函数第第一参数为起始日期。第二参数为结束日期。第三参数是指定哪一天为休息日,用7位数字表示,1是休息,0是工作。嗯嗯, 这个函数的强大之处,就是可以识别1和0……第四参数嘛,就是用于排除的法定节假日。

January 31, 2019 · 1 min · jiezi

spring cache 实现按照*号删除缓存

spring cache redis的使用过程中,删除缓存只能用具体的key删除,不能使用通配符号,原因是redis不支持del key这种通配符用法,可以通过修改redis源代码实现,但这种方式修改了redis本身代码,后期升级、维护不好操作,具体操作方式可以参见:redis del命令支持正则删除(pattern)git地址:redis-del-with-pattern我们使用改写spring-redis cache实现具体实现方式为:改写:org.springframework.data.redis.cache.RedisCache下的evict方法原为:cacheWriter.remove(name, createAndConvertCacheKey(key)); 改为:cacheWriter.clean(name, createAndConvertCacheKey(key));spring redis最底层是支持了通配符的方式的,但是经过包装后就去掉了具体在项目中的使用实例如:在查询方法上加入缓存: @Override @Cacheable(keyGenerator = “cacheKeyGenerator”) public List query(xx x) throws IllegalAccessException { return xxxx; }其中cacheKeyGenerator生成如com.demo.service.impl.xxServiceImpl-query-99986a删除或更新时: @Override @CacheEvict(key = “targetClass.name+’-*’”) public boolean saveOrUpdate(xx x) { return xxxx; }其中key时spEL表达式,生成 com.demo.service.impl.xxServiceImpl-*的key最终效果是在新增或更新时能删除所有列表的缓存key

January 30, 2019 · 1 min · jiezi

Redis Cluster 集群搭建

【背景】2年前在本地电脑上搭建过redis集群。但苦于创业公司服务器资源有限(穷),并没有应用到生产环境。近期换了个工作环境,有资源条件了准备开始搭建使用。虽然搭建过一次,但在搭建的过程当中,还是遇到一些问题,所以打算整理一份详情的搭建记录,也能给大家多一份参考选择。【环境以及资源】服务器:windows server 2008 r2下载地址:Redis:https://github.com/MSOpenTech…(Redis-x64-3.2.100.zip)【Redis服务】Ruby:http://dl.bintray.com/oneclic…(rubyinstaller-2.3.3-x64.exe)【Ruby环境安装】rubygems: https://rubygems.org/pages/do…【用于安装ruby的redis依赖】redis-trib.rb:https://raw.githubusercontent…【创建集群的ruby脚本】【部署步骤】一、部署6个redis服务1、下载redis服务绿色包 https://github.com/MSOpenTech…(Redis-x64-3.2.100.zip)。我们约定7000、7001、7002 3个端口为主节点,6000、6001、6002 3个端口为从节点,便于区分。那先来搭建第一redis服务,然后如法炮制复制另外5个即可。2、下载完成解压,修改文件夹名称为Redis-6000,修改redis.windows.conf文件里的信息如下:port 6000 [端口]cluster-enabled yes [是否开启集群]cluster-config-file nodes.conf [集群节点文件,会根据配置的名字生成在目录下]cluster-node-timeout 15000 [超时时间]appendonly yes注意:如果你是复制启动过的redis服务,记得检查目录下是否有appendonly.aof,nodes.conf和.rdb文件(持久化存储数据),删除他们,否则在创建集群的时候会失败。3、启动redis服务方法一:打开cmd,进入redis-6000目录下执行命令:redis-server.exe redis.windows.conf。方法二【推荐】:部署redis到windows服务里,便于启动。cmd进入redis-6000目录,执行命令redis-server –service-install redis.windows.conf –service-name Redis6000 –loglevel verbose打开服务找到redis6000,右击启动。启动后查看目录下会多出2个文件4、这样我们第一个redis服务安装成功,那接着依照同样的步骤搭建另外5个redis服务。二、安装Ruby环境redis-trib.rb是ruby脚本编写的,可方便的搭建集群,所以我们需要安装Ruby环境来执行脚本。1、打开下载地址 http://dl.bintray.com/oneclic… 选择 rubyinstaller-2.3.3-x64.exe 进行下载。2、运行安装(全部勾选)3、安装完毕,测试是否安装成功。打开cmd,输入命令 ruby -v 。显示版本代表环境安装成功。三、安装rubygems使用rubygems是为了安装ruby的redis依赖。1、打开下载地址 https://rubygems.org/pages/do…, 下载rubygems-update-3.0.2.gem,放到ruby目录。2、运行命令gem install –local D:Ruby23-x64rubygems-update-3.0.2.gem3、安装完成gem后,安装 Redis依赖,在cmd里继续执行 gem install redis四、使用redis-trib.rb安装集群1、打开下载地址 https://raw.githubusercontent… (指向最新版本),本文使用地址建议https://raw.githubusercontent…,另存为到本地 (一个文本类文件)。2、打开cmd,进入redis-trib.rb文件所在目录,执行命令redis-trib.rb create –replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:6000 127.0.0.1:6001 127.0.0.1:60023、提示后输入yes如图能看到3主3从的配置信息rediscluster采用了哈希分区的“虚拟槽分区”方式,所有的键根据哈希函数(CRC16[key]&16383)映射到0-16383槽内,共16384个槽位。主节点7000端口分槽是0-5460,对应从库是6001主节点7001端口分槽是5461-10922,对应从库是6002主节点7002端口分槽是10923-16383,对应从库是6000【错误帮助】情况1、redis-trib.rb脚本版本与ruby的redis依赖版本不一致说明redis-trib.rb的代码版本与当前ruby安装的redis依赖版本不匹配。查看版本方法:打开ruby的gems安装目录,搜索redis。https://raw.githubusercontent… (更改链接/redis/版本号/)下载对应版本的脚本。情况2:无法连接其他节点确保端口打开,各台机子上能互相通信五、数据读写测试1、打开cmd,进入redis6000端口的目录,输入redis-cli -h 127.0.0.1 -p 6000 -c2、我们存入2个key进行测试,key和age,如图所示2个key被分入7002和7000。那对应的从库节点应该是6000和6001,我们可以利用工具查看。六、集群高可用性测试首先模拟主节点7000挂掉,按照预期从节点6001选为主节点。1、进入服务里停止redis70002、查看集群信息,cmd进入redis-trib.rb文件目录,执行redis-trib.rb check 127.0.0.1:6001可以看到从节点6001(S)已经变为主节点(M)3、取值测试 get age,原来是从7000节点里取到的,现在任然取的到,只是是从6001里取到4、重新启动redis7000,重新查看集群信息7000变为从节点。七、额外添加集群节点集群在运行了一段时间之后,由于需求的变动,我们会增加或者删减集群节点。主从节点增加选择思路:(1)缓存服务存储压力大,增加主节点,横向扩展 (2)保证redis集群高可用,多增加从节点。任何一条主从线下的redis服务全部故障,则导致集群不可用。1、新部署一个redis服务端口70032、把7003加入集群,cmd进入redis-trib.rb文件目录,执行redis-trib.rb add-node 127.0.0.1:7003 127.0.0.1:70003、查看集群信息redis-trib.rb check 127.0.0.1:6001能看到7003已经被加入集群,作为主节点(M),但是这时候7003并没有分槽。没有分配哈希槽的话表示就没有存储数据的能力。4、分配槽点随便进入一个客户端,redis-trib.rb reshard 127.0.0.1:7001(1)问我们要移动多少个槽点,我们按均摊来分,大概是4000个节点。(2)输入4000,要我们输入接受节点的ID,就是7003的ID:28d7e06a951e82d8eca485fe465947100d78090a(3)接着输入all回车后提示输入yes,就会从当前的其他主节点里抽取4000个槽过来。【错误帮助】如果过程中出现槽错误分别登录7002和7003redis执行cluster setslot 11237 stable5、为7003添加从节点6003(1)、部署redis6003(2)、进入redis-trib.rb目录执行redis-trib.rb add-node –slave 127.0.0.1:6003 127.0.0.1:7003八、移除集群节点1、删除从节点,因为没有分配哈希槽,所以直接删除。IP:端口 IDredis-trib.rb del-node 127.0.0.1:6003 241c77920bde9952fbf3cb38f7b3085c946b03242、删除主节点,因为主节点有分槽,先把槽移动至其他主节点,再删除 ...

January 29, 2019 · 1 min · jiezi

浏览器的缓存机制

缓存类型缓存在宏观上可以分成两类:私有缓存和共享缓存。共享缓存就是那些能被各级代理缓存的缓存。私有缓存就是用户专享的,各级代理不能缓存的缓存。微观上可以分下面几类:浏览器缓存缓存存在的意义就是当用户点击back按钮或是再次去访问某个页面的时候能够更快的响应。尤其是在多页应用的网站中,如果你在多个页面使用了一张相同的图片,那么缓存这张图片就变得特别的有用。浏览器先向代理服务器发起Web请求,再将请求转发到源服务器。其中浏览器缓存包括强缓存和协商缓存,下文有详细介绍。本文主要侧重点就是针对于浏览器缓存。2.CDN缓存 CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。通常情况下,浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,浏览器和服务器之间的缓存机制,在这种架构下同样适用。3.代理服务器缓存 代理服务器是浏览器和源服务器之间的中间服务器,代理转发响应时,缓存代理会预先将资源的副本(缓存)保存到代理服务器上。当代理再次接收到对相同资源的请求时,就可以不从源服务器那里获取资源,而是将之前缓存的资源作为响应返回。4.数据库缓存 数据库缓存是指,当web应用的关系比较复杂,数据库中的表很多的时候,如果频繁进行数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。5.应用层缓存 应用层缓存是指我们在代码层面上做的缓存。通过代码逻辑,把曾经请求过的数据或资源等,缓存起来,再次需要数据时通过逻辑上的处理选择可用的缓存的数据。浏览器缓存浏览器缓存就是把一个已经请求过的 web 资源拷贝一份存储在浏览器中,当下次请求相同的资源时,浏览器会根据缓存机制决定直接使用副本响应访问请求还是再次向服务器发送请求。优点1.减少了冗余的数据传输2.减少了服务器的负担,大大提升了网站的性能3.加快了客户端加载网页的速度缓存位置从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。Service WorkerMemory CacheDisk CachePush Cache强缓存和协商缓存两者的主要区别是使用本地缓存的时候,是否需要向服务器验证本地缓存是否依旧有效。强缓存浏览器在加载资源时,会先根据本地缓存资源的 header 中的信息判断是否命中强缓存,如果命中则直接使用缓存中的资源不会再向服务器发送请求。协商缓存当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304 ,告诉浏览器资源未更新,可使用本地的缓存。缓存优先级禁用浏览器缓存在 Network 中有个 Disable cache ,钩上就可以了,钩上后浏览器会忽略掉文档过期验证和服务器再验证的过程,直接向服务器请求最新的资源。用户行为影响地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

January 23, 2019 · 1 min · jiezi

性能优化

在并发量一定的情况下如何对系统响应时间进行详细分析分析步骤1.1 在关键点位添加日志信息 -> 缩小目标范围a) 主要函数耗时b) 访问外部系统耗时:DB、MQ、Cache、FileSystem、RPC、HTTP等c) 接口内不同逻辑耗时百分比/绝对值1.2 详细分析瓶颈出现原因a) 技术层面:优先考虑b) 业务逻辑:业务逻辑改造一般影响较大、耗时较长,优先级低1.3 针对性解决问题一旦定位了问题原因,解决问题的方法都相当容易技术层面2.1 代码实现a) 串行逻辑是否可以并行化b) 串行请求是否可以批量(batch)请求c) SQL是否需要优化d) 算法复杂度是否需要优化e) 语言核心库是否提供了性能更高的使用方式f) 引用的第三方库是否存在性能问题g) 每次外部请求是否都重新建立连接2.2 内核/硬件层面a) CPU使用率b) 内存使用率c) 磁盘IO状况d) 网络状况d) 瓶颈是否由调用的内核函数引起?该函数是如何工作的;新版本内核是否已经对此优化;如何调整使用方式可以更高效2.3 日志线程会争夺日志锁,在高并发情况下,同步写日志很影响性能。异步写又可能引起OOM。业务逻辑随着业务的增长,接口负担的功能原来越复杂,逻辑链路越来越长,事务越来越大,性能越来越差,越来越没办法维护。(如果有人负责把控、从相对长远些的角度设计系统的迭代,这种情况本是可以避免的)优化办法只有一个就是:保留主链路,旁支链路异步化。常用工具4.1 内核a) CPU: top、vmstat htop w uptime dstatb) MEM: top、freec) disk IO: iostat例: iostat -kx 1 //每秒统计一次ioiotop -o //查看磁盘使用率较高的进程d) 网络流量:sar -n DEV 1 3 //每秒统计一次所有网卡流量,共3次网络抓包: tmpdump,可配合tmptrace、wireshark分析TCP连接状况:netstat -an | grep TIME_WAIT //client端近期关闭tcp连接数量e) 查看/proc/xxx中的系统详细信息小知识a) 网络抖动引起tcp重传,一般内部系统之间的调用,linux设置的重传时间间隔为至少200ms以上。b) 服务之间通过网络调用,网络开销在500us ~ 2msc) 机械磁盘寻址: 20msd) 磁盘的某page中如果存在数据,程序写此page时如果不会填充整个page,内核会先载入整个page,再输出整个page,保证磁盘数据不丢。这会导致一次被动读磁盘,性能损耗会很大。否则会直接写入磁盘高速缓存,间隔固定时间刷盘一次(5s)。e) 日志使用seaslog buffer随着系统并发量的增加,系统响应时间会逐步下降甚至雪崩,一般是由多线程之间、模块之间、子系统之间争夺资源(锁)引起的。

January 21, 2019 · 1 min · jiezi

MyBatis 缓存详解

参考文档:MyBatis官方文档MyBatis的缓存主要分为两种一级缓存也叫本地缓存(local cache)和二级缓存(second level cache)。一级缓存、本地缓存一级缓存是session级缓存,即缓存只在session范围生效。每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询语句本身都会被保存在本地缓存中,那么,相同的查询语句和相同的参数所产生的更改就不会二度影响数据库了。本地缓存会被增删改、提交事务、关闭事务以及关闭 session 所清空。默认情况下,本地缓存数据可在整个 session 的周期内使用,这一缓存需要被用来解决循环引用错误和加快重复嵌套查询的速度,所以它不可以被禁用掉,但是你可以设置 localCacheScope=STATEMENT 表示缓存仅在语句执行时有效。注意,如果 localCacheScope 被设置为 SESSION,那么 MyBatis 所返回的引用将传递给保存在本地缓存里的相同对象。对返回的对象(例如 list)做出任何更新将会影响本地缓存的内容,进而影响存活在 session 生命周期中的缓存所返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。手动清空本地缓存:void clearCache()二级缓存二级缓存是namespace级缓存,二级缓存会在同一 namespace中生效。默认情况下,MyBatis 3 没有开启二级缓存,要开启二级缓存,你需要在你的 SQL 映射文件(mapper.xml)中添加一行:<cache/>其实还需要在配置文件中把mybatis.configuration.cache-enabled设置为true(默认为true),若添加<cache/>标签后缓存不生效,可以检查是否将其设置为了false字面上看就是这样。这个简单语句的效果如下:映射语句文件中的所有 select 语句将会被缓存。映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。所有的这些属性都可以通过缓存元素的属性来修改。比如:<cache eviction=“FIFO” flushInterval=“60000” size=“512” readOnly=“true”/>这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会 导致冲突。可用的收回策略有:LRU – 最近最少使用的:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的缓存回收策略是 LRU。flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。若在SqlSession关闭时,SqlSession对应的本地缓存会自动转化为二级缓存。自定义缓存使用自定缓存,只需要实现MyBatis的Cache接口并在<cache/>中配置缓存类型:<cache type=“com.domain.something.MyCustomCache”/>自定义缓存没有使用过,如果大家有兴趣可以参考MyBatis官方文档自定义缓存部分后记这篇文章主要由MyBatis官方文档整理而来,用于记录我的学习过程,作为2019年的开始,以后的学习都需要有产出物,否则学了之后很快就会忘记。 ...

January 17, 2019 · 1 min · jiezi

网络协议 18 - CDN:家门口的小卖铺

【前五篇】系列文章传送门:网络协议 13 - HTTPS 协议:加密路上无尽头网络协议 14 - 流媒体协议:要说爱你不容易网络协议 15 - P2P 协议:小种子大学问网络协议 16 - DNS 协议:网络世界的地址簿网络协议 17 - HTTPDNS:私人定制的 DNS 服务 到现在为止,我们基本上已经了解了网络协议中的大部分常用协议,对于整个 HTTP 请求流程也较为熟悉了。从无到有后,我们就要考虑如何优化“有”这个过程,也就是我们常见的请求优化。而现在的技术栈中,CDN 是最常用的一种方式。 在了解 CDN 前,我们可以先了解下现代社会的物流配送。 例如我们去电商网站下单买东西,这个东西一定要从电商总部的中心仓库送过来吗?在电商刚兴起的时候,所有的配送都是从中心仓库发货,所以买家可能要很久才能收到货。但是后来电商网站的物流系统学聪明了,他们在全国各地建立了很多仓库,而不是只有总部的中心仓库才可以发货。 电商网站根据统计大概知道,北京、上海、广州、深圳、杭州等地,每天能够卖出去多少书籍、纸巾、包、电器等存放期较长的商品,就将这些商品分布存放在各地仓库中,客户一下单,就从临近的仓库发货,大大减少了运输时间,提高了用户体验。 同样的,互联网也借鉴了“就近配送”这个思路。CDN 就近配送 全球有那么多的数据中心,无论在哪里上网,临近不远的地方基本上都有数据中心。可以在每个数据中心里部署几台机器,形成一个缓存集群来缓存部分热数据,这样用户访问数据的是,就可以就近访问了。 这些分布在各个地方的各个数据中心的节点,我们一般称为边缘节点。 由于边缘节点数目比较多,但是每个集群规模比较小,不可能缓存下来所有东西,因而可能无法命中,这样就会在边缘节点之上,形成了区域节点。 区域节点规模较大,缓存的数据也较多,命中的概率也就更大。而在区域节点之上是中心节点,规模更大,缓存数据更多。 就这样,在这样一层层的节点中缓存数据,提高响应速度。但是所有的节点都没有缓存数据,就只有进行回源网站访问了。 如上图,就是 CDN 的分发系统的架构。CDN 系统的缓存,是一层层的,能不访问源数据,就不访问。这也是电商网站物流系统的思路,广州找不到,找华南局,华南局找不到,再找南方局。 有了这个分发系统之后,客户端如何找到相应的边缘节点进行访问呢? 还记得咱们之前了解的基于 DNS 的全局负载均衡吗?这个负载均衡主要用来选择一个就近的相同运营商的服务器进行访问。 同样的,CDN 分发网络也可以用相同的思路选择最合适的边缘节点。 如上图,CDN 的负载均衡流程图。1)没有 CDN 的情况(图中虚线部分)。用户向浏览器输入 www.web.com 这个域名,客户端访问本地 DNS 服务器的时候,如果本地 DNS 服务器有缓存,则返回网站的地址。如果没有,递归查询到网站的权威 DNS 服务器,这个权威 DNS 服务器是负责 web.com 的,它会返回网站的 IP 地址。本地 DNS 服务器缓存下 IP 地址,将 IP 地址返回,然后客户端直接访问这个 IP 地址,就访问到了网站。2)有 CDN 的情况(图中实线部分)。此时,在 web.com 这个权威 DNS 服务器上,会设置一个 CNAME 别名,指向另外一个域名 www.web.cdn.com,返回给本地 DNS 服务器。 当本地 DNS 服务器拿到这个新的域名时,需要继续解析这个新的域名。这个时候,再访问的就不是 web.com 这个权威 DNS 服务器了,而是 web.cdn.com 的权威 DNS 服务器,这是 CDN 自己的权威 DNS 服务器,在这个服务器上,还是会设置一个 CNAME,指向另外一个域名,也就是 CDN 网络的全局负载均衡器。 接下来,本地 DNS 服务器去请求 CDN 的全局负载均衡器解析域名。全局负载均衡器会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户 IP 地址,判断哪一台服务器距用户最近;用户所处的运营商;根据用户所请求的 URL 中携带的内容名词,判断哪一台服务器上有用户所需的内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。 基于以上这些条件,进行综合分析之后,全局负载均衡器会返回一台缓存服务器的 IP 地址。 本地 DNS 服务器缓存这个 IP 地址,然后将 IP 返回给客户端,客户端去访问这个边缘节点,下载资源。 缓存服务器响应用户请求,将用户所需内容传送给用户。如果这台缓存服务器上没有用户想要的内容,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器,将内容拉到本地。CDN 缓存内容 保质期长的日用品因为不容易过期,因此比较容易缓存。同样的,互联网中的静态页面、图片等,几乎不怎么改变,所以也适合缓存。而像生鲜之类的保存时间较短的,对应互联网中的动态资源,就需要用到动态 CDN 。静态资源缓存 还记得上图这个接入层缓存的架构吗?在进入数据中心的时候,我们希望通过最外层接入层的缓存,将大部分静态资源的访问拦在边缘。而 CDN 则更进一步,将这些静态资源缓存到离用户更近的数据中心外。总体来说,就是缩短用户的“访问距离”。离客户越近,客户访问性能越好,时延越低。流媒体 CDN 在静态内容中,流媒体也大量使用了 CDN。 CDN 支持流媒体协议。例如前面讲过的 RTMP 协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。 对于静态页面来讲,内容的分发往往采取拉取的方式。也即,当发现缓存未命中的是,再去上一级进行拉取。 这个方式对于流媒体就不合适了。流媒体数据量大,如果出现回源,压力会比较大,所以往往采取主动推送的模式,将热点数据主动推送到边缘节点。 对于流媒体来讲,很多 CDN 还提供预处理服务。也就是在文件分发之前,进行一定的处理。例如将视频转换成不同的码流,以适应不同的网络带宽的用户需求。再如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。这就是我们常见的,超清、标清、流畅等。 除此之外,流媒体 CDN 还有个关键的防盗链问题。因为视频要花大价钱买版权,如果流媒体被其他网站盗走,在其他网站的播放,那损失就大了。 对于防盗链问题,最常用也最简单的方法就是利用 HTTP 头的 refer 字段。当浏览器发送请求的时候,一般会带上 refer。告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果 refer 信息不是来自本站,就阻止访问或者跳到其它链接。 refer 的机制相对比较容易破解,所以还需要其它的机制配合。 一种常用的机制是时间戳防盗链。使用 CDN 的管理员可以在配置界面上,和 CDN 厂商约定一个加密字符串。 客户端访问时,取出当前的时间戳、要访问的资源极其路径,联通加密字符串进行前面算法得到一个字符串,然后生成一个下载链接,带上这个前面字符串和截止时间戳去访问 CDN。 在服务端,取出过期时间,和当前 CDN 节点时间进行比较,确认请求是否过期。然后 CDN 服务端根据请求的资源及路径、时间戳、和约定的加密字符串进行签名。只有签名和客户端发送的一致,才会将资源返回给客户。动态资源缓存 对于动态资源,用到动态 CDN。动态 CDN 主要有两种模式:1)“生鲜超市模式”,也就是边缘计算模式。 既然数据是动态生成的,所以数据的逻辑计算和存储,也相应的放在边缘的节点。其中定时从源数据那里同步存储的数据,然后在边缘节点进行计算得到结果。 这种方式很像现在的生鲜超市。新鲜的海鲜大餐是动态的,很难事先做好缓存,因而将生鲜超市放在你家旁边,既能够送货上门,也能够现场烹饪。这就是边缘计算的一种体现。2)“冷链运输模式”,也就是路径优化模式。数据不是在边缘计算生成的,而是在源站生成的,但是数据的下发则可以通过 CDN 的网络,对路径进行优化。 因为 CDN 节点较多,能够找到离源站很近的边缘节点,也能找到离用户很近的边缘节点。中间的链路完全由 CDN 来规划,选择一个更加可靠的路径,使用类似专线的方式进行访问。 除此之外,这些资源进行传输的时候,由于 TCP 的流量控制和拥塞控制,可以在 CDN 加速网络中调整 TCP 的参数,使得 TC 可以更加激进的传输数据。 所有这些手段就像冷链传输,优化整个物流运输,全程冷冻高速运输。不管是生鲜从你家旁边超市送过去,还是从产地运送,保证到你家是新鲜的。小结CDN 和电商系统的分布式仓储系统一样,分为中心节点、区域节点、边缘节点,从而将数据缓存在离用户最近的位置。CDN 最擅长的缓存是缓存静态数据。除此之外还可以缓存流媒体数据。这时候要注意防盗链问题。它也支持动态数据的缓存,一种是边缘计算,另一种是链路优化。参考:What is a CDN?;[维基百科 - Content delivery network](https://en.wikipedia.org/wiki...;刘超 - 趣谈网络协议系列课; ...

January 2, 2019 · 1 min · jiezi

网络协议 17 - HTTPDNS:私人定制的 DNS 服务

【前五篇】系列文章传送门:网络协议 12 - HTTP 协议:常用而不简单网络协议 13 - HTTPS 协议:加密路上无尽头网络协议 14 - 流媒体协议:要说爱你不容易网络协议 15 - DNS 协议:网络世界的地址簿网络协议 16 - HTTPDNS:私人定制的 DNS 服务 全球统一的 DNS 是很权威,但是我们都知道“适合自己的,才是最好的”。很多时候,标准统一化的 DNS 并不能满足我们定制的需求,这个时候就需要 HTTPDNS 了。 上一节我们知道了 DNS 可以根据名称查地址,也可以针对多个地址做负载均衡。然而,我们信任的地址簿也会存在指错路的情况。明明离你 500 米就有个吃饭的地方,非要把你推荐到 5 公里外。为什么会出现这样的情况呢? 还记得吗?由我们发出请求解析 DNS 的时候,首先会连接到运营商本地的 DNS 服务器,由这个服务器帮我们去整棵 “DNS 树” 上进行解析,然后将解析的结果返回给客户端。但是本地的 DNS 服务器,作为一个本地导游,往往会有自己的“小心思”。传统 DNS 存在的问题1)域名缓存问题 它可以在本地做一个缓存。也就是说,不是每一个请求,它都会去访问权威 DNS 服务器,而是把访问过一次的结果缓存到本地,当其他人来问的时候,直接返回缓存的内容。 这就相当于导游去过一个饭店,自己记住了地址,当有一个游客问的时候,他就凭记忆回答了,不用再去查地址簿。这样会存在一个问题,游客问的那个饭店如果已经搬走了,然而因为导游没有刷新“记忆缓存”,导致游客白跑一趟。 另外,有的运营商会把一些静态页面,缓存到本运营商的服务器内,这样用户请求的时候,就不用跨运营商进行访问,既加快了速度,也减少了运营商直接流量计算的成本。也就是说,在域名解析的时候,不会将用户导向真正的网站,而是指向这个缓存的服务器。 缓存的问题,很多情况下是看不出问题的,但是当页面更新,用户访问到老的页面,问题就出来了。 再就是本地的缓存,往往使得全局负载均衡失败。上次进行缓存的时候,缓存中的地址不一定是客户此次访问离客户最近的地方,如果把这个地址返回给客户,就会让客户绕远路了。2)域名转发问题 还记得我们域名解析的过程吗?捂脸是本地域名解析,还是去权威 DNS 服务器中查找,都可以认为是一种外包形式。有了请求,直接转发给其他服务去解析。如果转发的是权威 DNS 服务器还好说,但是如果因为“偷懒”转发给了邻居服务器去解析,就容易产生跨运营商访问的问题。 这就好像,如果 A 运营商的客户,访问自己运营商的 DNS 服务器,A 运营商去权威 DNS 服务器查询的话,会查到客户的 A 运营商的,返回一个部署在 A 运营商的网站地址,这样针对相同运营商的访问,速度就会快很多。 但是如果 A 运营商偷懒,没有转发给权威 DNS ,而是转发给了 B 运营商,让 B 运营商再去权威 DNS 服务器查询,这样就会让权威服务器误认为客户是 B 运营商的,返回一个 B 运营商的服务器地址,导致客户每次都要跨运营商访问,访问速度就会慢下来。3)出口 NAT 问题 前面了解网关的时候,我们知道,出口的时候,很多机房都会配置 NAT,也就是网络地址转换,使得从这个网关出去的包,都换成新的 IP 地址。 这种情况下,权威 DNS 服务器就没办法通过请求 IP 来判断客户到底是哪个运营商的,很有可能误判运营商,导致跨运营商访问。4)域名更新问题 本地 DNS 服务器是由不同地区、不同运营商独立部署的。对域名解析缓存的处理上,实现策略也有区别。有的会偷懒,忽略域名解析结构的 TTL 时间限制,在权威 DNS 服务器解析变更的时候,解析结果在全网生效的周期非常漫长。但是有的场景,在 DNS 的切换中,对生效时间要求比较高。 例如双机房部署的是,跨机房的负载均衡和容灾多使用 DNS 来做。当一个机房出问题之后,需要修改权威 DNS,将域名指向新的 IP 地址。但是如果更新太慢,很多用户都会访问一次。5)解析延迟问题 从 DNS 的查询过程来看,DNS 的查询过程需要递归遍历多个 DNS 服务器,才能获得最终的解析结果,这带来一定的延时,甚至会解析超时。 上面总结了 DNS 的五个问题。问题有了,总得有解决办法,就像因为 HTTP 的安全问题,才火了 HTTPS 协议一样,对应的,也有 HTTPDNS 来解决上述 DNS 出现的问题。HTTPDNS什么是 HTTPDNS ?其实很简单:HTTPDNS 是基于 HTTP 协议和域名解析的流量调度解决方案。它不走传统的 DNS 解析,而是自己搭建基于 HTTP 协议的 DNS 服务器集群,分布在多个地点和多个运营商。当客户端需要 DNS 解析的时候,直接通过 HTTP 请求这个服务器集群,得到就近的地址。 这就相当于每家基于 HTTP 协议,自己实现自己的域名解析,做一个自己的地址簿,而不使用统一的地址簿。但是我们知道,域名解析默认都是走 DNS 的,因而使用 HTTPDNS 需要绕过默认的 DNS 路径,也就不能使用默认的客户端。**使用 HTTPDNS 的,往往是手机应用,需要在手机端嵌入支持 HTTPDNS 的客户端 SDK。HTTPDNS 的工作流程 接下来,我们一起来认识下 HTTPDNS 的工作流程。 HTTPDNS 会在客户端的 SDK 里动态请求服务端,获取 HTTPDNS 服务器的 IP 列表,缓存在本地。随着不断地解析域名,SDK 也会在本地缓存 DNS 域名解析的结果。 当手机应用要访问一个地址的时候,首先看是否有本地的缓存,如果有直接返回。这个缓存和本地 DNS 的缓存不一样的是,这个是手机应用自己做的,而非整个运营商统一做。如何更新以及何时更新缓存,手机应用的客户端可以和服务器协调来做这件事情。 如果本地没有,就需要请求 HTTPDNS 的服务器,在本地 HTTPDNS 服务器的 IP 列表中,选择一个发出 HTTP 请求,获取一个要访问的网站的 IP 列表。请求的方式是这样的:curl http://123.4.5.6/d?dn=c.m.cnb… 手机客户端之道手机在哪个运营商、哪个地址。由于是直接的 HTTP 通信,HTTPDNS 服务器能够准确知道这些信息,因而可以做精准的全局负载均衡。 上面五个问题,归结起来就两大问题。一是解析速度和更新速度的平衡问题,二是智能调度的问题。HTTPDNS 对应的解决方案是 HTTPDNS 的缓存设计和调度设计。HTTPDNS 的缓存设计 解析 DNS 过程复杂,通信此时多,对解析速度造成很大影响。为了加快解析,因而有了缓存,但是这又会产生缓存更新速度不及时的问题。最要命的是,这两个方面都掌握在别人手中,也就是本地 DNS 服务器手中,它不会为你定制,作为客户端干着急也没办法。 而 HTTPDNS 就是将解析速度和更新速度全部掌控在自己手中。 一方面,解析的过程,不需要本地 DNS 服务递归的调用一大圈,一个 HTTP 的请求直接搞定。要实时更新的时候,马上就能起作用。另一方面,为了提高解析速度,本地也有缓存,缓存是在客户端 SDK 维护的,过期时间、更新时间,都可以自己控制。HTTPDNS 的缓存设计策略也是咱们做应用架构中常用的缓存设计模式,也即分为客户端、缓存、数据源三层。对于应用架构来讲,就是应用、缓存、数据库。常见的是 Tomcat、Redis、Mysql;对于 HTTPDNS 来讲,就是手机客户端、DNS 缓存、HTTPDNS 服务器。只要是缓存模式,就存在缓存的过期、更新、不一致的问题,解决思路也是相似的。例如,DNS 缓存在内存中,也可以持久化到存储上,从而 APP 重启之后,能够尽快从存储中加载上次累积的经常访问的网站的解析结果,就不需要每次都全部解析一遍,再变成缓存。这有点像 Redis 是基于内存的缓存,但是同样提供持久化的能力,使得重启或者主备切换的时候,数据不会完全丢失。SDK 中的缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的几率,则会发起一次解析,保证缓存记录是更新的。解析可以同步进行,也就是直接调用 HTTPDNS 的接口,返回最新的记录,更新缓存。也可以异步进行,添加一个解析任务到后台,由后台任务调用 HTTPDNS 的接口。同步更新的优点是实时性好,缺点是如果有多个请求都发现过期的时候,会同时请求 HTTPDNS 多次,造成资源浪费。同步更新的方式对应到应用架构缓存的 Cache-Aside 机制,也就是先读缓存,不命中读数据库,同时将结果写入缓存。异步更新的优点是,可以将多个请求都发现过期的情况,合并为一个对于 HTTPDNS 的请求任务,只执行一次,减少 HTTPDNS 的压力。同时,可以在即将过期的时候,就创建一个任务进行预加载,防止过期之后再刷新,称为预加载。它的缺点是,当前请求拿到过期数据的时候,如果客户端允许使用过期时间,需要冒一次风险。这次风险是指,如果过期的请求还能请求,就没问题,如果不能请求,就会失败一次,等下次缓存更新后,才能请求成功。异步更新的机制,对应到应用架构缓存的 Refresh-Ahead 机制,即业务仅仅访问缓存,当过期的时候定期刷新。在著名的应用缓存 Guava Cache 中,有个 RefreshAfterWrite 机制,对于并发情况下,多个缓存访问不命中从而引发并发回源的请求,可以采取只有一个请求回源的模式。在应用架构的缓存中,也常常用数据预热或者预加载的机制。HTTPDNS 的调度设计由于客户端嵌入了 SDK,因而就不会因为本地 DNS 的各种缓存、转发、NAT,让权威 DNS 服务器误会客户端所在的位置和运营商,从而可以拿到第一手资料。在客户端,可以知道手机是哪个国家、哪个运营商、哪个省、甚至是哪个市,HTTPDNS 服务端可以根据这些信息,选择最佳的服务节点返回。如果有多个节点,还会考虑错误率、请求时间、服务器压力、网络状态等,进行综合选择,而非仅仅考虑地理位置。当有一个节点宕机或者性能下降的时候,可以尽快进行切换。要做到这一点,需要客户端使用 HTTPDNS 返回的 IP 访问业务应用。客户端的 SDK 会收集网络请求数据,如错误率、请求时间等网络请求质量数据,并发送到统计后台,进行分析、聚合,以此查看不同 IP 的服务质量。在服务端,应用可以通过调用 HTTPDNS 的管理接口,配置不同服务质量的优先级、权重。HTTPDNS 会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那些优质的、时延低的 IP 地址。HTTPDNS 通过智能调度之后返回的结果,也会缓存在客户端。为了不让缓存使得调度失真,客户端可以根据不同的移动网络运营商的 SSID 来分维度缓存。不同的运营商解析出来的结果会不同。小结传统 DNS 会因为缓存、转发、NAT 等问题导致客户端误会自己所在的位置和运营商,从而影响流量的调度;HTTPDNS 通过客户端 SDK 和服务端,通过 HTTP 直接调用解析 DNS 的方式,绕过了传统 DNS 的缺点,实现了智能的调度。参考:HTTPDNS 的原理;刘超 - 趣谈网络协议系列课; ...

December 31, 2018 · 2 min · jiezi

重排与重绘

原文地址:http://www.cun-xu.cn/index.ph…在页面的生命周期中,一些效果的交互都有可能发生重排(Layout)和重绘(Painting),这些都会使我们付出高额的性能代价。浏览器从下载文件至本地到显示页面是个复杂的过程,这里包含了重绘和重排。通常来说,渲染引擎会解析HTML文档来构建DOM树,与此同时,渲染引擎也会用CSS解析器解析CSS文档构建CSSOM树。接下来,DOM树和CSSOM树关联起来构成渲染树(RenderTree),这一过程称为Attachment。然后浏览器按照渲染树进行布局(Layout),最后一步通过绘制显示出整个页面。其中重排和重绘是最耗时的部分,一旦触发重排,我们对DOM的修改引发了DOM几何元素的变化,渲染树需要重新计算,而重绘只会改变vidibility、outline、背景色等属性导致样式的变化,使浏览器需要根据新的属性进行绘制。更比而言,重排会产生比重绘更大的开销。所以,我们在实际生产中要严格注意减少重排的触发。触发重排的操作主要是几何因素:1.页面第一次渲染在页面发生首次渲染的时候,所有组件都要进行首次布局,这是开销最大的一次重排。2.浏览器窗口尺寸改变3.元素位置和尺寸发生改变的时候4.新增和删除可见元素5.内容发生改变(文字数量或图片大小等等)6.元素字体大小变化。7.激活CSS伪类(例如::hover)。8.设置style属性9.查询某些属性或调用某些方法。比如说:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight除此之外,当我们调用getComputedStyle方法,或者IE里的currentStyle时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。触发重绘的操作主要有:vidibility、outline、背景色等属性的改变我们应当注意的是:重绘不一定导致重排,但重排一定会导致重绘。那么我们可以采取哪些措施来避免或减少重排带来的巨大开销呢?1.分离读写操作div.style.top = “10px”;div.style.bottom = “10px”;div.style.right = “10px”;div.style.left = “10px”;console.log(div.offsetWidth);console.log(div.offseHeight);console.log(div.offsetRight);console.log(div.offsetLeft);原来的操作会导致四次重排和四次重绘,变换顺序之后只会触发一次重排在第一个console的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。因为渲染队列本来就是空的,所以剩下的console并没有触发重排,仅仅拿值而已。2.样式集中改变通过class和cssText进行集中改变样式未进行优化的代码是这样的://badvar left = 10;var top = 10;el.style.left = left + “px”;el.style.top = top + “px”;虽然现在大部分现代浏览器都会有Flush队列进行渲染队列优化,但是有些老版本的浏览器比如IE6这样的坑货效率依然低下:这时我们就可以通过上面所说的利用class和cssText属性集中改变样式//goodel.className += " className";//orel.style.cssText += “; left: " + left + “px; top: " + top + “px;";3. 缓存布局信息// bad 强制刷新 触发两次重排div.style.left = div.offsetLeft + 1 + ‘px’;div.style.top = div.offsetTop + 1 + ‘px’;// good 缓存布局信息 相当于读写分离var curLeft = div.offsetLeft;var curTop = div.offsetTop;div.style.left = curLeft + 1 + ‘px’;div.style.top = curTop + 1 + ‘px’;复制代码4. 将DOM离线DOM离线化一旦我们给元素设置display:none时,元素不会存在于渲染树中,相当于将其从页面“拿掉”,我们之后的操作将不会触发重排和重绘,这叫做DOM的离线化。dom.display = ’none’// 修改dom样式dom.display = ‘block’复制代码通过使用DocumentFragment创建一个dom碎片,在它上面批量操作dom,操作完成之后,再添加到文档中,这样只会触发一次重排。复制节点,在副本上工作,然后替换它!5. 将position属性设置为absolute或fixedposition属性为absolute或fixed的元素,重排开销比较小,不用考虑它对其他元素的影响6. 优化动画可以把动画效果应用到position属性为absolute或fixed的元素上,这样对其他元素影响较小动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:比如实现一个动画,以1个像素为单位移动这样最平滑,但是Layout就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。启用GPU加速GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。/** 根据上面的结论* 将 2d transform 换成 3d* 就可以强制开启 GPU 加速* 提高动画性能*/div {transform: translate3d(10px, 10px, 0);}娘滴,终于写完了,肩膀子疼的我,得要得肩周炎了。 ...

December 25, 2018 · 1 min · jiezi

缓存使用

1,使用nginx代理缓存2,使用304状态码,springboot项目使用shadowEtagFilter3,使用springboot的enableCacheing注解实现缓存

December 24, 2018 · 1 min · jiezi

spring cache 配置缓存存活时间

Spring Cache @Cacheable本身不支持key expiration的设置,以下代码可自定义实现Spring Cache的expiration,针对Redis、SpringBoot2.0。直接上代码:@Service@Configurationpublic class CustomCacheMng{ private Logger logger = LoggerFactory.getLogger(this.getClass()); // 指明自定义cacheManager的bean name @Cacheable(value = “test”,key = “‘obj1’",cacheManager = “customCacheManager”) public User cache1(){ User user = new User().setId(1); logger.info(“1”); return user; } @Cacheable(value = “test”,key = “‘obj2’”) public User cache2(){ User user = new User().setId(1); logger.info(“2”); return user; } // 自定义的cacheManager,实现存活2天 @Bean(name = “customCacheManager”) public CacheManager cacheManager( RedisTemplate<?, ?> redisTemplate) { RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(2)); return new RedisCacheManager(writer, config); } // 提供默认的cacheManager,应用于全局 @Bean @Primary public CacheManager defaultCacheManager( RedisTemplate<?, ?> redisTemplate) { RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory()); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); return new RedisCacheManager(writer, config); }} ...

December 21, 2018 · 1 min · jiezi

分布式缓存Redis使用心得

一、缓存在系统中用来做什么?少量数据存储,高速读写访问。通过数据全部in-momery 的方式来保证高速访问,同时提供数据落地的功能,实际这正是Redis最主要的适用场景。海量数据存储,分布式系统支持,数据一致性保证,方便的集群节点添加/删除。Redis3.0以后开始支持集群,实现了半自动化的数据分片,不过需要smart-client的支持。二、从不同的角度来详细介绍redis网络模型:Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select,对于单纯只有IO操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞住的。内存管理:Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片,Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据),这点上Redis更适合作为存储而不是cache。数据一致性问题:在一致性问题上,个人感觉redis没有memcached实现的好,Memcached提供了cas命令,可以保证多个并发访问操作同一份数据的一致性问题。 Redis没有提供cas 命令,并不能保证这点,不过Redis提供了事务的功能,可以保证一串命令的原子性,中间不会被任何操作打断。支持的KEY类型:Redis除key/value之外,还支持list,set,sorted set,hash等众多数据结构,提供了KEYS进行枚举操作,但不能在线上使用,如果需要枚举线上数据,Redis提供了工具可以直接扫描其dump文件,枚举出所有数据,Redis还同时提供了持久化和复制等功能。客户端支持:redis官方提供了丰富的客户端支持,包括了绝大多数编程语言的客户端,比如我此次测试就选择了官方推荐了Java客户端Jedis.里面提供了丰富的接口、方法使得开发人员无需关系内部的数据分片、读取数据的路由等,只需简单的调用即可,非常方便。数据复制:从2.8开始,Slave会周期性(每秒一次)发起一个Ack确认复制流(replication stream)被处理进度, Redis复制工作原理详细过程如下:如果设置了一个Slave,无论是第一次连接还是重连到Master,它都会发出一个SYNC命令;当Master收到SYNC命令之后,会做两件事:a) Master执行BGSAVE:后台写数据到磁盘(rdb快照);b) Master同时将新收到的写入和修改数据集的命令存入缓冲区(非查询类);当Master在后台把数据保存到快照文件完成之后,Master会把这个快照文件传送给Slave,而Slave则把内存清空后,加载该文件到内存中;而Master也会把此前收集到缓冲区中的命令,通过Reids命令协议形式转发给Slave,Slave执行这些命令,实现和Master的同步;Master/Slave此后会不断通过异步方式进行命令的同步,达到最终数据的同步一致;需要注意的是Master和Slave之间一旦发生重连都会引发全量同步操作。但在2.8之后,也可能是部分同步操作。2.8开始,当Master和Slave之间的连接断开之后,他们之间可以采用持续复制处理方式代替采用全量同步。Master端为复制流维护一个内存缓冲区(in-memory backlog),记录最近发送的复制流命令;同时,Master和Slave之间都维护一个复制偏移量(replication offset)和当前Master服务器ID(Masterrun id)。当网络断开,Slave尝试重连时:a. 如果MasterID相同(即仍是断网前的Master服务器),并且从断开时到当前时刻的历史命令依然在Master的内存缓冲区中存在,则Master会将缺失的这段时间的所有命令发送给Slave执行,然后复制工作就可以继续执行了;b. 否则,依然需要全量复制操作。读写分离:redis支持读写分离,而且使用简单,只需在配置文件中把redis读服务器和写服务器进行配置,多个服务器使用逗号分开如下:水平动态扩展:历时三年之久,终于等来了期待已由的Redis 3.0。新版本主要是实现了Cluster的功能,增删集群节点后会自动的进行数据迁移。实现 Redis 集群在线重配置的核心就是将槽从一个节点移动到另一个节点的能力。因为一个哈希槽实际上就是一些键的集合, 所以 Redis 集群在重哈希(rehash)时真正要做的,就是将一些键从一个节点移动到另一个节点。数据淘汰策略:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略:volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰no-enviction(驱逐):禁止驱逐数据三、集群(即分布式)下面详细介绍一下redis的集群功能,从3.0以后的版本开始支持集群功能,也就是正真意义上实现了分布式。Redis 集群是一个分布式(distributed)、容错(fault-tolerant)的 Redis 实现, 集群可以使用的功能是普通单机 Redis 所能使用的功能的一个子集(subset)。Redis 集群中不存在中心(central)节点或者代理(proxy)节点, 集群的其中一个主要设计目标是达到线性可扩展性(linear scalability)。Redis 集群为了保证一致性(consistency)而牺牲了一部分容错性: 系统会在保证对网络断线(netsplit)和节点失效(node failure)具有有限(limited)抵抗力的前提下,尽可能地保持数据的一致性。集群特性:(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。(2)节点的fail是通过集群中超过半数的节点检测失效时才生效。(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->valueRedis 集群实现的功能子集:Redis集群实现了单机 Redis 中, 所有处理单个数据库键的命令。针对多个数据库键的复杂计算操作, 比如集合的并集操作、合集操作没有被实现,那些理论上需要使用多个节点的多个数据库键才能完成的命令也没有被实现。在将来, 用户也许可以通过 MIGRATE COPY 命令,在集群的计算节点(computation node)中执行针对多个数据库键的只读操作, 但集群本身不会去实现那些需要将多个数据库键在多个节点中移来移去的复杂多键命令。Redis 集群不像单机Redis 那样支持多数据库功能, 集群只使用默认的 0 号数据库, 并且不能使用 SELECT 命令。Redis 集群协议中的客户端和服务器:Redis 集群中的节点有以下责任:持有键值对数据。记录集群的状态,包括键到正确节点的映射(mappingkeys to right nodes)。自动发现其他节点,识别工作不正常的节点,并在有需要时,在从节点中选举出新的主节点。为了执行以上列出的任务, 集群中的每个节点都与其他节点建立起了“集群连接(cluster bus)”, 该连接是一个 TCP 连接, 使用二进制协议进行通讯。节点之间使用Gossip 协议 来进行以下工作:传播(propagate)关于集群的信息,以此来发现新的节点。向其他节点发送 PING 数据包,以此来检查目标节点是否正常运作。在特定事件发生时,发送集群信息。除此之外, 集群连接还用于在集群中发布或订阅信息。因为集群节点不能代理(proxy)命令请求, 所以客户端应该在节点返回 -MOVED 或者 -ASK 转向(redirection)错误时,自行将命令请求转发至其他节点。因为客户端可以自由地向集群中的任何一个节点发送命令请求, 并可以在有需要时, 根据转向错误所提供的信息, 将命令转发至正确的节点,所以在理论上来说, 客户端是无须保存集群状态信息的。不过, 如果客户端可以将键和节点之间的映射信息保存起来, 可以有效地减少可能出现的转向次数, 籍此提升命令执行的效率。键分布模型Redis 集群的键空间被分割为 16384 个槽(slot), 集群的最大节点数量也是 16384 个。推荐的最大节点数量为 1000 个左右。每个主节点都负责处理 16384 个哈希槽的其中一部分。当我们说一个集群处于“稳定”(stable)状态时, 指的是集群没有在执行重配(reconfiguration)操作,每个哈希槽都只由一个节点进行处理。重配置指的是将某个/某些槽从一个节点移动到另一个节点。一个主节点可以有任意多个从节点,这些从节点用于在主节点发生网络断线或者节点失效时, 对主节点进行替换。集群节点属性:每个节点在集群中都有一个独一无二的 ID , 该 ID 是一个十六进制表示的 160 位随机数, 在节点第一次启动时由 /dev/urandom 生成。节点会将它的 ID 保存到配置文件, 只要这个配置文件不被删除,节点就会一直沿用这个 ID 。节点 ID 用于标识集群中的每个节点。一个节点可以改变它的 IP 和端口号, 而不改变节点 ID 。集群可以自动识别出 IP/端口号的变化, 并将这一信息通过 Gossip 协议广播给其他节点知道。以下是每个节点都有的关联信息, 并且节点会将这些信息发送给其他节点:节点所使用的 IP 地址和 TCP 端口号。节点的标志(flags)。节点负责处理的哈希槽。节点最近一次使用集群连接发送 PING 数据包(packet)的时间。节点最近一次在回复中接收到 PONG 数据包的时间。集群将该节点标记为下线的时间。该节点的从节点数量。如果该节点是从节点的话,那么它会记录主节点的节点 ID 。如果这是一个主节点的话,那么主节点 ID 这一栏的值为 0000000 。以上信息的其中一部分可以通过向集群中的任意节点(主节点或者从节点都可以)发送 CLUSTER NODES 命令来获得。节点握手:节点总是应答(accept)来自集群连接端口的连接请求,并对接收到的 PING 数据包进行回复, 即使这个 PING 数据包来自不可信的节点。然而,除了 PING 之外, 节点会拒绝其他所有并非来自集群节点的数据包。要让一个节点承认另一个节点同属于一个集群,只有以下两种方法:一个节点可以通过向另一个节点发送 MEET 信息,来强制让接收信息的节点承认发送信息的节点为集群中的一份子。 一个节点仅在管理员显式地向它发送CLUSTER MEET ipport 命令时, 才会向另一个节点发送 MEET 信息。如果一个可信节点向另一个节点传播第三者节点的信息, 那么接收信息的那个节点也会将第三者节点识别为集群中的一份子。也即是说, 如果 A 认识 B , B 认识 C , 并且 B 向 A 传播关于 C 的信息, 那么 A 也会将 C 识别为集群中的一份子, 并尝试连接 C 。这意味着如果我们将一个/一些新节点添加到一个集群中, 那么这个/这些新节点最终会和集群中已有的其他所有节点连接起来。这说明只要管理员使用 CLUSTER MEET 命令显式地指定了可信关系,集群就可以自动发现其他节点。这种节点识别机制通过防止不同的 Redis 集群因为 IP 地址变更或者其他网络事件的发生而产生意料之外的联合(mix), 从而使得集群更具健壮性。当节点的网络连接断开时,它会主动连接其他已知的节点。MOVED 转向:一个 Redis 客户端可以向集群中的任意节点(包括从节点)发送命令请求。节点会对命令请求进行分析, 如果该命令是集群可以执行的命令, 那么节点会查找这个命令所要处理的键所在的槽。如果要查找的哈希槽正好就由接收到命令的节点负责处理,那么节点就直接执行这个命令。另一方面, 如果所查找的槽不是由该节点处理的话, 节点将查看自身内部所保存的哈希槽到节点 ID 的映射记录,并向客户端回复一个 MOVED 错误。即使客户端在重新发送 GET 命令之前, 等待了非常久的时间,以至于集群又再次更改了配置, 使得节点 127.0.0.1:6381 已经不再处理槽 3999 , 那么当客户端向节点 127.0.0.1:6381 发送 GET 命令的时候, 节点将再次向客户端返回 MOVED 错误, 指示现在负责处理槽 3999 的节点。虽然我们用 ID 来标识集群中的节点, 但是为了让客户端的转向操作尽可能地简单,,节点在 MOVED 错误中直接返回目标节点的 IP 和端口号,而不是目标节点的 ID 。但一个客户端应该记录(memorize)下“槽 3999 由节点 127.0.0.1:6381 负责处理“这一信息, 这样当再次有命令需要对槽 3999 执行时, 客户端就可以加快寻找正确节点的速度。注意, 当集群处于稳定状态时, 所有客户端最终都会保存有一个哈希槽至节点的映射记录(map of hash slots to nodes), 使得集群非常高效: 客户端可以直接向正确的节点发送命令请求, 无须转向、代理或者其他任何可能发生单点故障(single point failure)的实体(entiy)。除了 MOVED转向错误之外, 一个客户端还应该可以处理稍后介绍的 ASK 转向错误。集群在线重配置:Redis 集群支持在集群运行的过程中添加或者移除节点。实际上, 节点的添加操作和节点的删除操作可以抽象成同一个操作,那就是, 将哈希槽从一个节点移动到另一个节点:添加一个新节点到集群, 等于将其他已存在节点的槽移动到一个空白的新节点里面。从集群中移除一个节点, 等于将被移除节点的所有槽移动到集群的其他节点上面去。因此, 实现Redis 集群在线重配置的核心就是将槽从一个节点移动到另一个节点的能力。 因为一个哈希槽实际上就是一些键的集合, 所以 Redis 集群在重哈希(rehash)时真正要做的, 就是将一些键从一个节点移动到另一个节点。要理解Redis 集群如何将槽从一个节点移动到另一个节点, 我们需要对 CLUSTER 命令的各个子命令进行介绍,这些命理负责管理集群节点的槽转换表(slots translation table)。以下是CLUSTER 命令可用的子命令:最开头的两条命令ADDSLOTS 和 DELSLOTS 分别用于向节点指派(assign)或者移除节点,当槽被指派或者移除之后, 节点会将这一信息通过 Gossip 协议传播到整个集群。 ADDSLOTS 命令通常在新创建集群时, 作为一种快速地将各个槽指派给各个节点的手段来使用。CLUSTERSETSLOT slot NODE node 子命令可以将指定的槽 slot 指派给节点node 。至于CLUSTER SETSLOT slot MIGRATING node 命令和 CLUSTER SETSLOTslot IMPORTING node 命令, 前者用于将给定节点 node 中的槽 slot 迁移出节点, 而后者用于将给定槽 slot导入到节点 node :当一个槽被设置为MIGRATING 状态时, 原来持有这个槽的节点仍然会继续接受关于这个槽的命令请求, 但只有命令所处理的键仍然存在于节点时, 节点才会处理这个命令请求。如果命令所使用的键不存在与该节点, 那么节点将向客户端返回一个 -ASK 转向(redirection)错误, 告知客户端, 要将命令请求发送到槽的迁移目标节点。当一个槽被设置为IMPORTING 状态时, 节点仅在接收到 ASKING 命令之后, 才会接受关于这个槽的命令请求。如果客户端没有向节点发送 ASKING 命令, 那么节点会使用 -MOVED 转向错误将命令请求转向至真正负责处理这个槽的节点。上面关于MIGRATING 和 IMPORTING 的说明有些难懂, 让我们用一个实际的实例来说明一下。假设现在, 我们有 A 和 B 两个节点, 并且我们想将槽8 从节点 A 移动到节点 B , 于是我们:向节点 B 发送命令 CLUSTER SETSLOT 8 IMPORTING A向节点 A 发送命令 CLUSTER SETSLOT 8 MIGRATING B每当客户端向其他节点发送关于哈希槽 8 的命令请求时, 这些节点都会向客户端返回指向节点 A 的转向信息:如果命令要处理的键已经存在于槽 8 里面, 那么这个命令将由节点 A 处理。如果命令要处理的键未存在于槽 8 里面(比如说,要向槽添加一个新的键), 那么这个命令由节点 B 处理。这种机制将使得节点 A 不再创建关于槽 8 的任何新键。与此同时, 一个特殊的客户端 redis-trib 以及 Redis 集群配置程序(configuration utility)会将节点 A 中槽 8 里面的键移动到节点 B 。键的移动操作由以下两个命令执行:CLUSTERGETKEYSINSLOT slot count上面的命令会让节点返回 count 个 slot 槽中的键, 对于命令所返回的每个键, redis-trib 都会向节点 A 发送一条 MIGRATE 命令, 该命令会将所指定的键原子地(atomic)从节点 A 移动到节点 B (在移动键期间,两个节点都会处于阻塞状态,以免出现竞争条件)。以下为MIGRATE 命令的运作原理:MIGRATEtarget_host target_port key target_database id timeout执行MIGRATE 命令的节点会连接到 target 节点, 并将序列化后的 key 数据发送给 target , 一旦 target 返回 OK , 节点就将自己的 key 从数据库中删除。从一个外部客户端的视角来看, 在某个时间点上, 键 key 要么存在于节点 A , 要么存在于节点 B , 但不会同时存在于节点 A 和节点 B 。因为 Redis集群只使用 0 号数据库, 所以当 MIGRATE 命令被用于执行集群操作时, target_database 的值总是 0 。target_database参数的存在是为了让 MIGRATE 命令成为一个通用命令, 从而可以作用于集群以外的其他功能。我们对MIGRATE 命令做了优化, 使得它即使在传输包含多个元素的列表键这样的复杂数据时, 也可以保持高效。不过, 尽管MIGRATE 非常高效, 对一个键非常多、并且键的数据量非常大的集群来说, 集群重配置还是会占用大量的时间, 可能会导致集群没办法适应那些对于响应时间有严格要求的应用程序。ASK 转向:在之前介绍 MOVED 转向的时候, 我们说除了 MOVED 转向之外, 还有另一种 ASK 转向。当节点需要让一个客户端长期地(permanently)将针对某个槽的命令请求发送至另一个节点时,节点向客户端返回 MOVED 转向。另一方面, 当节点需要让客户端仅仅在下一个命令请求中转向至另一个节点时, 节点向客户端返回 ASK 转向。比如说, 在我们上一节列举的槽 8 的例子中, 因为槽 8 所包含的各个键分散在节点 A 和节点 B 中, 所以当客户端在节点 A 中没找到某个键时, 它应该转向到节点 B 中去寻找, 但是这种转向应该仅仅影响一次命令查询,而不是让客户端每次都直接去查找节点 B : 在节点 A 所持有的属于槽 8 的键没有全部被迁移到节点 B 之前, 客户端应该先访问节点 A , 然后再访问节点 B 。因为这种转向只针对 16384 个槽中的其中一个槽, 所以转向对集群造成的性能损耗属于可接受的范围。因为上述原因, 如果我们要在查找节点 A 之后, 继续查找节点 B , 那么客户端在向节点 B 发送命令请求之前, 应该先发送一个 ASKING 命令, 否则这个针对带有IMPORTING 状态的槽的命令请求将被节点 B 拒绝执行。接收到客户端 ASKING 命令的节点将为客户端设置一个一次性的标志(flag), 使得客户端可以执行一次针对 IMPORTING 状态的槽的命令请求。从客户端的角度来看, ASK 转向的完整语义(semantics)如下:如果客户端接收到 ASK 转向, 那么将命令请求的发送对象调整为转向所指定的节点。先发送一个 ASKING 命令,然后再发送真正的命令请求。不必更新客户端所记录的槽 8 至节点的映射: 槽 8 应该仍然映射到节点 A , 而不是节点 B 。一旦节点 A 针对槽 8 的迁移工作完成, 节点 A 在再次收到针对槽 8 的命令请求时, 就会向客户端返回 MOVED 转向, 将关于槽 8 的命令请求长期地转向到节点 B 。注意, 即使客户端出现 Bug , 过早地将槽 8 映射到了节点 B 上面, 但只要这个客户端不发送 ASKING 命令, 客户端发送命令请求的时候就会遇上 MOVED 错误, 并将它转向回节点 A 。容错:节点失效检测,以下是节点失效检查的实现方法:当一个节点向另一个节点发送 PING 命令, 但是目标节点未能在给定的时限内返回 PING 命令的回复时, 那么发送命令的节点会将目标节点标记为 PFAIL(possible failure,可能已失效)。等待 PING 命令回复的时限称为“节点超时时限(node timeout)”, 是一个节点选项(node-wise setting)。每次当节点对其他节点发送 PING 命令的时候,它都会随机地广播三个它所知道的节点的信息, 这些信息里面的其中一项就是说明节点是否已经被标记为 PFAIL或者 FAIL 。当节点接收到其他节点发来的信息时, 它会记下那些被其他节点标记为失效的节点。这称为失效报告(failure report)。如果节点已经将某个节点标记为 PFAIL , 并且根据节点所收到的失效报告显式,集群中的大部分其他主节点也认为那个节点进入了失效状态, 那么节点会将那个失效节点的状态标记为 FAIL 。一旦某个节点被标记为 FAIL , 关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为 FAIL 。简单来说, 一个节点要将另一个节点标记为失效, 必须先询问其他节点的意见, 并且得到大部分主节点的同意才行。因为过期的失效报告会被移除,所以主节点要将某个节点标记为 FAIL 的话, 必须以最近接收到的失效报告作为根据。从节点选举:一旦某个主节点进入 FAIL 状态, 如果这个主节点有一个或多个从节点存在,那么其中一个从节点会被升级为新的主节点, 而其他从节点则会开始对这个新的主节点进行复制。新的主节点由已下线主节点属下的所有从节点中自行选举产生,以下是选举的条件:这个节点是已下线主节点的从节点。已下线主节点负责处理的槽数量非空。从节点的数据被认为是可靠的, 也即是, 主从节点之间的复制连接(replication link)的断线时长不能超过节点超时时限(nodetimeout)乘以REDIS_CLUSTER_SLAVE_VALIDITY_MULT 常量得出的积。如果一个从节点满足了以上的所有条件, 那么这个从节点将向集群中的其他主节点发送授权请求, 询问它们,是否允许自己(从节点)升级为新的主节点。如果发送授权请求的从节点满足以下属性, 那么主节点将向从节点返FAILOVER_AUTH_GRANTED 授权, 同意从节点的升级要求:发送授权请求的是一个从节点, 并且它所属的主节点处于 FAIL状态。在已下线主节点的所有从节点中, 这个从节点的节点 ID 在排序中是最小的。这个从节点处于正常的运行状态: 它没有被标记为 FAIL 状态,也没有被标记为 PFAIL 状态。一旦某个从节点在给定的时限内得到大部分主节点的授权,它就会开始执行以下故障转移操作:通过 PONG 数据包(packet)告知其他节点, 这个节点现在是主节点了。通过 PONG 数据包告知其他节点, 这个节点是一个已升级的从节点(promoted slave)。接管(claiming)所有由已下线主节点负责处理的哈希槽。显式地向所有节点广播一个 PONG 数据包, 加速其他节点识别这个节点的进度,而不是等待定时的 PING / PONG 数据包。所有其他节点都会根据新的主节点对配置进行相应的更新:所有被新的主节点接管的槽会被更新。已下线主节点的所有从节点会察觉到 PROMOTED 标志,并开始对新的主节点进行复制。如果已下线的主节点重新回到上线状态, 那么它会察觉到PROMOTED 标志, 并将自身调整为现任主节点的从节点。在集群的生命周期中, 如果一个带有 PROMOTED 标识的主节点因为某些原因转变成了从节点,那么该节点将丢失它所带有的 PROMOTED 标识。 ...

December 18, 2018 · 4 min · jiezi

spring boot 结合Redis 实现工具类

自己整理了 spring boot 结合 Redis 的工具类引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>加入配置# Redis数据库索引(默认为0)spring.redis.database=0# Redis服务器地址spring.redis.host=localhost# Redis服务器连接端口spring.redis.port=6379实现代码这里用到了 静态类工具类中 如何使用 @Autowiredpackage com.lmxdawn.api.common.utils;import java.util.Collection;import java.util.Set;import java.util.concurrent.TimeUnit;import javax.annotation.PostConstruct;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;/** * 缓存操作类 /@Componentpublic class CacheUtils { @Autowired private RedisTemplate<String, String> redisTemplate; // 维护一个本类的静态变量 private static CacheUtils cacheUtils; @PostConstruct public void init() { cacheUtils = this; cacheUtils.redisTemplate = this.redisTemplate; } /* * 将参数中的字符串值设置为键的值,不设置过期时间 * @param key * @param value 必须要实现 Serializable 接口 / public static void set(String key, String value) { cacheUtils.redisTemplate.opsForValue().set(key, value); } /* * 将参数中的字符串值设置为键的值,设置过期时间 * @param key * @param value 必须要实现 Serializable 接口 * @param timeout / public static void set(String key, String value, Long timeout) { cacheUtils.redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } /* * 获取与指定键相关的值 * @param key * @return / public static Object get(String key) { return cacheUtils.redisTemplate.opsForValue().get(key); } /* * 设置某个键的过期时间 * @param key 键值 * @param ttl 过期秒数 / public static boolean expire(String key, Long ttl) { return cacheUtils.redisTemplate.expire(key, ttl, TimeUnit.SECONDS); } /* * 判断某个键是否存在 * @param key 键值 / public static boolean hasKey(String key) { return cacheUtils.redisTemplate.hasKey(key); } /* * 向集合添加元素 * @param key * @param value * @return 返回值为设置成功的value数 / public static Long sAdd(String key, String… value) { return cacheUtils.redisTemplate.opsForSet().add(key, value); } /* * 获取集合中的某个元素 * @param key * @return 返回值为redis中键值为key的value的Set集合 / public static Set<String> sGetMembers(String key) { return cacheUtils.redisTemplate.opsForSet().members(key); } /* * 将给定分数的指定成员添加到键中存储的排序集合中 * @param key * @param value * @param score * @return / public static Boolean zAdd(String key, String value, double score) { return cacheUtils.redisTemplate.opsForZSet().add(key, value, score); } /* * 返回指定排序集中给定成员的分数 * @param key * @param value * @return / public static Double zScore(String key, String value) { return cacheUtils.redisTemplate.opsForZSet().score(key, value); } /* * 删除指定的键 * @param key * @return / public static Boolean delete(String key) { return cacheUtils.redisTemplate.delete(key); } /* * 删除多个键 * @param keys * @return */ public static Long delete(Collection<String> keys) { return cacheUtils.redisTemplate.delete(keys); }}相关地址GitHub 地址: https://github.com/lmxdawn/vu… ...

November 24, 2018 · 2 min · jiezi

分布式和事务你真的很熟吗?

本文注重实战或者实现,不涉及CAP,略提ACID。本文适合基础分布式程序员:1、本文会涉及集群中节点的failover和recover问题.2、本文会涉及事务及不透明事务的问题.3、本文会提到微博和tweeter,并引出一个大数据问题.由于分布式这个话题太大,事务这个话题也太大,我们从一个集群的一个小小节点开始谈起。集群中存活的节点与同步分布式系统中,如何判断一个节点(node)是否存活?kafka这样认为:1、此节点和zookeeper能喊话.(Keep sessions with zookeeper through heartbeats.)2、此节点如果是个从节点,必须能够尽可能忠实地反映主节点的数据变化。也就是说,必须能够在主节点写了新数据后,及时复制这些变化的数据,所谓及时,不能拉下太多哦.那么,符合上面两个条件的节点就可以认为是存活的,也可以认为是同步的(in-sync).关于第1点,大家对心跳都很熟悉,那么我们可以这样认为某个节点不能和zookeeper喊话了:zookeeper-node:var timer = new timer().setInterval(10sec).onTime(slave-nodes,function(slave-nodes){ slave-nodes.forEach( node -> { boolean isAlive = node.heartbeatACK(15sec); if(!isAlive) { node.numNotAlive += 1; if(node.numNotAlive >= 3) { node.declareDeadOrFailed(); slave-nodes.remove(node); //回调也可 leader-node-app.notifyNodeDeadOrFailed(node) } }else node.numNotAlive = 0; }); }); timer.run(); //你可以回调也可以像下面这样简单的计时判断 leader-node-app: var timer = new timer() .setInterval(10sec).onTime(slave-nodes,function(slave-nodes){slave-nodes.forEach(node -> { if(node.isDeadOrFailed) { //node不能和zookeeper喊话了 }});});timer.run();关于第二点,要稍微复杂点了,怎么搞呢?来这么分析:1:数据 messages.:2:操作 op-log.3:偏移 position/offset.// 1. 先考虑messages// 2. 再考虑log的postion或者offset// 3. 考虑msg和off都记录在同源数据库或者存储设备上.(database or storage-device.) var timer = new timer().setInterval(10sec).onTime(slave-nodes,function(nodes){var core-of-cpu = 8;//嫌慢就并发呗 mod hash go!nodes.groupParallel(core-of-cpu).forEach(node -> { boolean nodeSucked = false; if(node.ackTimeDiff > 30sec) { //30秒内没有回复,node卡住了 nodeSucked = true; } if(node.logOffsetDiff > 100) { //node复制跟不上了,差距超过100条数据 nodeSucked = true; } if(nodeSucked) { //总之node“死”掉了,其实到底死没死,谁知道呢?network-error在分布式系统中或者节点失败这个事情是正常现象. node.declareDeadOrFailed(); //不和你玩啦,集群不要你了 nodes.remove(node); //该怎么处理呢,抛个事件吧. fire-event-NodeDeadOrFailed(node); }});});timer.run();上面的节点的状态管理一般由zookeeper来做,leader或者master节点也会维护那么点状态。那么应用中的leader或者master节点,只需要从zookeeper拉状态就可以,同时,上面的实现是不是一定最佳呢?不是的,而且多数操作可以合起来,但为了描述节点是否存活这个事儿,咱们这么写没啥问题。节点死掉、失败、不同步了,咋处理呢?好嘛,终于说到failover和recover了,那failover比较简单,因为还有其它的slave节点在,不影响数据读取。1:同时多个slave节点失败了?没有100%的可用性.数据中心和机房瘫痪、网络电缆切断、hacker入侵删了你的根,总之你rp爆表了.2:如果主节点失败了,那master-master不行嘛?keep-alived或者LVS或者你自己写failover吧.高可用架构(HA)又是个大件儿了,此文不展开了。我们来关注下recover方面的东西,这里把视野打开点,不仅关注slave节点重启后追log来同步数据,我们看下在实际应用中,数据请求(包括读、写、更新)失败怎么办?大家可能都会说,重试(retry)呗、重放(replay)呗或者干脆不管了呗!行,都行,这些都是策略,但具体怎么个搞法,你真的清楚了?一个bigdata问题我们先摆个探讨的背景:问题:消息流,比如微博的微博(真绕),源源不断地流进我们的应用中,要处理这些消息,有个需求是这样的:Reach is the number of unique people exposed to a URL on Twitter.那么,统计一下3小时内的本条微博(url)的reach总数。怎么解决呢?把某时间段内转发过某条微博(url)的人拉出来,把这些人的粉丝拉出来,去掉重复的人,然后求总数,就是要求的reach.为了简单,我们忽略掉日期,先看看这个方法行不行:/** ———————————* 1. 求出转发微博(url)的大V. * __________________________________/方法 :getUrlToTweetersMap(String url_id)SQL : / 数据库A,表url_user存储了转发某url的user /SELECT url_user.user_id as tweeter_idFROM url_userWHERE url_user.url_id = ${url_id} 返回 :[user_1,…,user_m]./* ———————————* 2. 求出大V的粉丝 * __________________________________/方法 : getFollowers(String tweeter_id);SQL : / 数据库B /SELECT users.id as user_idFROM usersWHERE users.followee_id = ${tweeter_id}返回:tweeter的粉丝./* ———————————* 3. 求出Reach* __________________________________/var url = queryArgs.getUrl();var tweeters = getUrlToTweetersMap();var result = new HashMap<String,Integer>();tweeters.forEach(t -> {// 你可以批量in + 并发读来优化下面方法的性能var followers = getFollowers(t.tweeter_id);followers.forEach(f -> { //hash去重 result.put(f.user_id,1);});});//Reachreturn result.size(); 顶呱呱,无论如何,求出了Reach啊!其实这又引出了一个很重要的问题,也是很多大谈框架、设计、模式却往往忽视的问题:性能和数据库建模的关系。1:数据量有多大?不知道读者有木有对这个问题的数据库I/O有点想法,或者虎躯一震呢?Computing reach is too intense for a single machine – it can require thousands of database calls and tens of millions of tuples.在上面的数据库设计中避免了JOIN,为了提高求大V粉丝的性能,可以将一批大V作为batch/bulk,然后多个batch并发读,誓死搞死数据库。这里将微博到转发者表所在的库,与粉丝库分离,如果数据更大怎么办?库再分表…OK,假设你已经非常熟悉传统关系型数据库的分库分表及数据路由(读路径的聚合、写路径的分发)、或者你对于sharding技术也很熟悉、或者你良好的结合了HBase的横向扩展能力并有一致性策略来解决其二级索引问题.总之,存储和读取的问题假设你已经解决了,那么分布式计算呢?2:微博这种应用,人与人之间的关系成图状(网),你怎么建模存储?而不仅仅对应这个问题,比如:某人的好友的好友可能和某人有几分相熟?看看用storm怎么来解决分布式计算,并提供流式计算的能力: // url到大V -> 数据库1 TridentState urlToTweeters = topology.newStaticState(getUrlToTweetersState()); // 大V到粉丝 -> 数据库2 TridentState tweetersToFollowers =topology.newStaticState(getTweeterToFollowersState());topology.newDRPCStream(“reach”).stateQuery(urlToTweeters, new Fields(“args”), new MapGet(), new Fields(“tweeters”)).each(new Fields(“tweeters”), new ExpandList(), new Fields(“tweeter”)).shuffle() / 大V的粉丝很多,所以需要分布式处理*/.stateQuery(tweetersToFollowers, new Fields(“tweeter”), new MapGet(), new Fields(“followers”)).parallelismHint(200) /* 粉丝很多,所以需要高并发 / .each(new Fields(“followers”), new ExpandList(), new Fields(“follower”)).groupBy(new Fields(“follower”)).aggregate(new One(), new Fields(“one”)) / 去重 /.parallelismHint(20).aggregate(new Count(), new Fields(“reach”)); / 计算reach数 */最多处理一次(At most once)回到主题,引出上面的例子,一是为了引出一个有关分布式(存储+计算)的问题,二是透漏这么点意思:码农,就应该关注设计和实现的东西,比如Jay Kreps是如何发明Kafka这个轮子的 : ]如果你还是码农级别,咱来务点实吧,前面我们说到recover,节点恢复的问题,那么我们恢复几个东西?基本的:1、节点状态2、节点数据本篇从数据上来讨论下这个问题,为使问题再简单点,我们考虑写数据的场景,如果我们用write-ahead-log的方式来保证数据复制和一致性,那么我们会怎么处理一致性问题呢?1:主节点有新数据写入.2:从节点追log,准备复制这批新数据。从节点做两件事:(1). 把数据的id偏移写入log;(2). 正要处理数据本身,从节点挂了。那么根据上文的节点存活条件,这个从节点挂了这件事被探测到了,从节点由维护人员手动或者其自己恢复了,那么在加入集群和小伙伴们继续玩耍之前,它要同步自己的状态和数据。问题来了:如果根据log内的数据偏移来同步数据,那么,因为这个节点在处理数据之前就把偏移写好了,可是那批数据lost-datas没有得到处理,如果追log之后的数据来同步,那么那批数据lost-datas就丢了。在这种情况下,就叫作数据最多处理一次,也就是说数据会丢失。最少处理一次(At least once)好吧,丢失数据不能容忍,那么我们换种方式来处理:1:主节点有新数据写入.2:从节点追log,准备复制这批新数据。从节点做两件事:(1). 先处理数据;(2). 正要把数据的id偏移写入log,从节点挂了。问题又来了:如果从节点追log来同步数据,那么因为那批数据duplicated-datas被处理过了,而数据偏移没有反映到log中,如果这样追,会导致这批数据重复。这种场景,从语义上来讲,就是数据最少处理一次,意味着数据处理会重复。仅处理一次(Exactly once)Transaction好吧,数据重复也不能容忍?要求挺高啊。大家都追求的强一致性保证(这里是最终一致性),怎么来搞呢?换句话说,在更新数据的时候,事务能力如何保障呢?假设一批数据如下:// 新到数据{transactionId:4urlId:99reach:5}现在要更新这批数据到库里或者log里,那么原来的情况是:// 老数据{transactionId:3urlId:99reach:3}如果说可以保证如下三点:1、事务ID的生成是强有序的.(隔离性,串行)2、同一个事务ID对应的一批数据相同.(幂等性,多次操作一个结果)3、单条数据会且仅会出现在某批数据中.(一致性,无遗漏无重复)那么,放心大胆的更新好了:// 更新后数据{ transactionId:4urlId:99//3 + 5 = 8reach:8}注意到这个更新是ID偏移和数据一起更新的,那么这个操作靠什么来保证:原子性。 你的数据库不提供原子性?后文略有提及。这里是更新成功了。如果更新的时候,节点挂了,那么库里或者log里的id偏移不写,数据也不处理,等节点恢复,就可以放心去同步,然后加入集群玩耍了。所以说,要保证数据仅处理一次,还是挺困难的吧?上面的保障“仅处理一次”这个语义的实现有什么问题呢?性能问题。这里已经使用了batch策略来减少到库或磁盘的Round-Trip Time,那么这里的性能问题是什么呢?考虑一下,采用master-master架构来保证主节点的可用性,但是一个主节点失败了,到另一个主节点主持工作,是需要时间的。假设从节点正在同步,啪!主节点挂了!因为要保证仅处理一次的语义,所以原子性发挥作用,失败,回滚,然后从主节点拉失败的数据(你不能就近更新,因为这批数据可能已经变化了,或者你根本没缓存本批数据),结果是什么呢?老主节点挂了, 新的主节点还没启动,所以这次事务就卡在这里,直到数据同步的源——主节点可以响应请求。如果不考虑性能,就此作罢,这也不是什么大事。你似乎意犹未尽?来吧,看看“银弹”是什么?Opaque-Transaction现在,我们来追求这样一种效果:某条数据在一批数据中(这批数据对应着一个事务),很可能会失败,但是它会在另一批数据中成功。 换句话说,一批数据的事务ID一定相同。来看看例子吧,老数据不变,只是多了个字段:prevReach。// 老数据{transactionId:3urlId:99//注意这里多了个字段,表示之前的reach的值prevReach:2reach:3}// 新到数据{transactionId:4urlId:99reach:5}这种情况,新事务的ID更大、更靠后,表明新事务可以执行,还等什么,直接更新,更新后数据如下:// 新到数据{transactionId:4urlId:99//注意这里更新为之前的值prevReach:3//3 + 5 = 8reach:8}现在来看下另外的情况:// 老数据{transactionId:3urlId:99prevReach:2reach:3}// 新到数据{//注意事务ID为3,和老数据中的事务ID相同transactionId:3urlId:99reach:5}这种情况怎么处理?是跳过吗?因为新数据的事务ID和库里或者log里的事务ID相同,按事务要求这次数据应该已经处理过了,跳过?不,这种事不能靠猜的,想想我们有的几个性质,其中关键一点就是:给定一批数据,它们所属的事务ID相同。仔细体会下,上面那句话和下面这句话的差别: 给定一个事务ID,任何时候,其所关联的那批数据相同。我们应该这么做,考虑到新到数据的事务ID和存储中的事务ID一致,所以这批数据可能被分别或者异步处理了,但是,这批数据对应的事务ID永远是同一个,那么,即使这批数据中的A部分先处理了,由于大家都是一个事务ID,那么A部分的前值是可靠的。所以,我们将依靠prevReach而不是Reach的值来更新:// 更新后数据{transactionId:3urlId:99//这个值不变prevReach:2//2 + 5 = 7reach:7}你发现了什么呢?不同的事务ID,导致了不同的值:1:当事务ID为4,大于存储中的事务ID3,Reach更新为3+5 = 8.2:当事务ID为3,等于存储中的事务ID3,Reach更新为2+5 = 7.这就是Opaque Transaction.这种事务能力是最强的了,可以保证事务异步提交。所以不用担心被卡住了,如果说集群中:Transaction:数据是分批处理的,每个事务ID对应一批确定、相同的数据.保证事务ID的产生是强有序的.保证分批的数据不重复、不遗漏.如果事务失败,数据源丢失,那么后续事务就卡住直到数据源恢复.Opaque-Transaction:数据是分批处理的,每批数据有确定而唯一的事务ID.保证事务ID的产生是强有序的.保证分批的数据不重复、不遗漏.如果事务失败,数据源丢失,不影响后续事务,除非后续事务的数据源也丢了.其实这个全局ID的设计也是门艺术:冗余关联表的ID,以减少join,做到O(1)取ID.冗余日期(long型)字段,以避免order by.冗余过滤字段,以避免无二级索引(HBase)的尴尬.存储mod-hash的值,以方便分库、分表后,应用层的数据路由书写.这个内容也太多,话题也太大,就不在此展开了。你现在知道twitter的snowflake生成全局唯一且有序的ID的重要性了。两阶段提交现在用zookeeper来做两阶段提交已经是入门级技术,所以也不展开了。如果你的数据库不支持原子操作,那么考虑两阶段提交吧。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。结语To be continued. ...

November 21, 2018 · 2 min · jiezi

浏览器缓存是什么?它的机制又是什么?

对于浏览器缓存,相信很多开发者对它真的是又爱又恨。一方面极大地提升了用户体验,而另一方面有时会因为读取了缓存而展示了“错误”的东西,而在开发过程中千方百计地想把缓存禁掉。那么浏览器缓存究竟是个什么样的神奇玩意呢?什么是浏览器缓存: 简单来说,浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。 比如说,在页面请求之后,web资源都被缓存了,在后面的重复请求中,许多资源都是直接从缓存中读取的(from cache),而不是重新去向服务器请求。为什么使用缓存:(1)减少网络带宽消耗 无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。(2)降低服务器压力 给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。(3)减少网络延迟,加快页面打开速度 带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那Web缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。浏览器端的缓存规则: 对于浏览器端的缓存来讲,这些规则是在HTTP协议头和HTML页面的Meta标签中定义的。他们分别从新鲜度和校验值两个维度来规定浏览器是否可以直接使用缓存中的副本,还是需要去源服务器获取更新的版本。 新鲜度(过期机制):也就是缓存副本有效期。一个缓存副本必须满足以下条件,浏览器会认为它是有效的,足够新的: 1. 含有完整的过期时间控制头信息(HTTP协议报头),并且仍在有效期内; 2. 浏览器已经使用过这个缓存副本,并且在一个会话中已经检查过新鲜度; 满足以上两个情况的一种,浏览器会直接从缓存中获取副本并渲染。 校验值(验证机制):服务器返回资源的时候有时在控制头信息带上这个资源的实体标签Etag(Entity Tag),它可以用来作为浏览器再次请求过程的校验标识。如过发现校验标识不匹配,说明资源已经被修改或过期,浏览器需求重新获取资源内容。浏览器缓存的控制: (1)使用HTML Meta 标签 Web开发者可以在HTML页面的<head>节点中加入<meta>标签,代码如下<meta http-equiv=“Pragma” content=“no-cache”> <!- Pragma是http1.0版本中给客户端设定缓存方式之一,具体作用会在后面详细介绍 –> 上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。但是!这里有个坑… 事实上这种禁用缓存的形式用处很有限: a. 仅有IE才能识别这段meta标签含义,其它主流浏览器仅识别“Cache-Control: no-store”的meta标签。 b. 在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发新请求(仅限页面,页面上的资源则不受影响)。 (2)使用缓存有关的HTTP消息报头 在这里就需要先跟大家介绍一下HTTP的相关知识。一个URI的完整HTTP协议交互过程是由HTTP请求和HTTP响应组成的。有关HTTP详细内容可参考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP协议详解》等。 在HTTP请求和响应的消息报头中,常见的与缓存有关的消息报头有: 在我们对HTTP请求头和响应头的部分字段有了一定的认识之后,我们接下来就来讨论不同字段之间的关系和区别: · Cache-Control与Expires Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。 · Last-Modified/ETag与Cache-Control/Expires 配置Last-Modified/ETag的情况下,浏览器再次访问统一URI的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器; Cache-Control/Expires则不同,如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用时,Cache-Control/Expires的优先级要高于Last-Modified/ETag。即当本地副本根据Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(Etag)了。 一般情况下,使用Cache-Control/Expires会配合Last-Modified/ETag一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销。 · Last-Modified与ETag你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。Etag的服务器生成规则和强弱Etag的相关内容可以参考,《互动百科-Etag》和《HTTP Header definition》,这里不再深入。 注意: 1. Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存,但是需要注意的是分布式系统里多台机器间文件的last-modified必须保持一致,以免负载均衡到不同机器导致比对失败,Yahoo建议分布式系统尽量关闭掉Etag(每台机器生成的etag都会不一样,因为除了 last-modified、inode 也很难保持一致)。 2. Last-Modified/If-Modified-Since要配合Cache-Control使用,Etag/If-None-Match也要配合Cache-Control使用。浏览器HTTP请求流程: 第一次请求: 再次请求: 用户行为与缓存: 浏览器缓存行为还有用户的行为有关,具体情况如下:如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加Java进阶交流群:895244712 ,有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。不能缓存的请求: 当然并不是所有请求都能被缓存,无法被浏览器缓存的请求如下: 1. HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告诉浏览器不用缓存的请求 2. 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的 3. 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》) 4. POST请求无法被缓存 5. HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存参考资料:1. http://www.cnblogs.com/520yang/articles/4807408.html 浏览器 HTTP 协议缓存机制详解[](http://www.cnblogs.com/520yan...2. https://my.oschina.net/leejun2005/blog/369148  浏览器 HTTP 协议缓存机制详解3. http://web.jobbole.com/82997/ 浏览器缓存机制浅析4. http://www.alloyteam.com/2012/03/web-cache-2-browser-cache/ Web浏览器的缓存机制 5. http://www.cnblogs.com/vajoy/p/5341664.html 浅谈浏览器http的缓存机制6. http://mp.weixin.qq.com/s/yf0pWRFM7v9Ru3D9_JhGPQ 浏览器缓存机制剖析 ...

November 17, 2018 · 1 min · jiezi

图解浏览器缓存,教你提高用户体验

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由前端林子发表于云+社区专栏浏览器缓存,是浏览器端保存数据,用于快速读取或避免重复资源请求的优化机制,有效的缓存使用可以避免重复的网络请求和加快页面速度,从而提高用户体验。一 强缓存1.1 区分Expires和Cache-Control以一个接口返回的响应头为例:这里我画了张思维导图,对Expires和Cache-Control做比较:具体介绍Expires和Cache-Control:Expires:(1)Expires是HTTP1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略;(2)Expires规定了缓存失效时间(Date为当前时间),是绝对时间。由于Expires返回的是一个绝对时间,在服务器时间与客户端时间相差较大的时候,缓存命中不准确;Cache-Control:(1)Cache-Control是HTTP1.1的(2)Cache-Control的max-age规定了缓存有效时间(2552s),是相对时间;(3)若响应头Expires和Cache-Control同时存在,Cache-Control优先级高于ExpiresCache-Control的常用指令:no-cache:不使用本地缓存,需要使用协商缓存,也就是先与服务器确认缓存是否可用。no-store:禁用缓存。用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。public:其他用户也可使用缓存,适用于公共缓存服务器的情况。private:只有特定用户才能使用缓存,适用于公共缓存服务器的情况。max-age:客户机可以接收生存期不大于指定时间(以秒为单位)的响应。min-fresh客户机可以接收响应时间小于当前时间加上指定时间的响应。max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。注意:no-cache指令并不是不缓存,no-cache的意思是可以缓存,但每次用应该去向服务器验证缓存是否可用。no-store才是不缓存内容。1.2 强缓存的过程强缓存:浏览器直接从本地缓存中获取数据,不与服务器进行交互。· 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在response的header会加上Expires/Cache-Control的header;· 浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,比较Expires或Cache-Control的max-age字段值做比较, 如果在有效期内,则读取缓存内容;若缓存已过期,则重新向服务器发送请求;· header在重新加载的时候会被更新这里我画了两张图,浏览器第一次请求:浏览器第一次请求浏览器再次请求:强缓存对于强缓存,chrome浏览器的状态码:200 OK(from disk cache)或是200 OK (from memory cache)例如:请求某个图片后,当浏览器再次访问这个图片时,发现有这个图片的缓存,且缓存没过期,所以就使用缓存。当浏览器发现缓存过期后,缓存并不一定不能使用了。比如文件虽然过了有效期,但内容并没有发生改变,还是可以用缓存数据。所以,这个时候需要与服务器协商,让服务器判断本地缓存是否还能使用。那么又怎么判断服务端文件有没有更新呢?主要有两种方式:Last-Modified,If-Modified-since。二 协商缓存2.1 区分Last-Modified和If-Modified-Since以一个返回的接口为例:Last-Modified的格式:Last-Modified: Mon, 17 Sep 2018 12:06:18 GMTIf-Modified-Since的格式:If-Modified-Since: Mon, 17 Sep 2018 12:06:18 GMT2.2 Etag是什么web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。2.3 协商缓存的过程浏览器第一次请求:浏览器第一次缓存浏览器再一次请求:协商缓存Last-Modified、If-Modified-Since:· 浏览器第一次向服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified字段,表示该资源在服务器上的最后修改时间;· 浏览器再次向服务器请求这个资源时,在request的header上加上If-Modified-Since字段,这个值就是上一次请求时返回的Last-Modified的值;·服务器收到资源请求时,比较If-Modified-Since字段值和被请求资源的最后修改时间,若资源最后修改时间较旧,则说明文件没有修改,返回304 Not Modified, 浏览器从缓存中加载资源;若不相同,说明文件被更新,浏览器直接从服务器加载资源, 返回200;·重新加载资源时更新Last-Modified HeaderEtag、If-None-Match· 浏览器第一次向服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上ETag字段;·浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match,这个值就是上一次请求时返回的ETag的值;·服务器再次收到资源请求时,再根据资源生成一个新的ETag,与浏览器传过来If-None-Match比较,如果这两个值相同,则说明资源没有变化,返回304 Not Modified, 浏览器从缓存中加载资源,否则返回200 资源内容。与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化2.4 为什么有了Last-Modified,还要用Etag呢?HTTP1.1中ETag的出现主要是为了解决几个Last-Modified比较难解决的问题:·一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;·某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);·某些服务器不能精确的得到文件的最后修改时间。对于上述情景,利用ETag能够更加准确的控制缓存,因为ETag是服务器自动生成的资源在服务器端的唯一标识符,资源每次变动,都会生成新的ETag值。Last-Modified与ETag是可以一起使用的,但服务器会优先验证ETag。2.5 比较强缓存和协商缓存基于上文对强缓存和协商缓存过程的解释,这里我把强缓存和协商缓存绘制在一张图里,方便比较,具体过程可以参照上文:http缓存三 小结本文主要通过图解介绍了http的缓存,具体包括强缓存和协商缓存。如有问题,欢迎指正。相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识

November 1, 2018 · 1 min · jiezi

Spring事务事件监控

前面我们讲到了Spring在进行事务逻辑织入的时候,无论是事务开始,提交或者回滚,都会触发相应的事务事件。本文首先会使用实例进行讲解Spring事务事件是如何使用的,然后会讲解这种使用方式的实现原理。1.示例对于事务事件,Spring提供了一个注解@TransactionEventListener,将这个注解标注在某个方法上,那么就将这个方法声明为了一个事务事件处理器,而具体的事件类型则是由TransactionalEventListener.phase属性进行定义的。如下是TransactionalEventListener的声明:@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@EventListenerpublic @interface TransactionalEventListener {// 指定当前标注方法处理事务的类型TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;// 用于指定当前方法如果没有事务,是否执行相应的事务事件监听器boolean fallbackExecution() default false;// 与classes属性一样,指定了当前事件传入的参数类型,指定了这个参数之后就可以在监听方法上// 直接什么一个这个参数了@AliasFor(annotation = EventListener.class, attribute = “classes”)Class<?>[] value() default {};// 作用于value属性一样,用于指定当前监听方法的参数类型@AliasFor(annotation = EventListener.class, attribute = “classes”)Class<?>[] classes() default {};// 这个属性使用Spring Expression Language对目标类和方法进行匹配,对于不匹配的方法将会过滤掉String condition() default “”;}关于这里的classes属性需要说明一下,如果指定了classes属性,那么当前监听方法的参数类型就可以直接使用所发布的事件的参数类型,如果没有指定,那么这里监听的参数类型可以使用两种:ApplicationEvent和PayloadApplicationEvent。对于ApplicationEvent类型的参数,可以通过其getSource()方法获取发布的事件参数,只不过其返回值是一个Object类型的,如果想获取具体的类型还需要进行强转;对于PayloadApplicationEvent类型,其可以指定一个泛型参数,该泛型参数必须与发布的事件的参数类型一致,这样就可以通过其getPayload()方法获取事务事件发布的数据了。关于上述属性中的TransactionPhase,其可以取如下几个类型的值:public enum TransactionPhase { // 指定目标方法在事务commit之前执行 BEFORE_COMMIT, // 指定目标方法在事务commit之后执行 AFTER_COMMIT, // 指定目标方法在事务rollback之后执行 AFTER_ROLLBACK, // 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了 AFTER_COMPLETION } 这里我们假设数据库有一个user表,对应的有一个UserService和User的model,用于往该表中插入数据,并且插入动作时使用注解标注目标方法。如下是这几个类的声明:public class User {private long id;private String name;private int age;// getter and setter…}.@Service@Transactionalpublic class UserServiceImpl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate; @Autowired private ApplicationEventPublisher publisher;@Overridepublic void insert(User user) {jdbcTemplate.update(“insert into user (id, name, age) value (?, ?, ?)”, user.getId(), user.getName(), user.getAge());publisher.publishEvent(user);}}上述代码中有一点需要注意的是,对于需要监控事务事件的方法,在目标方法执行的时候需要使用ApplicationEventPublisher发布相应的事件消息。如下是对上述消息进行监控的程序:@Componentpublic class UserTransactionEventListener {@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)public void beforeCommit(PayloadApplicationEvent<User> event) {System.out.println(“before commit, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)public void afterCommit(PayloadApplicationEvent<User> event) {System.out.println(“after commit, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)public void afterCompletion(PayloadApplicationEvent<User> event) {System.out.println(“after completion, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)public void afterRollback(PayloadApplicationEvent<User> event) {System.out.println(“after rollback, id: " + event.getPayload().getId());}}这里对于事件的监控,只需要在监听方法上添加@TransactionalEventListener注解即可。这里需要注意的一个问题,在实际使用过程中,对于监听的事务事件,需要使用其他的参数进行事件的过滤,因为这里的监听还是会监听所有事件参数为User类型的事务,而无论其是哪个位置发出来的。如果需要对事件进行过滤,这里可以封装一个UserEvent对象,其内保存一个类似EventType的属性和一个User对象,这样在发布消息的时候就可以指定EventType属性,而在监听消息的时候判断当前方法监听的事件对象的EventType是否为目标type,如果是,则对其进行处理,否则直接略过。下面是上述程序的xml文件配置和驱动程序:<bean id=“dataSource” class=“org.apache.commons.dbcp.BasicDataSource”><property name=“url” value=“jdbc:mysql://localhost/test?useUnicode=true”/><property name=“driverClassName” value=“com.mysql.jdbc.Driver”/><property name=“username” value=””/><property name=“password” value=””/></bean><bean id=“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”><property name=“dataSource” ref=“dataSource”/></bean><bean id=“transactionManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”><property name=“dataSource” ref=“dataSource”/></bean><context:component-scan base-package=“com.transaction”/><tx:annotation-driven/>.public class TransactionApp {@Testpublic void testTransaction() {ApplicationContext ac = new ClassPathXmlApplicationContext(“applicationContext.xml”);UserService userService = context.getBean(UserService.class);User user = getUser();userService.insert(user);}private User getUser() {int id = new Random() .nextInt(1000000);User user = new User();user.setId(id);user.setName(“Mary”);user.setAge(27);return user;}}运行上述程序,其执行结果如下:before commit, id: 935052after commit, id: 935052after completion, id: 935052可以看到,这里确实成功监听了目标程序的相关事务行为。2.实现原理关于事务的实现原理,这里其实是比较简单的,在前面的文章中,我们讲解到,Spring对事务监控的处理逻辑在TransactionSynchronization中,如下是该接口的声明:public interface TransactionSynchronization extends Flushable {// 在当前事务挂起时执行default void suspend() {}// 在当前事务重新加载时执行default void resume() {}// 在当前数据刷新到数据库时执行default void flush() {}// 在当前事务commit之前执行default void beforeCommit(boolean readOnly) {}// 在当前事务completion之前执行default void beforeCompletion() {}// 在当前事务commit之后实质性default void afterCommit() {}// 在当前事务completion之后执行default void afterCompletion(int status) {}}很明显,这里的TransactionSynchronization接口只是抽象了一些行为,用于事务事件发生时触发,这些行为在Spring事务中提供了内在支持,即在相应的事务事件时,其会获取当前所有注册的TransactionSynchronization对象,然后调用其相应的方法。那么这里TransactionSynchronization对象的注册点对于我们了解事务事件触发有至关重要的作用了。这里我们首先回到事务标签的解析处,在前面讲解事务标签解析时,我们讲到Spring会注册一个TransactionalEventListenerFactory类型的bean到Spring容器中,这里关于标签的解析读者可以阅读本人前面的文章Spring事务用法示例与实现原理。这里注册的TransactionalEventListenerFactory实现了EventListenerFactory接口,这个接口的主要作用是先判断目标方法是否是某个监听器的类型,然后为目标方法生成一个监听器,其会在某个bean初始化之后由Spring调用其方法用于生成监听器。如下是该类的实现:public class TransactionalEventListenerFactory implements EventListenerFactory, Ordered {// 指定当前监听器的顺序private int order = 50;public void setOrder(int order) { this.order = order;}@Overridepublic int getOrder() { return this.order;}// 指定目标方法是否是所支持的监听器的类型,这里的判断逻辑就是如果目标方法上包含有// TransactionalEventListener注解,则说明其是一个事务事件监听器@Overridepublic boolean supportsMethod(Method method) { return (AnnotationUtils.findAnnotation(method, TransactionalEventListener.class) != null);}// 为目标方法生成一个事务事件监听器,这里ApplicationListenerMethodTransactionalAdapter实现了// ApplicationEvent接口@Overridepublic ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) { return new ApplicationListenerMethodTransactionalAdapter(beanName, type, method);}}这里关于事务事件监听的逻辑其实已经比较清楚了。ApplicationListenerMethodTransactionalAdapter本质上是实现了ApplicationListener接口的,也就是说,其是Spring的一个事件监听器,这也就是为什么进行事务处理时需要使用ApplicationEventPublisher.publish()方法发布一下当前事务的事件。ApplicationListenerMethodTransactionalAdapter在监听到发布的事件之后会生成一个TransactionSynchronization对象,并且将该对象注册到当前事务逻辑中,如下是监听事务事件的处理逻辑:@Override public void onApplicationEvent(ApplicationEvent event) {// 如果当前TransactionManager已经配置开启事务事件监听,// 此时才会注册TransactionSynchronization对象if (TransactionSynchronizationManager.isSynchronizationActive()) { // 通过当前事务事件发布的参数,创建一个TransactionSynchronization对象 TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); // 注册TransactionSynchronization对象到TransactionManager中 TransactionSynchronizationManager .registerSynchronization(transactionSynchronization);} else if (this.annotation.fallbackExecution()) { // 如果当前TransactionManager没有开启事务事件处理,但是当前事务监听方法中配置了 // fallbackExecution属性为true,说明其需要对当前事务事件进行监听,无论其是否有事务 if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn(“Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase”); } processEvent(event);} else { // 走到这里说明当前是不需要事务事件处理的,因而直接略过 if (logger.isDebugEnabled()) { logger.debug(“No transaction is active - skipping " + event); }}}这里需要说明的是,上述annotation属性就是在事务监听方法上解析的TransactionalEventListener注解中配置的属性。可以看到,对于事务事件的处理,这里创建了一个TransactionSynchronization对象,其实主要的处理逻辑就是在返回的这个对象中,而createTransactionSynchronization()方法内部只是创建了一个TransactionSynchronizationEventAdapter对象就返回了。这里我们直接看该对象的源码: private static class TransactionSynchronizationEventAdapter extends TransactionSynchronizationAdapter { private final ApplicationListenerMethodAdapter listener; private final ApplicationEvent event; private final TransactionPhase phase;public TransactionSynchronizationEventAdapter(ApplicationListenerMethodAdapter listener, ApplicationEvent event, TransactionPhase phase) { this.listener = listener; this.event = event; this.phase = phase;}@Overridepublic int getOrder() { return this.listener.getOrder();}// 在目标方法配置的phase属性为BEFORE_COMMIT时,处理before commit事件public void beforeCommit(boolean readOnly) { if (this.phase == TransactionPhase.BEFORE_COMMIT) { processEvent(); }}// 这里对于after completion事件的处理,虽然分为了三个if分支,但是实际上都是执行的processEvent()// 方法,因为after completion事件是事务事件中一定会执行的,因而这里对于commit,// rollback和completion事件都在当前方法中处理也是没问题的public void afterCompletion(int status) { if (this.phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_COMPLETION) { processEvent(); }}// 执行事务事件protected void processEvent() { this.listener.processEvent(this.event);}}可以看到,对于事务事件的处理,最终都是委托给了ApplicationListenerMethodAdapter.processEvent()方法进行的。如下是该方法的源码: public void processEvent(ApplicationEvent event) {// 处理事务事件的相关参数,这里主要是判断TransactionalEventListener注解中是否配置了value// 或classes属性,如果配置了,则将方法参数转换为该指定类型传给监听的方法;如果没有配置,则判断// 目标方法是ApplicationEvent类型还是PayloadApplicationEvent类型,是则转换为该类型传入Object[] args = resolveArguments(event);// 这里主要是获取TransactionalEventListener注解中的condition属性,然后通过// Spring expression language将其与目标类和方法进行匹配if (shouldHandle(event, args)) { // 通过处理得到的参数借助于反射调用事务监听方法 Object result = doInvoke(args); if (result != null) { // 对方法的返回值进行处理 handleResult(result); } else { logger.trace(“No result object given - no result to handle”); }} } // 处理事务监听方法的参数protected Object[] resolveArguments(ApplicationEvent event) {// 获取发布事务事件时传入的参数类型ResolvableType declaredEventType = getResolvableType(event);if (declaredEventType == null) { return null;}// 如果事务监听方法的参数个数为0,则直接返回if (this.method.getParameterCount() == 0) { return new Object[0];}// 如果事务监听方法的参数不为ApplicationEvent或PayloadApplicationEvent,则直接将发布事务// 事件时传入的参数当做事务监听方法的参数传入。从这里可以看出,如果事务监听方法的参数不是// ApplicationEvent或PayloadApplicationEvent类型,那么其参数必须只能有一个,并且这个// 参数必须与发布事务事件时传入的参数一致Class<?> eventClass = declaredEventType.getRawClass();if ((eventClass == null || !ApplicationEvent.class.isAssignableFrom(eventClass)) && event instanceof PayloadApplicationEvent) { return new Object[] {((PayloadApplicationEvent) event).getPayload()};} else { // 如果参数类型为ApplicationEvent或PayloadApplicationEvent,则直接将其传入事务事件方法 return new Object[] {event};} } // 判断事务事件方法方法是否需要进行事务事件处理private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) {if (args == null) { return false;}String condition = getCondition();if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, “EventExpressionEvaluator must no be null”); EvaluationContext evaluationContext = this.evaluator.createEvaluationContext( event, this.targetClass, this.method, args, this.applicationContext); return this.evaluator.condition(condition, this.methodKey, evaluationContext);}return true; } // 对事务事件方法的返回值进行处理,这里的处理方式主要是将其作为一个事件继续发布出去,这样就可以在// 一个统一的位置对事务事件的返回值进行处理protected void handleResult(Object result) {// 如果返回值是数组类型,则对数组元素一个一个进行发布if (result.getClass().isArray()) { Object[] events = ObjectUtils.toObjectArray(result); for (Object event : events) { publishEvent(event); }} else if (result instanceof Collection<?>) { // 如果返回值是集合类型,则对集合进行遍历,并且发布集合中的每个元素 Collection<?> events = (Collection<?>) result; for (Object event : events) { publishEvent(event); }} else { // 如果返回值是一个对象,则直接将其进行发布 publishEvent(result);}}对于事务事件的处理,总结而言,就是为每个事务事件监听方法创建了一个TransactionSynchronizationEventAdapter对象,通过该对象在发布事务事件的时候,会在当前线程中注册该对象,这样就可以保证每个线程每个监听器中只会对应一个TransactionSynchronizationEventAdapter对象。在Spring进行事务事件的时候会调用该对象对应的监听方法,从而达到对事务事件进行监听的目的。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3.小结本文首先对事务事件监听程序的使用方式进行了讲解,然后在源码层面讲解了Spring事务监听器是如何实现的。在Spring事务监听器使用过程中,需要注意的是要对当前接收到的事件类型进行判断,因为不同的事务可能会发布同样的消息对象过来。 ...

October 22, 2018 · 3 min · jiezi

微服务架构组件分析

微服务架构组件1、 如何发布和引用服务服务描述:服务调用首先解决的问题就是服务如何对外描述。 常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。RESTful API主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用优势:HTTP 协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本,所以比较适合用作跨业务平台之间的服务协议。劣势:-性能相对比较低XML 配置一般是私有 RPC 框架会选择 XML 配置这种方式来描述接口,因为私有 RPC 协议的性能比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置比较合适。这种方式的服务发布和引用主要分三个步骤:服务提供者定义接口,并实现接口服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。服务消费者进程启动时,通过加载 client.xml 配置文件引入要调用的接口。优势:私有 RPC 协议的性能比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置方式比较合适 劣势:对业务代码侵入性比较高XML 配置有变更的时候,服务消费者和服务提供者都要更新(建议:公司内部联系比较紧密的业务之间采用)IDL 文件IDL 就是接口描述语言(interface description language)的缩写,通过一种中立的方式来描接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。常用的 IDL:一个是 Facebook 开源的 Thrift 协议,另一个是 Google 开源的 gRPC 协议。无论是 Thrift 协议还是 gRPC 协议,他们的工作原来都是类似的。优势:用作跨语言平台的服务之间的调用劣势:在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL文件方式的接口定义就不太合适了。一方面会造成 IDL 文件过大难以维护另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本太高了。总结具体采用哪种服务描述方式是根据实际情况决定,通常情况下, 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用 IDL 文件方式进行描述服务。如果还存在对外开放服务调用的情形的话,使用 RESTful API 方式则更加通用。2、 如何注册和发现服务注册中心原理在微服务架构下, 主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系如图RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中配置的信息,向 Registry 注册服务,把Registry 返回的服务节点列表缓存在本地内存中,并于 RPC Server 建立连接。RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把Registry 返回的服务节点列表缓存在本地内存中,并于 RPC Client 建立连接。当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Server 发起调用。注册中心实现方式注册中心API服务注册接口:服务提供者通过调用注册接口来完成服务注册服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存货状态上报服务订阅接口:服务消费者调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表服务查询接口:查询注册中心当前住了哪些服务信息服务修改接口:修改注册中心某一服务的信息集群部署注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。Zookeeper 的工作原理:每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 ServerZookeeper 启动时,将从实例中选举一个 leader(Paxos 协议)Leader 负责处理数据更新等操作(ZAB 协议)一个更新操作方式,Zookeeper 保证了高可用性以及数据一致性目录存储ZooKeeper作为注册中心存储服务信息一般采用层次化的目录结构:每个目录在 ZooKeeper 中叫作 znode,并且其有一个唯一的路径标识znode 可以包含数据和子 znode。znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。服务健康状态检测注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也也随之建立,并生成一个全局唯一的 SessionID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT,ZooKeeper 就会认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。服务状态变更通知一旦注册中心探测到有服务器提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。基于 Zookeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 Zookeeper 的getData 方式订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData方法来获取变更后的数据,刷新本地混存的服务节点信息。白名单机制注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。总结注册中心可以说是实现服务话的关键,因为服务话之后,服务提供者和服务消费者不在同一个进程中运行,实现了解耦,这就需要一个纽带去连接服务提供者和服务消费者,而注册中心就正好承担了这一角色。此外,服务提供者可以任意伸缩即增加节点或者减少节点,通过服务健康状态检测,注册中心可以保持最新的服务节点信息,并将变化通知给订阅服务的服务消费者。注册中心一般采用分布式集群部署,来保证高可用性,并且为了实现异地多活,有的注册中心还采用多 IDC 部署,这就对数据一致性产生了很高的要求,这些都是注册中心在实现时必须要解决的问题。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3、如何实现 RPC 远程服务调用客户端和服务端如何建立网络连接HTTP 通信HTTP 通信是基于应用层HTTP 协议的,而 HTTP 协议又是基于传输层 TCP 协议的。一次 HTTP 通信过程就是发起一次 HTTP 调用,而一次 HTTP 调用就会建立一个 TCP 连接,经历一次下图所示的 “三次握手”的过程来建立连接。完成请求后,再经历一次“四次挥手”的过程来断开连接。Socket 通信Socket 通信是基于 TCP/IP 协议的封装,建立一次Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ;另一个运行于服务器端,称为 ServerSocket 。服务器监听:ServerSocket 通过点用 bind() 函数绑定某个具体端口,然后调用 listen() 函数实时监控网络状态,等待客户端的连接请求。客户端请求:ClientSocket 调用 connect() 函数向 ServerSocket 绑定的地址和端口发起连接请求。服务端连接确认:当 ServerSocket 监听都或者接收到 ClientSocket 的连接请求时,调用 accept() 函数响应ClientSocket 的请求,同客户端建立连接。数据传输:当 ClientSocket 和 ServerSocket 建立连接后,ClientSocket 调用 send() 函数,ServerSocket 调用 receive() 函数,ServerSocket 处理完请求后,调用 send() 函数,ClientSocket 调用 receive() 函数,就可以得到返回结果。当客户端和服务端建立网络连接后,就可以起发起请求了。但网络不一定总是可靠的,经常会遇到网络闪断、连接超时、服务端宕机等各种异常,通常的处理手段有两种:链路存活检测:客户端需要定时地发送心跳检测小心(一般通过 ping 请求) 给服务端,如果服务端连续 n次心跳检测或者超过规定的时间没有回复消息,则认为此时链路已经失效,这个时候客户端就需要重新与服务端建立连接。断连重试:通常有多种情况会导致连接断开,比如客户端主动关闭、服务端宕机或者网络故障等。这个时候客户端就需要与服务端重新建立连接,但一般不能立刻完成重连,而是要等待固定的间隔后再发起重连,避免服务端的连接回收不及时,而客户端瞬间重连的请求太多而把服务端的连接数占满。服务端如何处理请求同步阻塞方式(BIO)客户端每发一次请求,服务端就生成一个线程去处理。当客户端同时发起的请求很多事,服务端需要创建很多的线程去处理每一个请求,如果达到了系统最大的线程数瓶颈,新来的请求就没法处理了。BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。同步非阻塞(NIO)客户端每发一次请求,服务端并不是每次都创建一个新线程来处理,而是通过 I/O 多路复用技术进行处理。就是把多个 I/O 的阻塞复用到听一个 select 的阻塞上,从而使系统在单线程的情况下可以同时处理多个客户端请求。这种方式的优势是开销小,不用为每个请求创建一个线程,可以节省系统开销。NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。异步非阻塞(AIO)客户端只需要发起一个 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客户端会得到 I/O 操作完成的通知,此时客户端只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。这种方式的优势是客户端无需等待,不存在阻塞等待问题。AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难难度最大,程序也不易于理解。建议最为稳妥的方式是使用成熟的开源方案,比如 Netty、MINA 等,它们都是经过业界大规模应用后,被充分论证是很可靠的方案。数据传输采用什么协议无论是开放的还是私有的协议,都必须定义一个“契约”,以便服务消费和服务提供者之间能够达成共识。服务消费者按照契约,对传输的数据进行编码,然后通过网络传输过去;服务提供者从网络上接收到数据后,按照契约,对传输的数据进行解码,然后处理请求,再把处理后的结果进行编码,通过网络传输返回给服务消费者;服务消费者再对返回的结果进行解码,最终得到服务提供者处理后的返回值。HTTP 协议消息头Server 代表是服务端服务器类型Content-Length 代表返回数据的长度Content-Type 代表返回数据的类型消息体具体的返回结果数据该如何序列化和反序列化一般数据在网络中进行传输,都要先在发送方一段对数据进行编码,经过网络传输到达另一段后,再对数据进行解码,这个过程就是序列化和反序列化常用的序列化方式分为两类:文本类如 XML/JSON 等,二进制类如 PB/Thrift 等,而具体采用哪种序列化方式,主要取决于三个方面的因素。支持数据结构类型的丰富度。数据结构种类支持的越多越好,这样的话对于使用者来说在编程时更加友好,有些序列化框架如 Hessian 2.0还支持复杂的数据结构比如 Map、List等。跨语言支持。性能。主要看两点,一个是序列化后的压缩比,一个是序列化的速度。以常用的 PB 序列化和 JSON 序列化协议为例来对比分析,PB序列化的压缩比和速度都要比 JSON 序列化高很多,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合;而 JSON序列化虽然性能要差一些,但可读性更好,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合适对外部提供服务。总结通信框架:它主要解决客户端和服务端如何建立连接、管理连接以及服务端如何处理请求的问题。通信协议:它主要解决客户端和服务端采用哪些数据传输协议的问题。序列化和反序列化:它主要解决客户端和服务端采用哪种数据编码的问题。这三部分就组成了一个完成的RPC 调用框架,通信框架提供了基础的通信能力,通信协议描述了通信契约,而序列化和反序列化则用于数据的编/解码。一个通信框架可以适配多种通信协议,也可以采用多种序列化和反序列化的格式,比如服务话框架 不仅支持 Dubbo 协议,还支持 RMI 协议、HTTP 协议等,而且还支持多种序列化和反序列化格式,比如 JSON、Hession 2.0 以及 Java 序列化等。4、如何监控微服务调用在谈论监控微服务监控调用前,首先要搞清楚三个问题:监控的对象是什么?具体监控哪些指标?从哪些维度进行监控?监控对象用户端监控:通常是指业务直接对用户提供的功能的监控。接口监控:通常是指业务提供的功能所以来的具体 RPC 接口监控。资源监控:通常是指某个接口依赖的资源的监控。(eg:Redis 来存储关注列表,对 Redis 的监控就属于资源监控。)基础监控:通常是指对服务器本身的健康状况的监控。(eg: CPU、MEM、I/O、网卡带宽等)监控指标1、请求量实时请求量(QPS Queries Per Second):即每秒查询次数来衡量,反映了服务调用的实时变化情况统计请求量(PV Page View):即一段时间内用户的访问量来衡量,eg:一天的 PV 代表了服务一天的请求量,通常用来统计报表2、响应时间:大多数情况下,可以用一段时间内所有调用的平均耗时来反应请求的响应时间。但它只代表了请求的平均快慢情况,有时候我们更关心慢请求的数量。为此需要把响应时间划分为多个区间,比如0~10ms、10ms~50ms、50ms~100ms、100ms~500ms、500ms 以上这五个区间,其中 500ms 以上这个区间内的请求数就代表了慢请求量,正常情况下,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数会大幅增加,可能平均耗时并不能反映出这一变化。除此之外,还可以从P90、P95、P99、P999 角度来监控请求的响应时间在 500ms 以内,它代表了请求的服务质量,即 SLA。3、错误率:通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为 503 的比率来表示。监控维度全局维度:从整体角度监控对象的请求量、平均耗时以及错误率,全局维度的监控一般是为了让你对监控对象的调用情况有个整体了解。分机房维度:为了业务高可用,服务部署不止一个机房,因为不同机房地域的不同,同一个监控对象的各种指标可能会相差很大。单机维度:同一个机房内部,可能由于采购年份和批次不的不同,各种指标也不一样。时间维度:同一个监控对象,在每天的同一时刻各种指标通常也不会一样,这种差异要么是由业务变更导致,要么是运营活动导致。为了了解监控对象各种指标的变化,通常需要与一天前、一周前、一个月前,甚至三个月前比较。核心维度:业务上一般会依据重要性成都对监控对象进行分级,最简单的是分成核心业务和非核心业务。核心业务和非核心业务在部署上必须隔离,分开监控,这样才能对核心业务做重点保障。对于一个微服务来说,必须要明确监控哪些对象、哪些指标,并且还要从不同的维度进行监控,才能掌握微服务的调用情况。监控系统原理数据采集:收集到每一次调用的详细信息,包括调用的响应时间、调用是否成功、调用的发起者和接收者分别是谁,这个过程叫做数据采集。数据传输:采集到数据之后,要把数据通过一定的方式传输给数据处理中心进行处理,这个过程叫做数据出传输。数据处理:数据传输过来后,数据处理中心再按照服务的维度进行聚合,计算出不同服务的请求量、响应时间以及错误率等信息并存储起来,这个过程叫做数据处理。数据展示:通过接口或者 DashBoard 的形式对外展示服务的调用情况,这个过程叫做数据展示。数据采集服务主动上报代理收集:这种处理方式通过服务调用后把调用的详细信息记录到本地日志文件中,然后再通过代理去解析本地日志文件,然后再上报服务的调用信息。不管是哪种方式,首先要考虑的问题就是采样率,也就是采集数据的频率。一般来说,采样率越高,监控的实时性就越高,精确度也越高。但采样对系统本身的性能也会有一定的影响,尤其是采集后的数据需要写到本地磁盘的时候,过高的采样率会导致系统写入的 I/O 过高,进而会影响到正常的服务调用。所以合理的采样率是数据采集的关键,最好是可以动态控制采样率,在系统比较空闲的时候加大采样率,追求监控的实时性与精确度;在系统负载比较高的时候减少采样率,追求监控的可用性与系统的稳定性。数据传输UDP传输:这种处理方式是数据处理单元提供服务器的请求地址,数据采集后通过 UDP 协议与服务器建立连接,然后把数据发送过去。Kafka传输:这种处理方式是数据采集后发送都指定的 Topic,然后数据处理单元再订阅对应的 Topic,就可以从 Kafka 消息队列中读取对应的数据。无论哪种传输方式,数据格式十分重要,尤其是对带宽敏感以及解析性能要求比较高的场景,一般数据传输时采用的数据格式有两种:二进制协议,最常用的就是 PB 对象文本协议,最常用的就是 JSON 字符串数据处理接口维度聚合:把实时收到的数据按照调用的节点维度聚合在一起,这样就可以得到每个接口的实时请求、平均耗时等信息。机器维度聚合:把实时收到的数据按照调用的节点维度聚合在一起,这样就可以从单机维度去查看每个接口的实时请求量、平均耗时等信息。聚合后的数据需要持久化到数据库中存储,所选用的数据库一般分为两种:索引数据库:比如 Elasticsearcher,以倒排索引的数据结构存书,需要查询的时候,根据索引来查询。时序数据库:比如 OpenTSDB,以时序序列数据的方式存储,查询的时候按照时序如 1min、5min 等维度查询数据展示曲线图:监控变化趋势。饼状图:监控占比分布。格子图:主要坐一些细粒度的监控。总结服务监控子啊微服务改造过程中的重要性不言而喻,没有强大的监控能力,改造成微服务架构后,就无法掌控各个不同服务的情况,在遇到调用失败时,如果不能快速发现系统的问题,对于业务来说就是一场灾难。搭建一个服务监控系统,设计数据采集、数据传输、数据处理、数据展示等多个环节,每个环节都需要根据自己的业务特点选择合适的解决方案在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多5、如何追踪微服务调用跟踪记录一次用户请求都发起了哪些调用,经过哪些服务处理,并且记录每一次调用所涉及的详细信息,这时候如果发生调用失败,就可以通过这个日志快速定位是在哪个环节出了问题。服务追踪的作用优化系统瓶颈通过记录调用经过的每一条链路上的耗时,可以快速定位整个系统的瓶颈点在哪里。可能出现的原因如下:运营商网络延迟网关系统异常某个服务异常缓存或者数据库异常通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化优化链路调用通过服务追踪可以分析调用所经过的路径,然后评估是否合理一般业务都会在多个数据中心都部署服务,以实现异地容灾,这个时候经常会出现一种状况就是服务 A 调用了另外一个数据中心的服务B,而没有调用同处于一个数据中心的服务B。跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到了30ms以上,这对于有些业务几乎是不可接受的。通过对调用链路进行分析,可以找出跨数据中的服务调用,从而进行优化,尽量规避这总情况出现。生成网络拓扑通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。除此之外,在网络拓扑图上还可以把服务调用的详细信息也标出来,也能起到服务监控的作用。透明传输数据除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行A/B 测试。服务追踪原理服务追踪鼻祖:Google 发布的一篇的论文Dapper, [a Large-Scale Distributed Systems Tracing Infrastructure核心理念:通过一个全局唯一的 ID将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题、分析调用数据并统计各种系统指标可以说后面的诞生各种服务追踪系统都是基于 Dapper 衍生出来的,比较有名的有 Twitter的Zipkin、阿里的鹰眼、美团的MTrace等。讲解下服务追踪系统中几个最基本概念traceId:用于标识某一次具体的请求ID。spanId:用于标识一次 RPC 调用在分布式请求中的位置。annotation:用于业务自定义埋点数据,可以是业务感兴趣的上上传到后端的数据,比如一次请求的用户 UID。traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系,而annotation 是用于业务自定义一些自己感兴趣的数据,在上传 traceId 和 spanId 这些基本信息之外,添加一些自己感兴趣的信息。服务追踪系统实现上面是服务追踪系统架构图,一个服务追踪系统可以分三层:数据采集层:负责数据埋点并上报数据处理层:负责数据的存储与计算数据展示层:负责数据的图形化展示数据采集层作用:在系统的各个不同的模块中尽心埋点,采集数据并上报给数据处理层进行处理。CS(Client Send)阶段 : 客户端发起请求,并生成调用的上下文。SR(Server Recieve)阶段 : 服务端接收请求,并生成上下文。SS(Server Send)阶段 :服务端返回请求,这个阶段会将服务端上下文数据上报,下面这张图可以说明上报的数据有:traceId=123456,spanId=0.1,appKey=B,method=B.method,start=103,duration=38.CR(Client Recieve)阶段 :客户端接收返回结果,这个阶段会将客户端上下文数据上报,上报的数据有:traceid=123456,spanId=0.1,appKey=A,method=B.method,start=103,duration=38。数据处理层作用:把数据上报的数据按需计算,然后落地存储供查询使用实时数据处理:要求计算效率比较高,一般要对收集的链路数据能够在秒级别完成聚合计算,以供实时查询针对实时数据处理,一般使用 Storm 或者 Spack Streaming 来对链路数据进行实时聚合加工,存储一拜是用 OLTP 数据仓库,比如 HBase,使用 traceId 作为 RowKey,能天然地把一条调用链聚合在一起,提高查询效率。离线数据处理:要求计算效率相对没那么高,一般能在小时级别完成链路数据的聚合计算即可,一般用作汇总统计。针对离线数据处理,一般通过运行 MapReduce 或者 Spark 批处理程序来对链路数据进行离线计算,存储一般使用 Hive数据展示作用:将处理后的链路信息以图形化的方式展示给用户和做故障定位调用链路图(eg:Zipkin)服务整体情况:服务总耗时、服务调用的网络深度、每一层经过的系统,以及多少次调用。下图展示的一次调用,总耗时 209.323ms,经过了 5个不同系统模块,调用深度为 7 层,共发生了 2调用拓扑图(Pinpoint)调用拓扑图是一种全局视野,在实际项目中,主要用作全局监控,用户发现系统异常的点,从而快速做出决策。比如,某一个服务突然出现异常,那么在调用链路拓扑图中可以看出对这个服务的调用耗时都变高了,可以用红色的图样标出来,用作监控报警。总结服务追踪能够帮助查询一次用户请求在系统中的具体执行路径,以及每一条路径下的上下游的详细情况,对于追查问题十分有用。实现一个服务追踪系统,设计数据采集、数据处理和数据展示三个流程,有多种实现方式,具体采取某一种要根据自己的业务情况来选择。6、微服务治理的手段有哪些一次服务调用,服务提供者、注册中心、网络这三者都可能会有问题,此时服务消费者应该如何处理才能确保调用成功呢?这就是服务治理要解决的问题。节点管理服务调用失败一般是由两类原因引起的服务提供者自身出现问题,比如服务器宕机、进程意外退出等网络问题,如服务提供者、注册中心、服务消费者这三者任意两者之间的网络问题 无论是服务哪种原因,都有两种节点管理手段:注册中心主动摘除机制这种机制要求服务提供者定时的主动向注册中心汇报心跳,注册中心根据服务提供者节点最近一次汇报心跳的时间与上一次汇报心跳时间做比较,如果超出一定时间,就认为服务提供者出现问题,继而把节点从服务列表中摘除,并把最近的可用服务节点列表推送给服务消费者。服务消费者摘除机制虽然注册中心主动摘除机制可以解决服务提供者节点异常的问题,但如果是因为注册中心与服务提供者之间的网络出现异常,最坏的情况是注册中心会把服务节点全部摘除,导致服务消费者没有可能的服务节点调用,但其实这时候提供者本身是正常的。所以,将存活探测机制用在服务消费者这一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存保存的可用夫提供者节点列表一处。负载均衡算法常用的负载均衡算法主要包括以下几种:随机算法(均匀)轮询算法(按照固定的权重,对可用服务节点进行轮询)最少活跃调用算法(性能理论最优)一致性 Hash 算法(相同参数的请求总是发到同一服务节点)服务路由对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则决定。所谓的路由规则,就是通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围。为什么要指定路由规则呢?主要有两个原因:业务存在灰度发布的需求比如,服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否全量发布。多机房就近访问的需求跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到了30ms以上,这对于有些业务几乎是不可接受的,所以就要一次服务调用尽量选择同一个 IDC 内部节点,从而减少网络耗时开销,提高性能。这时一般可以通过 IP 段规则来控制访问,在选择服务节点时,优先选择同一 IP段的节点。那么路由规则该如何配置?静态配置:服务消费者本地存放调用的路由规则,如果改变,需重新上线才能生效动态配置:路由规则存放在配置中心,服务消费者定期去请求注册中心来保持同步,要想改变消费者的路由配置,可以通过修改注册中心的配置,服务消费者在下一个同步周期之后,就会请求注册中心更新配置,从而实现动态变更服务容错常用的手段主要有以下几种:FailOver:失败自动切换(调用失败或者超时,可以设置重试次数)FailBack:失败通知(调用失败或者超时,不立即发起重试,而是根据失败的详细信息,来决定后续的执行策略)FailCache:失败缓存(调用失败或者超时,不立即发起重试,而是隔一段时间后再次尝试发起调用)FailFirst:快速失败(调用一次失败后,不再充实,一般非核心业务的调用,会采取快速失败策略,调用失败后一般就记录下失败日志就返回了)一般对于幂等的调用可以选择 FailOver 或者 FailCache,非幂等的调用可以选择 Failback 或者 FailFast在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 总结节点管理是从服务节点健康状态角度来考虑,负载均衡和服务路由是从服务节点访问优先级角度来考虑,而服务容错是从调用的健康状态来考虑,可谓殊途同归。在实际的微服务架构中,上面的服务治理手段一般都会在服务框架中默认即成,比如 阿里的 Dubbo、微博开源的服务架构 Motan等。原文出处:https://my.oschina.net/rosett… ...

October 21, 2018 · 2 min · jiezi

网络协议之 - HTTP

前言关于HTTP的知识比较繁杂,也比较零散,于是想要通过这篇文章对常用知识点进行总结,一些知识点描述是从网上搜集而来,如有侵权,可以联系我进行修改。提出问题Http的报文结构。Http request的几种类型。Http的状态码含义。Http1.1和Http1.0的区别,以及缓存原理Http长连接keep-alive。Cookie与Session的作用于原理。电脑上访问一个网页,整个过程是怎么样的:DNS、HTTP、TCP、OSPF、IP、ARP。解答1. Http的报文结构http协议的请求报文和响应报文都是由以下4部分组成:请求行请求头空行 CR + LF,回车 + 换行请求体2. Http request的几种类型请求行Request格式:【方法 URL HTTP版本】 例如: GET /csrfToken HTTP/1.1HTTP版本:目前有 HTTP/1.0、HTTP/1.1、HTTP/2.0 版本,其中 HTTP1.1 版本使用较广泛方法说明支持HTTP协议版本GET获取资源1.0 、1.1POST传输实体主体1.0、1.1PUT传输文件1.0、1.1HEAD获得报文首部1.0、1.1DELETE删除资源1.0、1.1OPTIONS询问支持的方法1.1TRACE追踪路径1.1CONNECT要求用隧道协议连接代理1.1LINK建立和资源之间的联系1.0UNLINK断开连接关系1.0Response格式:【HTTP版本 状态码 描述】 例如: HTTP/1.1 200 OK、HTTP/1.1 404 NOT FOUND 类别原因短语1xxInformational(信息性状态码)接收的请求正在处理2xxSuccess(成功状态码)请求正常处理完毕3xxRedirection(重定向状态码)需要进行附加操作以完成请求4xxClient Error(客户端错误状态码)服务器无法处理请求5xxServer Error(服务器错误状态码)服务器处理请求出错3. Http的状态码含义常用状态码200:OK,请求被正常处理204:No Content,请求被受理但没有资源可以返回,返回204响应,浏览器显示的页面不发生更新206:Partial Content,客户端进行范围请求,服务器成功执行了这部分的GET请求,响应报文中包含由Content-Range指定范围的实体内容301:Moved Permanently,永久性重定向,比如访问http://test.zhixue.com/zbptadmin,服务器会返回301,response的headers中会包含一个字段:Location: http://test.zhixue.com/zbptadmin/,浏览器会按照这个地址重新请求,并且如果用户保存了书签,浏览器会自动按照Location更新书签302:Found,临时性重定向,和301类似,但是浏览器不会更新书签303:See Other,和302类似,但303明确表示客户端应该使用GET方法获取Location资源备注:当301,302,303响应状态码返回时,几乎所有的浏览器都会把POST改成GET,并删除请求报文内的主体,之后请求会自动再次发送。301,302标准是禁止将POST方法改成GET方法的,但实际使用时浏览器厂商都会这么做。304:Not Modified,发送附带条件的请求时,(附带条件的请求是指采用GET方法的请求报文中包含If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since中任一首部),条件不满足返回304,可直接使用客户端缓存,与重定向无关307:Temporary Redirect,临时重定向,与302类似,只是强制要求使用POST方法400:Bad Request,请求报文中存在语法错误,服务器无法识别401:Unauthorized,请求需要认证403:Forbidden,请求对应的资源禁止被访问404:Not Found,服务器无法找到对应资源500:Internal Server Error,服务器内部错误503:Service Unavailable,服务器正忙常见HTTP首部字段通用首部字段(请求报文与响应报文都会使用的首部字段)Date:创建报文的时间Connection:连接的管理Cache-Control:缓存的控制Transfer-Encoding:报文主题的传输编码方式请求首部字段(请求报文会使用的首部字段)Host:请求资源所在的服务器Accept:可处理的媒体类型Accept-Charset:可接收的字符集Accept-Encoding:可接收的内容编码Accept-Language:可接收的自然语言响应首部字段(响应报文会使用的首部字段)Accept-Range:可接收的字节范围Location:让客户端重定向到的URIServer:HTTP服务器的安装信息实体首部字段(请求报文与响应报文的实体部分使用的首部字段)Allow:资源可支持的HTTP方法Content-Type:实体主类的类型Content-Encoding:实体主体适用的编码方式Content-Language:实体主体的自然语言Content-Length:实体主体的字节数Content-Range:实体主体的位置范围,一般用于发出部分请求时使用4. HTTP1.0、HTTP1.1、HTTP2.0区别HTTP1.0 和 HTTP1.1 主要区别长连接HTTP1.0需要使用keep-alive参数告知服务器要建立一个长连接,而HTTP1.1默认支持长连接HTTP是基于TCP/IP协议的,创建一个TCP连接是需要经过三次握手的,有一定的开销,如果每次通讯都要重新建立连接的话,对性能有影响。因此最好能维持一个长连接,可以用这个长连接来发送多个请求节约带宽HTTP1.1支持只发送header信息,也就是HTTP的HEAD Method,如果服务器认为客户端有权限请求服务器,则返回100,否则返回401。客户端如果接收到100,才开始把请求体body发送到服务器。这样,当服务器返回401的时候,客户端就可以不用发送请求体了,节约了带宽HTTP1.1支持分块传输,比如206状态码,由Content-Range指定传输内容,这是支持文件断点续传的基础。HOST域现在一台服务器上可以有多个虚拟主机,这些虚拟站点可以共享同一个ip和端口,HTTP1.0是没有host域的,HTTP1.1才支持这个参数HTTP1.1 和 HTTP2.0的区别多路复用HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级当然HTTP1.1也可以多建立几个TCP连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的TCP连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立的连接,并且这个连接可以支持瞬时并发的请求HTTP/1.1,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某个请求超时,后续请求只能被阻塞,也就是人们常说的线头阻塞HTTP/2.0,多个请求可同时在一个连接上并行执行,某个任务耗时严重,不会影响到其他连接的正常执行数据压缩HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快服务器推送当我们对HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器获取。这种方式非常适合加载静态资源。服务器推送的这些资源其实存在客户端的某个地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然快很多。HTTP缓存缓存分类强缓存浏览器在加载资源时,先根据这个资源的一些http header判断它是否命中强缓存,如果命中,浏览器直接从自己的缓存中读取资源,不会发请求到服务器,此时状态码是200,但是看Size项是from memory cache或from disk cache协商缓存当强缓存没有命中时,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些http header验证这个资源是否命中协商缓存,如果命中,服务器会将这个请求返回304,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就又会从自己的缓存中去加载这个资源;如果没有命中,则将资源返回客户端,状态为200,浏览器同时依据response header中的相关项更新本地缓存数据from memory cache 和 from disk cache区别from memory cache 是从内存中读取缓存资源,很快,关闭浏览器后,内存中的资源就消失了from disk cache 是从磁盘中读取缓存资源,要比 from memory cache 慢,但是持久化,关闭浏览器后仍然存在哪些资源存在memory,哪些资源存在disk看了一些文章,大部分观点是说:资源在disk和memory中都会存当关闭浏览器,再打开那个页面的时候,从disk cache中获取缓存资源,同时将缓存资源存在了内存中,当再次刷新页面的时候,就从memory cache中读取,因为这样会更快一些我自己试了一些页面,再次刷新还是有的from disk cache,有的from memory cache,所以这个持保留观点还有的文章说css文件存在disk中,js等脚本存在memory中,个人认为这个不太靠谱,因为memory中的缓存在关闭浏览器就消失了,如果js只存在memory,就起不到缓存的作用了强缓存相关http headerExpiresExpires的值是服务端返回的到期时间,用GMT格式的字符串表示,如:Expires: Thu, 31 Dec 2016 23:55:55 GMT。即下一次请求时,请求时间小于服务器返回的到期时间,直接使用缓存数据。不过Expires是HTTP1.0的东西,现在浏览器默认使用HTTP1.1,所以它的作用基本忽略。另一个问题是,到期时间是由服务器生成的,但是客户端时间可能和服务器时间有偏差,这就会导致缓存命中的误差。所以HTTP1.1的版本中,使用Cache-Control替代。Cache-ControlCache-Control是最重要的规则。常见的取值有:- private:客户端可以缓存- public:客户端和代理服务器都可以缓存- max-age=xxx:缓存的内容将在xxx秒后失效,单位是秒- no-cache:需要使用协商缓存来验证缓存数据- no-store:不缓存协商缓存相关http header第1组:Last-Modified/If-Modified—SinceLast-Modified:第一次请求资源时,服务器返回的http header,告诉浏览器资源的最后修改时间,为GMT格式,如Last-Modified: Thu, 24 Jan 2017 23:55:55 GMTIf-Modified-Since:再次请求服务器资源时,浏览器设置在请求header中,值 为该资源第一次请求时服务器设置的Last-Modified`值,如If-Modified-Since: Thu, 24 Jan 2017 23:55:55 GMT,服务器收到请求后发现request header中有If-Modified-Since,则与被请求资源的最后修改时间进行比对。若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则返回资源内容,状态码200,同时浏览器依据相应response header更新缓存资源;若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则返回状态码304,但是不返回资源,告知浏览器继续使用所保存的cache第2组:Etag/If-None-MatchEtag:服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器确定),如Etag: W/“5886c231-8d9"If-None-Match:再次请求服务器时,通过此字段告知服务器该资源的唯一标识,如If-None-Match: W/“5886c231-8d9”,服务器收到请求后发现request header中有If-None-Match,则与被请求资源的唯一标识进行比对,若不同,说明资源又被改动过,则返回资源内容,状态码200,同时浏览器依据相应response header更新缓存资源;若相同,则说明资源无更改,返回304,告知浏览器继续使用所保存的缓存资源为什么要有Etag?Last-Modified颗粒度是秒,只能记录秒级的修改,比如1s内修改了N次,If-Modified-Since就无法判断了,所以要引入Etag。总结先执行强缓存策略,服务器通知浏览器一个缓存有效时间,在有效时间内,下次请求时直接使用缓存,不发起http请求,缓存从from memory cache或from disk cache中读取,若超过了有效时间,则执行协商缓存策略对于协商缓存,将缓存信息中的Etag和Last-Modified的值通过If-None-Match和If-Modified-Since发送给服务器,由服务器进行校验,若资源无更改,返回304,浏览器继续使用缓存,若资源被更改,则返回资源,状态码200,同时浏览器更新缓存流程图浏览器第一次请求:浏览器第二次请求:5. Http长连接keep-alive三个概念短连接所谓短连接,就是每次请求一个资源就建立连接,请求完成后立马关闭。每次请求都经过“创建TCP连接 -> 请求资源 -> 响应资源 -> 释放连接”。长连接所谓长连接(persistent connection),就是只建立一次TCP连接,多次HTTP请求都复用该连接。并行连接所谓并行连接(multiple connection),其实就是并发的短连接。keep-alive在HTTP/1.0里,为了实现client到web-server能支持长连接,必须在HTTP请求头里显式指定Connection: keep-alive在HTTP/1.1里,就默认开启了keep-alive,要关闭keep-alive必须在HTTP请求头里显式指定Connection: close现在大多数浏览器都默认是使用HTTP/1.1,所以keep-alive都是默认打开的。一旦client和server达成协议,那么长连接就建立好了。keepalive_timeoutHttpd守护进程,一般都提供了keep-alive timeout时间设置参数。比如nginx的keepalive_timeout,和Apache的KeepAliveTimeout。这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接。Nginx配置中的keepalive_timeout默认为75s,6. cookie与Session这个重点在于理解,这里就不赘述了。7. 网页经历了什么这个东西很多,后面会专门写一篇文章来进行说明。参考文章:HTTP1.0、HTTP1.1 和 HTTP2.0 的区别彻底弄懂HTTP缓存机制及原理What is the difference between memory cache and disk cache in Chrome?理解HTTP之keep-alive ...

October 19, 2018 · 1 min · jiezi

关于MySQL 通用查询日志和慢查询日志分析

MySQL中的日志包括:错误日志、二进制日志、通用查询日志、慢查询日志等等。这里主要介绍下比较常用的两个功能:通用查询日志和慢查询日志。1)通用查询日志:记录建立的客户端连接和执行的语句。2)慢查询日志:记录所有执行时间超过longquerytime秒的所有查询或者不使用索引的查询(1)通用查询日志在学习通用日志查询时,需要知道两个数据库中的常用命令:1) show variables like ‘%general%’;可以查看,当前的通用日志查询是否开启,如果general_log的值为ON则为开启,为OFF则为关闭(默认情况下是关闭的)。1) show variables like ‘%log_output%’;查看当前慢查询日志输出的格式,可以是FILE(存储在数数据库的数据文件中的hostname.log),也可以是TABLE(存储在数据库中的mysql.general_log)问题:如何开启MySQL通用查询日志,以及如何设置要输出的通用日志输出格式呢?开启通用日志查询: set global general_log=on;关闭通用日志查询: set global general_log=off;设置通用日志输出为表方式: set global log_output=’TABLE’;设置通用日志输出为文件方式: set global log_output=’FILE’;设置通用日志输出为表和文件方式:set global log_output=’FILE,TABLE’;(注意:上述命令只对当前生效,当MySQL重启失效,如果要永久生效,需要配置 my.cnf)my.cnf文件的配置如下:general_log=1 #为1表示开启通用日志查询,值为0表示关闭通用日志查询log_output=FILE,TABLE#设置通用日志的输出格式为文件和表(2)慢查询日志MySQL的慢查询日志是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中(日志可以写入文件或者数据库表,如果对性能要求高的话,建议写文件)。默认情况下,MySQL数据库是不开启慢查询日志的,long_query_time的默认值为10(即10秒,通常设置为1秒),即运行10秒以上的语句是慢查询语句。一般来说,慢查询发生在大表(比如:一个表的数据量有几百万),且查询条件的字段没有建立索引,此时,要匹配查询条件的字段会进行全表扫描,耗时查过long_query_time,则为慢查询语句。问题:如何查看当前慢查询日志的开启情况?在MySQL中输入命令:show variables like ‘%quer%’;主要掌握以下的几个参数:(1)slow_query_log的值为ON为开启慢查询日志,OFF则为关闭慢查询日志。(2)slow_query_log_file 的值是记录的慢查询日志到文件中(注意:默认名为主机名.log,慢查询日志是否写入指定文件中,需要指定慢查询的输出日志格式为文件,相关命令为:show variables like ‘%log_output%’;去查看输出的格式)。(3)long_query_time 指定了慢查询的阈值,即如果执行语句的时间超过该阈值则为慢查询语句,默认值为10秒。(4)log_queries_not_using_indexes 如果值设置为ON,则会记录所有没有利用索引的查询(注意:如果只是将log_queries_not_using_indexes设置为ON,而将slow_query_log设置为OFF,此时该设置也不会生效,即该设置生效的前提是slow_query_log的值设置为ON),一般在性能调优的时候会暂时开启。问题:设置MySQL慢查询的输出日志格式为文件还是表,或者两者都有?通过命令:show variables like ‘%log_output%’;通过log_output的值可以查看到输出的格式,上面的值为TABLE。当然,我们也可以设置输出的格式为文本,或者同时记录文本和数据库表中,设置的命令如下:慢查询日志输出到表中(即mysql.slow_log)set globallog_output=’TABLE’;慢查询日志仅输出到文本中(即:slow_query_log_file指定的文件)setglobal log_output=’FILE’;在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多慢查询日志同时输出到文本和表中setglobal log_output=’FILE,TABLE’; 关于慢查询日志的表中的数据个文本中的数据格式分析:慢查询的日志记录myql.slow_log表中,格式如下:慢查询的日志记录到hostname.log文件中,格式如下:可以看到,不管是表还是文件,都具体记录了:是那条语句导致慢查询(sql_text),该慢查询语句的查询时间(query_time),锁表时间(Lock_time),以及扫描过的行数(rows_examined)等信息。问题:如何查询当前慢查询的语句的个数?在MySQL中有一个变量专门记录当前慢查询语句的个数:输入命令:show global status like ‘%slow%’;(注意:上述所有命令,如果都是通过MySQL的shell将参数设置进去,如果重启MySQL,所有设置好的参数将失效,如果想要永久的生效,需要将配置参数写入my.cnf文件中)。补充知识点:如何利用MySQL自带的慢查询日志分析工具mysqldumpslow分析日志?perlmysqldumpslow –s c –t 10 slow-query.log具体参数设置如下:-s 表示按何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒叙;-t 表示top的意思,后面跟着的数据表示返回前面多少条;-g 后面可以写正则表达式匹配,大小写不敏感。上述中的参数含义如下:Count:414 语句出现了414次;Time=3.51s(1454) 执行最长时间为3.51s,累计总耗费时间1454s;Lock=0.0s(0) 等待锁最长时间为0s,累计等待锁耗费时间为0s;Rows=2194.9(9097604) 发送给客户端最多的行数为2194.9,累计发送给客户端的函数为90976404(注意:mysqldumpslow脚本是用perl语言写的,具体mysqldumpslow的用法后期再讲)问题:实际在学习过程中,如何得知设置的慢查询是有效的?很简单,我们可以手动产生一条慢查询语句,比如,如果我们的慢查询log_query_time的值设置为1,则我们可以执行如下语句:selectsleep(1);该条语句即是慢查询语句,之后,便可以在相应的日志输出文件或表中去查看是否有该条语句。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..原文:https://my.oschina.net/u/3575…

October 11, 2018 · 1 min · jiezi

消息中间件—简谈Kafka中的NIO网络通信模型

摘要:很多人喜欢把RocketMQ与Kafka做对比,其实这两款消息队列的网络通信层还是比较相似的,本文就为大家简要地介绍下Kafka的NIO网络通信模型,通过对Kafka源码的分析来简述其Reactor的多线程网络通信模型和总体框架结构,同时简要介绍Kafka网络通信层的设计与具体实现。一、Kafka网络通信模型的整体框架概述Kafka的网络通信模型是基于NIO的Reactor多线程模型来设计的。这里先引用Kafka源码中注释的一段话:相信大家看了上面的这段引文注释后,大致可以了解到Kafka的网络通信层模型,主要采用了 1(1个Acceptor线程)+N(N个Processor线程)+M(M个业务处理线程) 。下面的表格简要的列举了下(这里先简单的看下后面还会详细说明):线程数线程名线程具体说明1kafka-socket-acceptor_%xAcceptor线程,负责监听Client端发起的请求Nkafka-network-thread_%dProcessor线程,负责对Socket进行读写Mkafka-request-handler-_%dWorker线程,处理具体的业务逻辑并生成Response返回Kafka网络通信层的完整框架图如下图所示:Kafka消息队列的通信层模型—1+N+M模型.png刚开始看到上面的这个框架图可能会有一些不太理解,并不要紧,这里可以先对Kafka的网络通信层框架结构有一个大致了解。本文后面会结合Kafka的部分重要源码来详细阐述上面的过程。这里可以简单总结一下其网络通信模型中的几个重要概念:(1), Acceptor :1个接收线程,负责监听新的连接请求,同时注册OP_ACCEPT 事件,将新的连接按照 “round robin” 方式交给对应的 Processor 线程处理;(2), Processor :N个处理器线程,其中每个 Processor 都有自己的 selector,它会向 Acceptor 分配的 SocketChannel 注册相应的 OP_READ 事件,N 的大小由 “num.networker.threads” 决定;(3), KafkaRequestHandler :M个请求处理线程,包含在线程池—KafkaRequestHandlerPool内部,从RequestChannel的全局请求队列—requestQueue中获取请求数据并交给KafkaApis处理,M的大小由 “num.io.threads” 决定;(4), RequestChannel :其为Kafka服务端的请求通道,该数据结构中包含了一个全局的请求队列 requestQueue和多个与Processor处理器相对应的响应队列responseQueue,提供给Processor与请求处理线程KafkaRequestHandler和KafkaApis交换数据的地方。(5), NetworkClient :其底层是对 Java NIO 进行相应的封装,位于Kafka的网络接口层。Kafka消息生产者对象—KafkaProducer的send方法主要调用NetworkClient完成消息发送;(6), SocketServer :其是一个NIO的服务,它同时启动一个Acceptor接收线程和多个Processor处理器线程。提供了一种典型的Reactor多线程模式,将接收客户端请求和处理请求相分离;(7), KafkaServer :代表了一个Kafka Broker的实例;其startup方法为实例启动的入口;(8), KafkaApis :Kafka的业务逻辑处理Api,负责处理不同类型的请求;比如 “发送消息”、 “获取消息偏移量—offset” 和 “处理心跳请求” 等;二、Kafka网络通信层的设计与具体实现这一节将结合Kafka网络通信层的源码来分析其设计与实现,这里主要详细介绍网络通信层的几个重要元素—SocketServer、Acceptor、Processor、RequestChannel和KafkaRequestHandler。本文分析的源码部分均基于Kafka的0.11.0版本。1、SocketServerSocketServer是接收客户端Socket请求连接、处理请求并返回处理结果的核心类,Acceptor及Processor的初始化、处理逻辑都是在这里实现的。在KafkaServer实例启动时会调用其startup的初始化方法,会初始化1个 Acceptor和N个Processor线程(每个EndPoint都会初始化,一般来说一个Server只会设置一个端口),其实现如下:2、AcceptorAcceptor是一个继承自抽象类AbstractServerThread的线程类。Acceptor的主要任务是监听并且接收客户端的请求,同时建立数据传输通道—SocketChannel,然后以轮询的方式交给一个后端的Processor线程处理(具体的方式是添加socketChannel至并发队列并唤醒Processor线程处理)。在该线程类中主要可以关注以下两个重要的变量:(1), nioSelector :通过NSelector.open()方法创建的变量,封装了JAVA NIO Selector的相关操作;(2), serverChannel :用于监听端口的服务端Socket套接字对象;下面来看下Acceptor主要的run方法的源码:在上面源码中可以看到,Acceptor线程启动后,首先会向用于监听端口的服务端套接字对象—ServerSocketChannel上注册OP_ACCEPT 事件。然后以轮询的方式等待所关注的事件发生。如果该事件发生,则调用accept()方法对OP_ACCEPT事件进行处理。这里,Processor是通过 round robin 方法选择的,这样可以保证后面多个Processor线程的负载基本均匀。Acceptor的accept()方法的作用主要如下:(1)通过SelectionKey取得与之对应的serverSocketChannel实例,并调用它的accept()方法与客户端建立连接;(2)调用connectionQuotas.inc()方法增加连接统计计数;并同时设置第(1)步中创建返回的socketChannel属性(如sendBufferSize、KeepAlive、TcpNoDelay、configureBlocking等)(3)将socketChannel交给processor.accept()方法进行处理。这里主要是将socketChannel加入Processor处理器的并发队列newConnections队列中,然后唤醒Processor线程从队列中获取socketChannel并处理。其中,newConnections会被Acceptor线程和Processor线程并发访问操作,所以newConnections是ConcurrentLinkedQueue队列(一个基于链接节点的无界线程安全队列)3、ProcessorProcessor同Acceptor一样,也是一个线程类,继承了抽象类AbstractServerThread。其主要是从客户端的请求中读取数据和将KafkaRequestHandler处理完响应结果返回给客户端。在该线程类中主要关注以下几个重要的变量:(1), newConnections :在上面的 Acceptor 一节中已经提到过,它是一种ConcurrentLinkedQueue[SocketChannel]类型的队列,用于保存新连接交由Processor处理的socketChannel;(2), inflightResponses :是一个Map[String, RequestChannel.Response]类型的集合,用于记录尚未发送的响应;(3), selector :是一个类型为KSelector变量,用于管理网络连接;下面先给出Processor处理器线程run方法执行的流程图:Kafk_Processor线程的处理流程图.png从上面的流程图中能够可以看出Processor处理器线程在其主流程中主要完成了这样子几步操作:(1), 处理newConnections队列中的socketChannel 。遍历取出队列中的每个socketChannel并将其在selector上注册OP_READ事件;(2), 处理RequestChannel中与当前Processor对应响应队列中的Response 。在这一步中会根据responseAction的类型(NoOpAction/SendAction/CloseConnectionAction)进行判断,若为“NoOpAction”,表示该连接对应的请求无需响应;若为“SendAction”,表示该Response需要发送给客户端,则会通过“selector.send”注册OP_WRITE事件,并且将该Response从responseQueue响应队列中移至inflightResponses集合中;“CloseConnectionAction”,表示该连接是要关闭的;(3), 调用selector.poll()方法进行处理 。该方法底层即为调用nioSelector.select()方法进行处理。(4), 处理已接受完成的数据包队列—completedReceives 。在processCompletedReceives方法中调用“requestChannel.sendRequest”方法将请求Request添加至requestChannel的全局请求队列—requestQueue中,等待KafkaRequestHandler来处理。同时,调用“selector.mute”方法取消与该请求对应的连接通道上的OP_READ事件;(5), 处理已发送完的队列—completedSends 。当已经完成将response发送给客户端,则将其从inflightResponses移除,同时通过调用“selector.unmute”方法为对应的连接通道重新注册OP_READ事件;(6), 处理断开连接的队列 。将该response从inflightResponses集合中移除,同时将connectionQuotas统计计数减1;4、RequestChannel在Kafka的网络通信层中,RequestChannel为Processor处理器线程与KafkaRequestHandler线程之间的数据交换提供了一个数据缓冲区,是通信过程中Request和Response缓存的地方。因此,其作用就是在通信中起到了一个数据缓冲队列的作用。Processor线程将读取到的请求添加至RequestChannel的全局请求队列—requestQueue中;KafkaRequestHandler线程从请求队列中获取并处理,处理完以后将Response添加至RequestChannel的响应队列—responseQueue中,并通过responseListeners唤醒对应的Processor线程,最后Processor线程从响应队列中取出后发送至客户端。5、KafkaRequestHandlerKafkaRequestHandler也是一种线程类,在KafkaServer实例启动时候会实例化一个线程池—KafkaRequestHandlerPool对象(包含了若干个KafkaRequestHandler线程),这些线程以守护线程的方式在后台运行。在KafkaRequestHandler的run方法中会循环地从RequestChannel中阻塞式读取request,读取后再交由KafkaApis来具体处理。6、KafkaApisKafkaApis是用于处理对通信网络传输过来的业务消息请求的中心转发组件。该组件反映出Kafka Broker Server可以提供哪些服务。三、总结仔细阅读Kafka的NIO网络通信层的源码过程中还是可以收获不少关于NIO网络通信模块的关键技术。Apache的任何一款开源中间件都有其设计独到之处,值得借鉴和学习。对于任何一位使用Kafka这款分布式消息队列的同学来说,如果能够在一定实践的基础上,再通过阅读其源码能起到更为深入理解的效果,对于大规模Kafka集群的性能调优和问题定位都大有裨益。对于刚接触Kafka的同学来说,想要自己掌握其NIO网络通信层模型的关键设计,还需要不断地使用本地环境进行debug调试和阅读源码反复思考。 ...

September 29, 2018 · 1 min · jiezi

IOC 之深入理解 Spring IoC

IOC 理论IoC 全称为 Inversion of Control,翻译为 “控制反转”,它还有一个别名为 DI(Dependency Injection),即依赖注入。如何理解“控制反转”好呢?理解好它的关键在于我们需要回答如下四个问题:谁控制谁控制什么为何是反转哪些方面反转了在回答这四个问题之前,我们先看 IOC 的定义:所谓 IOC ,就是由 Spring IOC 容器来负责对象的生命周期和对象之间的关系上面这句话是整个 IoC 理论的核心。如何来理解这句话?我们引用一个例子来走阐述(看完该例子上面四个问题也就不是问题了)。已找女朋友为例(对于程序猿来说这个值得探究的问题)。一般情况下我们是如何来找女朋友的呢?首先我们需要根据自己的需求(漂亮、身材好、性格好)找一个妹子,然后到处打听她的兴趣爱好、微信、电话号码,然后各种投其所好送其所要,最后追到手。如下:/*** 年轻小伙子*/public class YoungMan {private BeautifulGirl beautifulGirl;YoungMan(){// 可能你比较牛逼,指腹为婚// beautifulGirl = new BeautifulGirl();}public void setBeautifulGirl(BeautifulGirl beautifulGirl) {this.beautifulGirl = beautifulGirl;}public static void main(String[] args){YoungMan you = new YoungMan();BeautifulGirl beautifulGirl = new BeautifulGirl(“你的各种条件”);beautifulGirl.setxxx(“各种投其所好”);// 然后你有女票了you.setBeautifulGirl(beautifulGirl);}}这就是我们通常做事的方式,如果我们需要某个对象,一般都是采用这种直接创建的方式(new BeautifulGirl()),这个过程复杂而又繁琐,而且我们必须要面对每个环节,同时使用完成之后我们还要负责销毁它,在这种情况下我们的对象与它所依赖的对象耦合在一起。其实我们需要思考一个问题?我们每次用到自己依赖的对象真的需要自己去创建吗?我们知道,我们依赖对象其实并不是依赖该对象本身,而是依赖它所提供的服务,只要在我们需要它的时候,它能够及时提供服务即可,至于它是我们主动去创建的还是别人送给我们的,其实并不是那么重要。再说了,相比于自己千辛万苦去创建它还要管理、善后而言,直接有人送过来是不是显得更加好呢?这个给我们送东西的“人” 就是 IoC,在上面的例子中,它就相当于一个婚介公司,作为一个婚介公司它管理着很多男男女女的资料,当我们需要一个女朋友的时候,直接跟婚介公司提出我们的需求,婚介公司则会根据我们的需求提供一个妹子给我们,我们只需要负责谈恋爱,生猴子就行了。你看,这样是不是很简单明了。诚然,作为婚介公司的 IoC 帮我们省略了找女朋友的繁杂过程,将原来的主动寻找变成了现在的被动接受(符合我们的要求),更加简洁轻便。你想啊,原来你还得鞍马前后,各种巴结,什么东西都需要自己去亲力亲为,现在好了,直接有人把现成的送过来,多么美妙的事情啊。所以,简单点说,IoC 的理念就是让别人为你服务,如下图(摘自Spring揭秘):在没有引入 IoC 的时候,被注入的对象直接依赖于被依赖的对象,有了 IoC 后,两者及其他们的关系都是通过 Ioc Service Provider 来统一管理维护的。被注入的对象需要什么,直接跟 IoC Service Provider 打声招呼,后者就会把相应的被依赖对象注入到被注入的对象中,从而达到 IOC Service Provider 为被注入对象服务的目的。所以 IoC 就是这么简单!原来是需要什么东西自己去拿,现在是需要什么东西让别人(IOC Service Provider)送过来现在在看上面那四个问题,答案就显得非常明显了:谁控制谁:在传统的开发模式下,我们都是采用直接 new 一个对象的方式来创建对象,也就是说你依赖的对象直接由你自己控制,但是有了 IOC 容器后,则直接由 IoC 容器来控制。所以“谁控制谁”,当然是 IoC 容器控制对象。控制什么:控制对象。为何是反转:没有 IoC 的时候我们都是在自己对象中主动去创建被依赖的对象,这是正转。但是有了 IoC 后,所依赖的对象直接由 IoC 容器创建后注入到被注入的对象中,依赖的对象由原来的主动获取变成被动接受,所以是反转。哪些方面反转了:所依赖对象的获取被反转了。妹子有了,但是如何拥有妹子呢?这也是一门学问。可能你比较牛逼,刚刚出生的时候就指腹为婚了。大多数情况我们还是会考虑自己想要什么样的妹子,所以还是需要向婚介公司打招呼的。还有一种情况就是,你根本就不知道自己想要什么样的妹子,直接跟婚介公司说,我就要一个这样的妹子。所以,IOC Service Provider 为被注入对象提供被依赖对象也有如下几种方式:构造方法注入、stter方法注入、接口注入。构造器注入构造器注入,顾名思义就是被注入的对象通过在其构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。YoungMan(BeautifulGirl beautifulGirl){this.beautifulGirl = beautifulGirl;}构造器注入方式比较直观,对象构造完毕后就可以直接使用,这就好比你出生你家里就给你指定了你媳妇。setter 方法注入对于 JavaBean 对象而言,我们一般都是通过 getter 和 setter 方法来访问和设置对象的属性。所以,当前对象只需要为其所依赖的对象提供相对应的 setter 方法,就可以通过该方法将相应的依赖对象设置到被注入对象中。如下:public class YoungMan {private BeautifulGirl beautifulGirl;public void setBeautifulGirl(BeautifulGirl beautifulGirl) {this.beautifulGirl = beautifulGirl;}}相比于构造器注入,setter 方式注入会显得比较宽松灵活些,它可以在任何时候进行注入(当然是在使用依赖对象之前),这就好比你可以先把自己想要的妹子想好了,然后再跟婚介公司打招呼,你可以要林志玲款式的,赵丽颖款式的,甚至凤姐哪款的,随意性较强。接口方式注入接口方式注入显得比较霸道,因为它需要被依赖的对象实现不必要的接口,带有侵入性。一般都不推荐这种方式。各个组件该图为 ClassPathXmlApplicationContext 的类继承体系结构,虽然只有一部分,但是它基本上包含了 IOC 体系中大部分的核心类和接口。下面我们就针对这个图进行简单的拆分和补充说明。Resource体系Resource,对资源的抽象,它的每一个实现类都代表了一种资源的访问策略,如ClasspathResource 、 URLResource ,FileSystemResource 等。有了资源,就应该有资源加载,Spring 利用 ResourceLoader 来进行统一资源加载,类图如下:BeanFactory 体系BeanFactory 是一个非常纯粹的 bean 容器,它是 IOC 必备的数据结构,其中 BeanDefinition 是她的基本结构,它内部维护着一个 BeanDefinition map ,并可根据 BeanDefinition 的描述进行 bean 的创建和管理。BeanFacoty 有三个直接子类 ListableBeanFactory、HierarchicalBeanFactory 和 AutowireCapableBeanFactory,DefaultListableBeanFactory 为最终默认实现,它实现了所有接口。Beandefinition 体系BeanDefinition 用来描述 Spring 中的 Bean 对象。BeandefinitionReader体系BeanDefinitionReader 的作用是读取 Spring 的配置文件的内容,并将其转换成 Ioc 容器内部的数据结构:BeanDefinition。ApplicationContext体系这个就是大名鼎鼎的 Spring 容器,它叫做应用上下文,与我们应用息息相关,她继承 BeanFactory,所以它是 BeanFactory 的扩展升级版,如果BeanFactory 是屌丝的话,那么 ApplicationContext 则是名副其实的高富帅。由于 ApplicationContext 的结构就决定了它与 BeanFactory 的不同,其主要区别有:继承 MessageSource,提供国际化的标准访问策略。继承 ApplicationEventPublisher ,提供强大的事件机制。扩展 ResourceLoader,可以用来加载多个 Resource,可以灵活访问不同的资源。对 Web 应用的支持。上面五个体系可以说是 Spring IoC 中最核心的部分,后面博文也是针对这五个部分进行源码分析。其实 IoC 咋一看还是挺简单的,无非就是将配置文件(暂且认为是 xml 文件)进行解析(分析 xml 谁不会啊),然后放到一个 Map 里面就差不多了,初看有道理,其实要面临的问题还是有很多的,下面就劳烦各位看客跟着 LZ 博客来一步一步揭开 Spring IoC 的神秘面纱。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..原文:http://www.uml.org.cn/j2ee/20… ...

September 27, 2018 · 1 min · jiezi

微服务写的最全的一篇文章

今年有人提出了2018年微服务将疯狂至死,可见微服务的争论从未停止过。在这我将自己对微服务的理解整理了一下,希望对大家有所帮助。1.什么是微服务1)一组小的服务(大小没有特别的标准,只要同一团队的工程师理解服务的标识一致即可)2)独立的进程(java的tomcat,nodejs等)3)轻量级的通信(不是soap,是http协议)4)基于业务能力(类似用户服务,商品服务等等)5)独立部署(迭代速度快)6)无集中式管理(无须统一技术栈,可以根据不同的服务或者团队进行灵活选择)ps:微服务的先行者Netflix公司,开源了一些好的微服务框架,后续会有介绍。2.怎么权衡微服务的利于弊利:强模块边界 。(模块化的演化过程:类–>组件/类库(sdk)–>服务(service),方式越来越灵活)可独立部署。技术多样性。弊:分布式复杂性。最终一致性。(各个服务的团队,数据也是分散式治理,会出现不一致的问题)运维复杂性。测试复杂性。3.企业在什么时候考虑引入微服务从生产力和系统的复杂性这两个方面来看。公司一开始的时候,业务复杂性不高,这时候是验证商业模式的时候,业务简单,用单体服务反而生产力很高。随着公司的发展,业务复杂性慢慢提高,这时候就可以采用微服务来提升生产力了。至于这个转化的点,需要团队的架构师来进行各方面衡量,就个人经验而言,团队发展到百人以上,采用微服务就很有必要了。有些架构师是具有微服务架构能力,所以设计系统时就直接设计成了微服务,而不是通过单服务慢慢演化发展成微服务。在这里我并不推荐这种做法,因为一开始对业务领域并不是很了解,并且业务模式还没有得到验证,这时候上微服务风险比较高,很有可能失败。所以建议大家在单服务的应用成熟时,并且对业务领域比较熟悉的时候,如果发现单服务无法适应业务发展时,再考虑微服务的设计和架构。4.微服务的组织架构如上图左边,传统的企业中,团队是按职能划分的。开发一个项目时,会从不同的职能团队找人进行开发,开发完成后,再各自回到自己的职能团队,这种模式实践证明,效率还是比较低的。如上图右边,围绕每个业务线或产品,按服务划分团队。团队成员从架构到运维,形成一个完整的闭环。一直围绕在产品周围,进行不断的迭代。不会像传统的团队一样离开。这样开发效率会比较高。至于这种团队的规模,建议按照亚马逊的两个披萨原则,大概10人左右比较好。5:怎么理解中台战略和微服务中台战略的由来:马云2015年去欧洲的一家公司supersell参观,发现这个公司的创新能力非常强,团队的规模很小,但是开发效率很高。他们就是采用中台战略。马云感触很深,回国后就在集团内部推出了中台战略。简单的理解就是把传统的前后台体系中的后台进行了细分。阿里巴巴提出了大中台小前台的战略。就是强化业务和技术中台,把前端的应用变得更小更灵活。当中台越强大,能力就越强,越能更好的快速响应前台的业务需求。打个比喻,就是土壤越肥沃,越适合生长不同的生物,打造好的生态系统。6:服务分层每个公司的服务分层都不相同,有的公司服务没有分层,有的怎分层很多。目前业界没有统一的标准。下面推荐一个比较容易理解的两层结构。1:基础服务: 比如一个电商网站,商品服务和订单服务就属于基础服务(核心领域服务)。缓存服务,监控服务,消息队列等也属于基础服务(公共服务)2:聚合服务 :例如网关服务就算一种聚合服务(适配服务)。这是一种逻辑划分,不是物理划分,实际设计的东西很多很复杂。7:微服务的技术架构体系下图是一个成型的互联网微服务的架构体系:1:接入层 负载均衡作用,运维团队负责2:网关层 反向路由,安全验证,限流等3:业务服务层 基础服务和领域服务4:支撑服务层5:平台服务6:基础设施层 运维团队负责。(或者阿里云)8:微服务的服务发现的三种方式第一种:如下图所示,传统的服务发现(大部分公司的做法)。服务上线后,通知运维,申请域名,配置路由。调用方通过dns域名解析,经过负载均衡路由,进行服务访问。缺点: LB的单点风险,服务穿透LB,性能也不是太好第二种:也叫客户端发现方式。如下图所示。通过服务注册的方式,服务提供者先注册服务。消费者通过注册中心获取相应服务。并且把LB的功能移动到了消费者的进程内,消费者根据自身路由去获取相应服务。优点是,没有了LB单点问题,也没有了LB的中间一跳,性能也比较好。但是这种方式有一个非常明显的缺点就是具有非常强的耦合性。针对不同的语言,每个服务的客户端都得实现一套服务发现的功能。第三种:也叫服务端发现方式,如下图所示。和第二种很相似。但是LB功能独立进程单独部署,所以解决了客户端多语言开发的问题。唯一的缺点就是运维成比较高,每个节点都得部署一个LB的代理,例如nginx。9.微服务网关网关就好比一个公司的门卫。屏蔽内部细节,统一对外服务接口。下图是一个网关所处位置的示例图。10.Netflix Zuul网关介绍核心就是一个servlet,通过filter机制实现的。主要分为三类过滤器:前置过滤器,过滤器和后置过滤器。主要特色是,这些过滤器可以动态插拔,就是如果需要增加减少过滤器,可以不用重启,直接生效。原理就是:通过一个db维护过滤器(上图蓝色部分),如果增加过滤器,就将新过滤器编译完成后push到db中,有线程会定期扫描db,发现新的过滤器后,会上传到网关的相应文件目录下,并通知过滤器loader进行加载相应的过滤器。整个网关调用的流程上图从左变http Request开始经过三类过滤器,最终到最右边的Http Response,这就是Zull网关的整个调用流程。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多11:微服务的路由发现体系整个微服务的路由发现体系,一般由服务注册中心和网关两部分组成。以NetFlix为例子,Eureka和Zull这两个组件支撑了netFlix整个的路由发现体系。如下图所示,首先外部请求发送到网关,网关去服务注册中心获取相应的服务,进行调用。其次内部服务间的调用,也通过服务注册中心进行的12.微服务配置中心目前大部分公司都是把配置写到配置文件中,遇到修改配置的情况,成本很高。并且没有修改配置的记录,出问题很难追溯。配置中心就接解决了以上的问题。可配置内容:数据库连接,业务参数等等配置中心就是一个web服务,配置人员通过后台页面修改配置,各个服务就会得到新的配置参数。实现方式主要有两种,一种是push,另一种是pull。两张方式各有优缺点。push实时性较好,但是遇到网络抖动,会丢失消息。pull不会丢失消息但是实时性差一些。大家可以同时两种方式使用,实现一个比较好的效果。如下图所示,这是一个国内知名互联网公司的配置中心架构图。开源地址:http://github.com/ctripcorp/a…13:RPC遇到了REST内部一些核心服务,性能要求比较高的可以采用RPC,对外服务的一般可以采用rest。14:服务框架和治理微服务很多的时候,就需要有治理了。一个好的微服务框架一般分为以下14个部分。如下图所示。这就是开篇所说的,微服务涉及的东西很多,有些初创公司和业务不成熟的产品是不太适合的,成本比较高。目前国内比较好的微服务框架就是阿里巴巴的DUBBO了,国外的就是spring cloud,大家可以去研究一下.15:监控体系监控是微服务治理的重要环节。一般分为以下四层。如下图所示。监控的内容分为五个部分:日志监控,Metrics监控(服务调用情况),调用链监控,告警系统和健康检查。日志监控,国内常用的就是ELK+KAFKA来实现。健康检查和Metrics,像spring boot会自带。Nagios也是一个很好的开源监控框架。16:Trace调用链监控调用链监控是用来追踪微服务之前依赖的路径和问题定位。例如阿里的鹰眼系统。主要原理就是子节点会记录父节点的id信息。下图是目前比较流行的调用链监控框架。17:微服务的限流熔断假设服务A依赖服务B和服务C,而B服务和C服务有可能继续依赖其他的服务,继续下去会使得调用链路过长。如果在A的链路上某个或几个被调用的子服务不可用或延迟较高,则会导致调用A服务的请求被堵住,堵住的请求会消耗占用掉系统的线程、io等资源,当该类请求越来越多,占用的计算机资源越来越多的时候,会导致系统瓶颈出现,造成其他的请求同样不可用,最终导致业务系统崩溃。一般情况对于服务依赖的保护主要有两种方式:熔断和限流。目前最流行的就是Hystrix的熔断框架。下图是Hystrix的断路器原理图:限流方式可以采用zuul的API限流方法。18.Docker 容器部署技术&持续交付流水线随着微服务的流行,容器技术也相应的被大家重视起来。容器技术主要解决了以下两个问题:1:环境一致性问题。例如java的jar/war包部署会依赖于环境的问题(操着系统的版本,jdk版本问题)。2:镜像部署问题。例如java,rubby,nodejs等等的发布系统是不一样的,每个环境都得很麻烦的部署一遍,采用docker镜像,就屏蔽了这类问题。下图是Docker容器部署的一个完整过程。更重要的是,拥有如此多服务的集群环境迁移、复制也非常轻松,只需选择好各服务对应的Docker服务镜像、配置好相互之间访问地址就能很快搭建出一份完全一样的新集群。19.容器调度和发布体系目前基于容器的调度平台有Kubernetes,mesos,omega。下图是mesos的一个简单架构示意图。下图是一个完整的容器发布体系 大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..

September 27, 2018 · 1 min · jiezi

良好的RPC接口设计,需要注意这些方面

RPC 框架的讨论一直是各个技术交流群中的热点话题,阿里的 dubbo,新浪微博的 motan,谷歌的 grpc,以及不久前蚂蚁金服开源的 sofa,都是比较出名的 RPC 框架。RPC 框架,或者一部分人习惯称之为服务治理框架,更多的讨论是存在于其技术架构,比如 RPC 的实现原理,RPC 各个分层的意义,具体 RPC 框架的源码分析…但却并没有太多话题和“如何设计 RPC 接口”这样的业务架构相关。可能很多小公司程序员还是比较关心这个问题的,这篇文章主要分享下一些个人眼中 RPC 接口设计的最佳实践。初识 RPC 接口设计由于 RPC 中的术语每个程序员的理解可能不同,所以文章开始,先统一下 RPC 术语,方便后续阐述。大家都知道共享接口是 RPC 最典型的一个特点,每个服务对外暴露自己的接口,该模块一般称之为 api;外部模块想要实现对该模块的远程调用,则需要依赖其 api;每个服务都需要有一个应用来负责实现自己的 api,一般体现为一个独立的进程,该模块一般称之为 app。api 和 app 是构建微服务项目的最简单组成部分,如果使用 maven 的多 module 组织代码,则体现为如下的形式。serviceA 服务serviceA/pom.xml 定义父 pom 文件 <modules> <module>serviceA-api</module> <module>serviceA-app</module></modules><packaging>pom</packaging><groupId>moe.cnkirito</groupId><artifactId>serviceA</artifactId><version>1.0.0-SNAPSHOT</version>serviceA/serviceA-api/pom.xml 定义对外暴露的接口,最终会被打成 jar 包供外部服务依赖 <parent> <artifactId>serviceA</artifactId> <groupId>moe.cnkirito</groupId> <version>1.0.0-SNAPSHOT</version></parent><packaging>jar</packaging><artifactId>serviceA-api</artifactId>serviceA/serviceA-app/pom.xml 定义了服务的实现,一般是 springboot 应用,所以下面的配置文件中,我配置了 springboot 应用打包的插件,最终会被打成 jar 包,作为独立的进程运行。 <parent> <artifactId>serviceA</artifactId> <groupId>moe.cnkirito</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <packaging>jar</packaging> <artifactId>serviceA-app</artifactId> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>麻雀虽小,五脏俱全,这样一个微服务模块就实现了。旧 RPC 接口的痛点统一好术语,这一节来描述下我曾经遭遇过的 RPC 接口设计的痛点,相信不少人有过相同的遭遇。查询接口过多各种 findBy 方法,加上各自的重载,几乎占据了一个接口 80% 的代码量。这也符合一般人的开发习惯,因为页面需要各式各样的数据格式,加上查询条件差异很大,便造成了:一个查询条件,一个方法的尴尬场景。这样会导致另外一个问题,需要使用某个查询方法时,直接新增了方法,但实际上可能这个方法已经出现过了,隐藏在了令人眼花缭乱的方法中。难以扩展接口的任何改动,比如新增一个入参,都会导致调用者被迫升级,这也通常是 RPC 设计被诟病的一点,不合理的 RPC 接口设计会放大这个缺点。升级困难在之前的 “初识 RPC 接口设计”一节中,版本管理的粒度是 project,而不是 module,这意味着:api 即使没有发生变化,app 版本演进,也会造成 api 的被迫升级,因为 project 是一个整体。问题又和上一条一样了,api 一旦发生变化,调用者也得被迫升级,牵一发而动全身。难以测试接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护。特别是对于那些有良好习惯编写单元测试的程序员而言,简直是噩梦,用例也得跟着改。异常设计不合理在既往的工作经历中曾经有一次会议,就 RPC 调用中的异常设计引发了争议,一派人觉得需要有一个业务 CommonResponse,封装异常,每次调用后,优先判断调用结果是否 success,在进行业务逻辑处理;另一派人觉得这比较麻烦,由于 RPC 框架是可以封装异常调用的,所以应当直接 try catch 异常,不需要进行业务包裹。在没有明确规范时,这两种风格的代码同时存在于项目中,十分难看!单参数接口如果你使用过 springcloud ,可能会不适应 http 通信的限制,因为 @RequestBody 只能使用单一的参数,也就意味着,springcloud 构建的微服务架构下,接口天然是单参数的。而 RPC 方法入参的个数在语法层面是不会受到限制的,但如果强制要求入参为单参数,会解决一部分的痛点。使用 Specification 模式解决查询接口过多的问题public interface StudentApi{Student findByName(String name);List<Student> findAllByName(String name);Student findByNameAndNo(String name,String no);Student findByIdcard(String Idcard);}如上的多个查询方法目的都是同一个:根据条件查询出 Student,只不过查询条件有所差异。试想一下,Student 对象假设有 10 个属性,最坏的情况下它们的排列组合都可能作为查询条件,这便是查询接口过多的根源。public interface StudentApi{Student findBySpec(StudentSpec spec);List<Student> findListBySpec(StudentListSpec spec);Page<Student> findPageBySpec(StudentPageSpec spec);}上述接口便是最通用的单参接口,三个方法几乎囊括了 99% 的查询条件。所有的查询条件都被封装在了 StudentSpec,StudentListSpec,StudentPageSpec 之中,分别满足了单对象查询,批量查询,分页查询的需求。如果你了解领域驱动设计,会发现这里借鉴了其中 Specification 模式的思想。单参数易于做统一管理 public interface SomeProvider {void opA(ARequest request);void opB(BRequest request);CommonResponse<C> opC(CRequest request); }入参中的入参虽然形态各异,但由于是单个入参,所以可以统一继承 AbstractBaseRequest,即上述的 ARequest,BRequest,CRequest 都是 AbstractBaseRequest 的子类。在千米内部项目中,AbstractBaseRequest 定义了 traceId、clientIp、clientType、operationType 等公共入参,减少了重复命名,我们一致认为,这更加的 OO。有了 AbstractBaseRequest,我们可以更加轻松地在其之上做 AOP,千米的实践中,大概做了如下的操作:请求入参统一校验(request.checkParam(); param.checkParam();)实体变更统一加锁,降低锁粒度请求分类统一处理(if (request instanceof XxxRequest))请求报文统一记日志(log.setRequest(JsonUtil.getJsonString(request)))操作成功统一发消息如果不遵守单参数的约定,上述这些功能也并不是无法实现,但所需花费的精力远大于单参数,一个简单的约定带来的优势,我们认为是值得的。单参数入参兼容性强还记得前面的小节中,我提到了 SpringCloud,在 SpringCloud Feign 中,接口的入参通常会被 @RequestBody 修饰,强制做单参数的限制。千米内部使用了 Dubbo 作为 Rpc 框架,一般而言,为 Dubbo 服务设计的接口是不能直接用作 Feign 接口的(主要是因为 @RequestBody 的限制),但有了单参数的限制,便使之成为了可能。为什么我好端端的 Dubbo 接口需要兼容 Feign 接口?可能会有人发出这样的疑问,莫急,这样做的初衷当然不是为了单纯做接口兼容,而是想充分利用 HTTP 丰富的技术栈以及一些自动化工具。自动生成 HTTP 接口实现(让服务端同时支持 Dubbo 和 HTTP 两种服务接口)看过我之前文章的朋友应该了解过一个设计:千米内部支持的是 Dubbo 协议和 HTTP 协议族(如 JSON RPC 协议,Restful 协议),这并不意味着程序员需要写两份代码,我们可以通过 Dubbo 接口自动生成 HTTP 接口,体现了单参数设计的兼容性之强。通过 Swagger UI 实现对 Dubbo 接口的可视化便捷测试又是一个兼容 HTTP 技术栈带来的便利,在 Restful 接口的测试中,Swagger 一直是备受青睐的一个工具,但可惜的是其无法对 Dubbo 接口进行测试。兼容 HTTP 后,我们只需要做一些微小的工作,便可以实现 Swagger 对 Dubbo 接口的可视化测试。有利于 TestNg 集成测试自动生成 TestNG 集成测试代码和缺省测试用例,这使得服务端接口集成测试变得异常简单,程序员更能集中精力设计业务用例,结合缺省用例、JPA 自动建表和 PowerMock 模拟外部依赖接口实现本机环境。这块涉及到了公司内部的代码,只做下简单介绍,我们一般通过内部项目 com.qianmi.codegenerator:api-dubbo-2-restful ,com.qianmi.codegenerator:api-request-json 生成自动化的测试用例,方便测试。而这些自动化工具中大量使用了反射,而由于单参数的设计,反射用起来比较方便。接口异常设计首先肯定一点,RPC 框架是可以封装异常的,Exception 也是返回值的一部分。在 go 语言中可能更习惯于返回 err,res 的组合,但 JAVA 中我个人更偏向于 try catch 的方法捕获异常。RPC 接口设计中的异常设计也是一个注意点。初始方案 public interface ModuleAProvider { void opA(ARequest request); void opB(BRequest request); CommonResponse<C> opC(CRequest request); }我们假设模块 A 存在上述的 ModuleAProvider 接口,ModuleAProvider 的实现中或多或少都会出现异常,例如可能存在的异常 ModuleAException,调用者实际上并不知道 ModuleAException 的存在,只有当出现异常时,才会知晓。对于 ModuleAException 这种业务异常,我们更希望调用方能够显示的处理,所以 ModuleAException 应该被设计成 Checked Excepition。正确的异常设计姿势public interface ModuleAProvider {void opA(ARequest request) throws ModuleAException;void opB(BRequest request) throws ModuleAException;CommonResponse<C> opC(CRequest request) throws ModuleAException;}上述接口中定义的异常实际上也是一种契约,契约的好处便是不需要叙述,调用方自然会想到要去处理 Checked Exception,否则连编译都过不了。调用方的处理方式在 ModuleB 中,应当如下处理异常: public class ModuleBService implements ModuleBProvider {@ReferenceModuleAProvider moduleAProvider;@Overridepublic void someOp() throws ModuleBexception{ try{ moduleAProvider.opA(…); }catch(ModuleAException e){ throw new ModuleBException(e.getMessage()); }}@Overridepublic void anotherOp(){ try{ moduleAProvider.opB(…); }catch(ModuleAException e){ // 业务逻辑处理 }}}someOp 演示了一个异常流的传递,ModuleB 暴露出去的异常应当是 ModuleB 的 api 模块中异常类,虽然其依赖了 ModuleA ,但需要将异常进行转换,或者对于那些意料之中的业务异常可以像 anotherOp() 一样进行处理,不再传递。这时如果新增 ModuleC 依赖 ModuleB,那么 ModuleC 完全不需要关心 ModuleA 的异常。异常与熔断作为系统设计者,我们应该认识到一点: RPC 调用,失败是常态。通常我们需要对 RPC 接口做熔断处理,比如千米内部便集成了 Netflix 提供的熔断组件 Hystrix。Hystrix 需要知道什么样的异常需要进行熔断,什么样的异常不能够进行熔断。在没有上述的异常设计之前,回答这个问题可能还有些难度,但有了 Checked Exception 的契约,一切都变得明了清晰了。public class ModuleAProviderProxy {@Referenceprivate ModuleAProvider moduleAProvider;@HystrixCommand(ignoreExceptions = {ModuleAException.class})public void opA(ARequest request) throws ModuleAException { moduleAProvider.opA(request);}@HystrixCommand(ignoreExceptions = {ModuleAException.class})public void opB(BRequest request) throws ModuleAException { moduleAProvider.oBB(request);}@HystrixCommand(ignoreExceptions = {ModuleAException.class})public CommonResponse<C> opC(CRequest request) throws ModuleAException { return moduleAProvider.opC(request);}}如服务不可用等原因引发的多次接口调用超时异常,会触发 Hystrix 的熔断;而对于业务异常,我们则认为不需要进行熔断,因为对于接口 throws 出的业务异常,我们也认为是正常响应的一部分,只不过借助于 JAVA 的异常机制来表达。实际上,和生成自动化测试类的工具一样,我们使用了另一套自动化的工具,可以由 Dubbo 接口自动生成对应的 Hystrix Proxy。我们坚定的认为开发体验和用户体验一样重要,所以公司内部会有非常多的自动化工具。API 版本单独演进引用一段公司内部的真实对话:A:我下载了你们的代码库怎么编译不通过啊,依赖中 xxx-api-1.1.3 版本的 jar 包找不到了,那可都是 RELEASE 版本啊。B:你不知道我们 nexus 容量有限,只能保存最新的 20 个 RELEASE 版本吗?那个 API 现在最新的版本是 1.1.31 啦。A:啊,这才几个月就几十个 RELEASE 版本啦?这接口太不稳定啦。B: 其实接口一行代码没改,我们业务分析是很牛逼的,一直很稳定。但是这个 API是和我们项目一起打包的,我们需求更新一次,就发布一次,API 就被迫一起升级版本。发生这种事,大家都不想的。在单体式架构中,版本演进的单位是整个项目。微服务解决的一个关键的痛点便是其做到了每个服务的单独演进,这大大降低了服务间的耦合。正如我文章开始时举得那个例子一样:serviceA 是一个演进的单位,serviceA-api 和 serviceA-app 这两个 Module 从属于 serviceA,这意味着 app 的一次升级,将会引发 api 的升级,因为他们是共生的!而从微服务的使用角度来看,调用者关心的是 api 的结构,而对其实现压根不在乎。所以对于 api 定义未发生变化,其 app 发生变化的那些升级,其实可以做到对调用者无感知。在实践中也是如此api 版本的演进应该是缓慢的,而 app 版本的演进应该是频繁的。所以,对于这两个演进速度不一致的模块,我们应该单独做版本管理,他们有自己的版本号。问题回归查询接口过多各种 findBy 方法,加上各自的重载,几乎占据了一个接口 80% 的代码量。这也符合一般人的开发习惯,因为页面需要各式各样的数据格式,加上查询条件差异很大,便造成了:一个查询条件,一个方法的尴尬场景。这样会导致另外一个问题,需要使用某个查询方法时,直接新增了方法,但实际上可能这个方法已经出现过了,隐藏在了令人眼花缭乱的方法中。解决方案:使用单参+Specification 模式,降低重复的查询方法,大大降低接口中的方法数量。难以扩展接口的任何改动,比如新增一个入参,都会导致调用者被迫升级,这也通常是 RPC 设计被诟病的一点,不合理的 RPC 接口设计会放大这个缺点。解决方案:单参设计其实无形中包含了所有的查询条件的排列组合,可以直接在 app 实现逻辑的新增,而不需要对 api 进行改动(如果是参数的新增则必须进行 api 的升级,参数的废弃可以用 @Deprecated 标准)。升级困难在之前的 “初识 RPC 接口设计”一节中,版本管理的粒度是 project,而不是 module,这意味着:api 即使没有发生变化,app 版本演进,也会造成 api 的被迫升级,因为 project 是一个整体。问题又和上一条一样了,api 一旦发生变化,调用者也得被迫升级,牵一发而动全身。解决方案:以 module 为版本演进的粒度。api 和 app 单独演进,减少调用者的不必要升级次数。难以测试接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护。特别是对于那些有良好习惯编写单元测试的程序员而言,简直是噩梦,用例也得跟着改。解决方案:单参数设计+自动化测试工具,打造良好的开发体验。异常设计不合理在既往的工作经历中曾经有一次会议,就 RPC 调用中的异常设计引发了争议,一派人觉得需要有一个业务 CommonResponse,封装异常,每次调用后,优先判断调用结果是否 success,在进行业务逻辑处理;另一派人觉得这比较麻烦,由于 RPC 框架是可以封装异常调用的,所以应当直接 try catch 异常,不需要进行业务包裹。在没有明确规范时,这两种风格的代码同时存在于项目中,十分难看!解决方案:Checked Exception+正确异常处理姿势,使得代码更加优雅,降低了调用方不处理异常带来的风险。原文出处:https://www.jianshu.com/p/dca…作者:占小狼 ...

September 24, 2018 · 3 min · jiezi