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. VISP视觉库识别AprilTag详细解读
  2. 23种设计模式[5]:原型模式
  3. centos 7 Hadoop2.7.4完全分布式搭建(一)
  4. java forkjoin 简书_浅谈Java的Fork/Join并发框架
  5. 【转】Java计算文件的hash值
  6. macOS Sierra 10.12.6 odoo 10.0 开发环境配置
  7. 【pandas教程】索引操作
  8. 遗传算法求解TSP问题及MTATLAB代码
  9. DSP 6678的NETCP
  10. 多媒体文件格式全解说(下)--图片
  11. 西门子S7-200 SMART控制步进电机(一)
  12. MEGARAC(宝德)服务器BMC登录失败解决办法
  13. fedora 29 使用百度网盘客户端
  14. android studio 制作app欢迎界面-两种方法(功能)(备忘)
  15. 台湾大学 李宏毅教授的个人主页
  16. vsftpd.conf 配置详解
  17. GNN手写字体识别java_深度之眼Paper带读笔记GNN.09.GGNN
  18. 解决32G或64G的SD卡无法使用NOOBS安装树莓派的问题
  19. 降维算法总结(超全!附代码)
  20. 2018计算机二级c语言通过率,计算机二级各科通过率是多少

热门文章

  1. CentOs 7 root用户被锁 解锁过程
  2. 10X单细胞空间联合分析之SPOTlight进阶版
  3. Linux FTP并发连接及带宽限制
  4. MiniGUl-Threads 和 MiniGUl-Lite 的选择
  5. 信号(三)Kill函数和Kill命令
  6. 【调剂】华北计算技术研究所2023年硕士研究生调剂信息
  7. 了解区块链延迟和吞吐量
  8. 网络安全工程师能拿高薪的秘密!
  9. SIFT特征提取与检索
  10. 据报道,小米曝光 12S Ultra 概念机