5.1本章目标

5.2缓存设计原则概览

缓存设计原则:

用快速存取设备,用内存
将缓存推到离用户最近的地方
脏缓存清理

我们的项目采用多级缓存的架构

第一级 Redis缓存
Redis缓存有集中管理缓存的特点,是常见NoSql数据库组件

第二级 热点缓存本地缓存
热点数据存到JVM本地缓存中

第三级 nginx proxy cache缓存
所有数据最后都会在nginx服务器上做反向代理,nginx服务器也可以开启proxy cache缓存

第四级 nginx lua缓存
nginx定制lua脚本做nginx内存缓存

5.3 Redis集中式缓存介绍

Redis是一个NoSql 基于Key-valule数据库的中间件,是易失的

Redis缓存的几种形式:

1.单机版

单个redis,目前项目就采用这种设计
优点:架构简单,方便,高性能

缺点:缓存中使用,重启后会丢失,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务+受CPU处理能力限制,CPU性能有瓶颈

2.Redis sentinal哨兵模式

sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求


Redis支持主从同步机制,如上图所示redis2作为redis1的slave从机,同步复制master的内容,当其中一个数据库宕机,应用服务器是很难直接通过找地址来切换成redis2,这时就用到了redis sentinal 哨兵机制。sentinal与redis1和redis2建立长连接,与主机连接是心跳机制,miaosha.jar无需知道redis1,redis2主从关系,只需ask redis sentinal,之后sentinal就response回应redis1为master,redis2为slave


一旦发生redis1坏掉或者发生网络异常,心跳机制就会破坏掉,sentinal更改redis2为master,redis1为slave,变换主从关系,然后发送change给应用服务器,然后miaosha.jar就向redis2进行get、set操作(或者redis读写分离,在master上set,slave上get)——redis 哨兵机制

总结一下

Sentinal作用:
Master状态检测
如果Master异常,则会进行Master-Slave切换,将其中一个Slave作为Master,将之前的Master作为Slave

3.Redis集群cluster模式

但是当数据量过大一个主机放不下的时候,就需要对数据进行分区,将key按照一定的规则进行计算,并将key对应的value分配到指定的Redis实例上,这样的模式简称Redis集群。

cluster集群配置有多个slave用来读,多个master用来写,各种redis服务器彼此知道相互关系、也知道自己的主从关系。每个master管理不同的数据。

1. 节点故障判断

首先,在Redis Cluster中每个节点都存有集群中所有节点的信息。它们之间通过互相ping-pong判断节点是否可以连接。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机。

2.slave选举

当主节点被集群公认为fail状态,那么它的从节点就会发起竞选,如果存在多个从节点,数据越新的节点越有可能发起竞选。集群中其他主节点返回响应信息。

3.结构变更

当竞选从节点收到过半主节点同意,便会成为新的主节点。此时会以最新的Epoch通过PONG消息广播,让Redis Cluster的其他节点尽快的更新集群信息。当原主节点恢复加入后会降级为从节点。

cluster好处:
将数据自动切分到多个节点
当集群某台设备故障时,仍然可以处理请求
节点的fail是集群中超过半数的节点检测失效时才生效

5.4/5 Redis集中式缓存商品详情页接入

在商品详情页添加redis缓存

ItemController:

 //商品详情页浏览@RequestMapping(value = "/get", method = {RequestMethod.GET})@ResponseBodypublic CommonReturnType getItem(@RequestParam(name = "id")Integer id){ItemModel itemModel = null;//根据商品的id到redis中获取itemModel = (ItemModel)redisTemplate.opsForValue().get("item_"+id);//若redis内不存在对应的itemmodel,则访问下游的serviceif(itemModel == null){itemModel = itemService.getItemById(id);//设置itemModel到redis内redisTemplate.opsForValue().set("item_"+id,itemModel);//redis缓存失效时间——十分钟,为了更好的节约空间和服务器负担redisTemplate.expire("item_"+id,10, TimeUnit.MINUTES);}ItemVO itemVO = convertVOFromModel(itemModel);return CommonReturnType.create(itemVO);}

与此同时,ItemModel、PromoModel要实现序列化,(implements Serializable)

这样可以实现使用网页端的redis进行十分钟内的暂时存取。

查看redis写入的数据:

flushall命令可以在redis中清除所有的key-value对

此时是因为序列化的时候没有使用的redis默认的序列化编码方式,所以为了能够让redis能够认清我们的定义,方便的debug,使用我们自己定义的序列化方式,打开并修改config文件夹下的RedisConfig,重写RedisTemplate

//加载这个bean
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisTemplate redisTemplate = new RedisTemplate();redisTemplate.setConnectionFactory(redisConnectionFactory);//为了方便查看redis中的缓存内容,要String序列化缓存数据//解决key的序列化方式StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(stringRedisSerializer);//解决value的序列化方式Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//转化PromoModel里面的String类型的时间ObjectMapper objectMapper =  new ObjectMapper();SimpleModule simpleModule = new SimpleModule();simpleModule.addSerializer(DateTime.class,new JodaDateTimeJsonSerializer());simpleModule.addDeserializer(DateTime.class,new JodaDateTimeJsonDeserializer());objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);objectMapper.registerModule(simpleModule);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);return redisTemplate;}}

5.6 Redis集中式缓存压测

采用Jmeter压测工具发现,Average耗时为250多ms,Tps 2000/s 采用top -H查看发现redisserver占用的cpu只有2%,没有到达瓶颈

5.7/8 本地数据热点缓存

  • 热点数据
  • 脏读非常不敏感
  • 内存可控

本地数据热点缓存的解决方案类似于hashmap,key是item_id,value装的是itemModel。而且还要解决高并发问题,我们想到有Concurrenthashmap,为什么不用呢?

1.Concurrenthashmap是分段锁,在JDK1.8之前,采用的是Segment+HashEntry+ReentrantLock实现的,在1.8后采用Node+CAS+Synchronized实现,get操作没有加锁,put锁加上后,会对读锁性能有影响
2.热点数据缓存要设置过期时间

Google公司推出了一款Guava cache组件,本质上也是一种可并发的hashmap,特点有:

1.可控制的大小和超过时间
2.可配置的LRU策略(最近最少访问策略,用于内存不足的淘汰机制)
3.线程安全

在pom文件中引入依赖

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>18.0</version>
</dependency>

在service包下CacheService接口和CacheServiceImpl,实现读和写两种操作

CacheService.java

//封装本地缓存操作类
public interface CacheService {//存方法void setCommonCache(String key,Object value);//取方法Object getFromCommonCache(String key);
}

CacheServiceImpl.java

@Service
public class CacheServiceImpl implements CacheService {private Cache<String,Object> commonCache = null;//spring bean在加载的时候会优先加载这里init方法@PostConstructpublic void init(){commonCache = CacheBuilder.newBuilder()//设置缓存容齐的初始容量是10.initialCapacity(10)//设置缓存中最大可以存储100个KEY,超过100个之后,会按照LRU的策略一处缓存项.maximumSize(100)//写入缓存之后多少秒过期.expireAfterWrite(60, TimeUnit.SECONDS).build();}@Overridepublic void setCommonCache(String key, Object value) {commonCache.put(key,value);}@Overridepublic Object getFromCommonCache(String key) {return commonCache.getIfPresent(key);}
}

之后在ItemController类中实现的原理就是:

//商品详情页浏览//多级缓存,本地取不到就取redis,redis取不到就取数据库@RequestMapping(value = "/get", method = {RequestMethod.GET})@ResponseBodypublic CommonReturnType getItem(@RequestParam(name = "id")Integer id){ItemModel itemModel = null;//先取本地缓存itemModel = (ItemModel) cacheService.getFromCommonCache("item_"+id);if(itemModel==null){//根据商品的id到redis中获取itemModel = (ItemModel)redisTemplate.opsForValue().get("item_"+id);//若redis内不存在对应的itemmodel,则访问下游的serviceif(itemModel == null){itemModel = itemService.getItemById(id);//设置itemModel到redis内redisTemplate.opsForValue().set("item_"+id,itemModel);//redis缓存失效时间——十分钟,为了更好的节约空间和服务器负担redisTemplate.expire("item_"+id,10, TimeUnit.MINUTES);}//填充本地缓存cacheService.setCommonCache("item_"+id,itemModel);}ItemVO itemVO = convertVOFromModel(itemModel);return CommonReturnType.create(itemVO);}

5.9 本地缓存压测验证

线程数1000,ramp-up时间:5s,循环次数:60
Average time 150ms,对应的Tps:3500/s,对应的redis几乎没有任何压力,
缓存机制从redis缓存加载到了jvm缓存之后,减少了多段的网络开销,并且完成了对应的内存访问输出结果,性能提升明显,但是数据更新之后缓存失效,还有JVM容量大小的限制;

5.10 nginx proxy cache缓存实现及压测结果验证

此时我们已经做到热点数据、redis缓存、数据库三种存取关系,主要流程是,H5客户端发送获取数据的请求,发送到nginx上,nginx反向代理发送给对应的运行着miaosha.jar程序的服务器上,之后查找是否有热点数据,没有热点数据就查询对应的redis,没有redis之后,再去查找数据库。如下图所示。

此时有一个新的优化方法,就是将热点数据存取从miaosha.jar服务器上,搬给nginx服务器上,这样速度会更快

启用nginx缓存的条件:

  • nginx可以用作反向代理
  • 依靠文件系统存索引级的文件(将请求存成本地文件,存放在本地磁盘中,下一次请求过来之后,直接查看文件)
  • 依靠内存缓存文件地址(内存缓存文件的内容value是以文件形式存放在磁盘中,但是缓存的key以缓存的方式在内存中,缓存key在内存的内容就是 —内存缓存文件的地址)也就是说nginx proxy cahce 寻址的key在内存当中,value在磁盘中,key内存中存储的是value的地址

缓存实现

首先连接到nginx反向代理的服务器

在nginx的conf文件nginx.conf增加下列代码

# 声明一个cache缓存节点到内存
# 这个路径可以被作为一级路径,也可以作为二级路径
# 给在内存中开辟100m的空间存放key
# 生存时间是7天
# value总值最大为10G,大于10G之后,会开始LRU的淘汰算法。
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;location / {proxy_cache tmp_cache;proxy_cache_key &uri;proxy_cache_valid 200 206 304 302 7d;//只有后端返回的状态码是这些,对应的cache操作才会生效,缓存周期7天
}

sbin/nginx -s reload重启服务器

性能压测

nginx的缓存本质上缓存读取的内容还是本地的文件(企业级运用是NAS),并没有把对应的文件缓存在nginx内存中,所以不如nginx反向代理存的内容更高效。不是很理想

所以放弃这个改动

5.11-5.14 nginx lua原理和实战

协程机制

协程又叫微线程,最近几年在Lua脚本中得以广泛应用。协程,区别于子程序的层级调用,执行过程中,在子程序内部可中断,然后转而执行其他子程序,在适当的时候再返回来接着执行。

  • 协程不是内部函数调用,类似于中断机制
  • 协程区别于多线程就是不需要锁机制,只在某个线程内部,省去了线程切换的开销
  • 多进程+协程 发挥协程的高效性

总结一下:

依附于线程的内存模型,切换开销小
遇到阻塞归还对应的执行权限,代码同步
协程在线程中串行访问,无需加锁

nginx协程机制

(1)nginx的每一个Worker进程都是在epoll或queue这种事件模型之上,封装成协程;
(2)每一个请求都有一个协程进行处理
(3)即使ngx_lua需要运行lua,相对与C有一定的开销,但依旧能保证高并发的能力;

运行机制:

  • nginx每个工作进程创建一个lua虚拟机
  • 工作进程内的所有协程共享同一个vm
  • 每一个外部请求都是由一个lua协程处理,之间数据隔离;
  • lua代码调用io等异步接口时,协程被挂起,上下文数据保持不变;
  • 自动保存,不阻塞工作进程
  • io异步操作完成后还原协程上下文,代码继续执行

在普通的http请求中,一个线程对应一个请求,多线程互相之间是异步操作,在nginx中,虽然nginx是单线程模型,但是线程内部多协程操作会显得更高效。

typedef enum {NGX_HTTP_POST_READ_PHASE = 0,   //读取请求头,例如get还是post,cookie中有哪些方法NGX_HTTP_SERVER_REWRITE_PHASE,   //执行rewrite -> rewrite_handler,uri与location匹配前,修改uri的阶段,用于重定向NGX_HTTP_FIND_CONFIG_PHASE,  //根据uri替换locationNGX_HTTP_REWRITE_PHASE,      //根据替换结果继续执行rewrite -> rewrite_handler,上一阶段找到location块后再修改uriNGX_HTTP_POST_REWRITE_PHASE, //执行rewrite后处理,防止重写URL后导致的死循环NGX_HTTP_PREACCESS_PHASE,    //认证预处理   请求限制,连接限制 -limit_conn_handler -limit_req_handlerNGX_HTTP_ACCESS_PHASE,       //认证处理 - auth_basic_handler,access_handler,让HTTP模块判断是否允许这个请求进入Nginx服务器NGX_HTTP_POST_ACCESS_PHASE,  //认证后处理, 认证不通过, 丢包, 向用户发送拒绝服务的错误码,用来响应上一阶段的拒绝NGX_HTTP_TRY_FILES_PHASE,    //尝试try标签,为访问静态文件资源而设置NGX_HTTP_CONTENT_PHASE,      //内容处理 - static_handler 处理HTTP请求内容的阶段NGX_HTTP_LOG_PHASE           //日志处理 - log_handler 处理完请求后的日志记录阶段
} ngx_http_phases;

nginx lua插载点

Nginx提供了许多再执行lua脚本的挂载方案,用的最多的几个nginx lua插载点

  • init_by_lua:系统启动时调用;
  • init_worker_by_lua:worker进程启动时调用;
  • set_by_lua:nginx变量用复杂lua return
  • access_by_lua:权限验证阶段
  • content_by_lua:内容输出结点(重要)

在conf/nginx.conf中增加下图中红框语句

表明http的response是.html文件(如果不加这句话,那么lua返回的value是文件)这个response的内容是在…/lua/staticitem.lua中定义的。

输出入下图所示:

5.15-5.18 OpenResty实战

openresty原理

  • OpenResty由Nginx核心加很多第三方模块组成,默认集成了Lua开发环境,使得Nginx可以作为一个Web Server使用

  • 借助于Nginx的事件驱动模型和非阻塞IO(epoll多路复用机制),可以实现高性能的Web应用程序

  • OpenResty提供了大量组件如Mysq、Redis、Memcached等等,使得在Nginx上开发Web应用更方便更简单。

openresty hello world

在lua文件夹下新建helloworld.lua脚本

ngx.exec("/item/get?id=6");

在conf/nginx.conf的server中增加

        location /helloworld {content_by_lua_file ../lua/helloworld.lua;}

重新reload,可以发现访问miaoshaserver/helloworld和访问miaoshaserver/item/get?id=1一样

Shared dic: 共享内存字典,所有worker进程可见

新建itemsharedic.lua脚本:

function get_from_cache(key)local cache_ngx = ngx.shared.my_cachelocal value = cache_ngx:get(key)return value
end      function set_to_cache(key,value,exptime)if not exptime thenexptime = 0end     local cache_ngx = ngx.shared.my_cachelocal succ,err,forcible = cache_ngx:set(key,value,exptime)return succ
end     local args = ngx.req.get_uri_args()
local id = args["id"]
local item_model = get_from_cache("item_"..id)
if item_model == nil thenlocal resp = ngx.location.capture("/item/get?id="..id)item_model = resp.bodyset_to_cache("item_"..id,item_model,1*60)
end
ngx.say(item_model)

修改nginx.conf,在server代码块之外添加第一行代码,剩下的添加在server代码块里面

lua_shared_dict my_cache 128m;location /luaitem/get {default_type "application/json";content_by_lua_file ../lua/itemsharedic.lua;}

openresty redis支持


nginx的shared dic只将热点数据存在内存缓存中,非热点但是是高流量的数据存在redis slave中,redis slave和redis master之间的同步机制更新脏数据

我们打算做这种架构,nginx通过读redis slave的内容,来兼顾内容的更新问题,redis自身有master/slave的主从机制。

若nginx可以连接到redis上,进行只读不写,若redis内没有对应的数据,那就回源到miaoshaserver上面,然后对应的miaoshaserver也判断一下redis内有没有对应的数据,

若没有,回源mysql读取,读取之后放入redis中 ,那下次h5对应的ajax请求就可以直接在redis上做一个读的操作,nginx不用管数据的更新机制,下游服务器可以填充redis,nginx只需要实时的感知redis内数据的变化,在对redis添加一个redis slave,redis slave通过redis master做一个主从同步,更新对应的脏数据。

在lua文件中新建itemredis.lua脚本

local args = ngx.req.get_uri_args()
local id = args["id"]
local redis = require "resty.redis"
local cache = redis:new()
local ok,err = cache:connect("172.26.241.149",6379)
local item_model = cache:get("item_"..id)
if item_model == ngx.null or item_model == nil thenlocal resp = ngx.location.capture("/item/get?id="..id)item_model = resp.body
endngx.say(item_model)

修改nginx.conf如下,之后重启nginx

3、查询性能优化技术之多级缓存相关推荐

  1. 大数据存储系统I/O性能优化技术研究进展

    大数据存储系统I/O性能优化技术研究进展 肖利民,霍志胜 北京航空航天大学计算机学院,北京 100191 摘要:大数据存储系统的I/O性能是影响大数据应用整体性能的关键因素之一,总结了当前在存储系统架 ...

  2. 查询性能优化(使用 Explain 进行分析、优化数据访问、重构查询方式)、存储引擎(InnoDB/MyISAM)

    1.查询性能优化 1.1 使用 Explain 进行分析 Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句. 比较重要的字段有: select ...

  3. 读薄《高性能MySql》(四)查询性能优化

    读薄<高性能MySql>(一)MySql基本知识 读薄<高性能MySql>(二)Scheme与数据优化 读薄<高性能MySql>(三)索引优化 读薄<高性能M ...

  4. mysql笔记03 查询性能优化

    查询性能优化 1. 为什么查询速度会慢? 1). 如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间.如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减 ...

  5. 高性能MySQL-3rd-(六)查询性能优化

    2019独角兽企业重金招聘Python工程师标准>>> /* * -------------------------------------------------------- * ...

  6. mysql获取查询策略语句_MySQL数据库查询性能优化策略

    优化查询 使用Explain语句分析查询语句 Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句. 通过对查询语句的分析,可以了解查询语句的执行 ...

  7. 【Elasticsearch】Elasticsearch的IndexSorting:一种查询性能优化利器

    1.概述 转载:Elasticsearch的IndexSorting:一种查询性能优化利器 前言 前两周写过一篇<基于Lucene查询原理分析Elasticsearch的性能>,在最后留了 ...

  8. mysql 主键查询性能_MySQL查询性能优化(精)

    MySQL查询性能优化 MySQL查询性能的优化涉及多个方面,其中包括库表结构.建立合理的索引.设计合理的查询.库表结构包括如何设计表之间的关联.表字段的数据类型等.这需要依据具体的场景进行设计.如下 ...

  9. Sql Server查询性能优化之索引篇【推荐】

    Sql Server查询性能优化之索引篇[推荐] 这篇是索引系列中比较完整的,经过整理而来的 一 索引基础知识 索引概述 1.概念 可以把索引理解为一种特殊的目录.就好比<新华字典>为了加 ...

最新文章

  1. postgresql数据库修改表
  2. DB2 9 根蒂根基底细(730 考试)认证指南,第 6 局部: 数据并发性(5)
  3. python not instance_python isinstance 判断各种类型的小细节|python3教程|python入门|python教程...
  4. OpenCV计算机视觉编程攻略之行人检测
  5. Java知多少(4)J2SE、J2EE、J2ME的区别
  6. PHP实现四位数字+字母验证码
  7. php7 参数类型限定,PHP参数类型限制 - Corwien的博客 - OSCHINA - 中文开源技术交流社区...
  8. C 学习笔记 - 数组
  9. lucene之Field属性的解释
  10. LeetCode MySQL 1607. 没有卖出的卖家
  11. 送给520的产品经理
  12. 接口规范 11. 串流相关接口
  13. LeetCode Convert Sorted List to Binary Search Tree 解题报告
  14. 此图片来自微信公众平台未经允许不可引用 解决方法
  15. 基于Cache的Fibonacci数列的计算
  16. iOS AnchorPoint 引起的坐标问题
  17. Miniflter中 NPInstanceSetup调查
  18. 网络是怎样连接起来的
  19. HTML做成信纸格式,css实现一个写信的格式_html/css_WEB-ITnose
  20. U盘拷贝时提示文件过大

热门文章

  1. 特征选择-熵和互信息
  2. 获得与管线相连的管件连接件
  3. bzoj-3307 雨天的尾巴
  4. 小tips;CSS和JS“通信”
  5. Linux介绍和基础操作
  6. IDEA反编译class文件
  7. HttpClient的使用
  8. android webview 禁止放大缩小,在Android WebView中启用/禁用缩放
  9. golang 面试题(十三)interface内部结构和nil详解
  10. mysql 密码文件改成密文_需求:实现数据库密码通过密文的方式存储在配置文件中 | 学步园...