四、查询性能优化
- 多级缓存
- redis 缓存 , 本地缓存
- 热点nginx lua 缓存
1)怎么设计缓存效率高?
- 用快速存取设备, 用内存
- 将缓存推到离用户最近的地方
- 去清理脏缓存
2)多级缓存设计
- redis 缓存
- 热点本地缓存
- nginx proxy cache缓存
- nginx lua 缓存
多级缓存架构每一层的意义
- nginx 本地缓存,抗的是热数据的高并发访问。 一般来说,商品的购买热点数, 比如每天购买 苹果手机 华为手机 海尔 知名品牌总是比较多。
这就是一些热点数据,利用nginx本地缓存,由于我们经常访问。 所以可以被锁定到nginx的本地缓存内。
2.redis 分布式缓存 , 主要抗的是很高的离散访问。 支撑海量的数据。 高并发的访问。 高可用的服务
redis缓存大量的数据。 最完整的数据和缓存。 可用性 非常好的提供稳定的服务 。
- nginx 本地内存有限,能缓存热点数据,有限。 能缓存一些 苹果手机 耐克鞋子。。 相对与不是热点的数据。可能流量就走到了。 redis那里。 redis 利用 cluster 多个master写入。 读的话我们从slave 读。 横行一种扩容。
- tomcat jvm 堆内存, 主要是抗住 redis大规模灾难, 如果redis出现了大规模宕机。 导致nginx大量流量直接涌入数据生成服务。。那么最后的tomcat 堆内存至少可以在扛一下。 不至于让我们数据库 裸奔。
本地热点缓存实现:
guava 开源java 库。是由谷歌公司研发。 这个库主要是为了方便编码,并且减少编码错误。 这个库用于集合 缓存 并发性 常用注解 字符串处理呀 i/o 和 验证 实用的方法。
- 1引入pom引入guava
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
2.redis缓存
略
- nginx_proxy_cache
- nginx 反向代理
- 依靠文件系统存索引文件
- 依靠内存保存索引文件地址
proxy_cache 应用:
在nginx.conf 文件中的http模块
proxy_cache_path /usr/local/openresty/nginx/tmp-test levels=1:2 keys_zone=tmp-test:100m inactive=7d max_size=10g ;
解释:
proxy_cache_path:
缓存文件的路径
levels 设置缓存文件的目录层次
1:2 代表2级目录
keys_zone
: 设置缓存的名字和共享内存的大小
inactive
: 在指定时间内没有人访问就被删除
max_size
: 最大的缓存空间。 如果缓存满了, 默认覆盖掉缓存时间最长
location / {
proxy_pass http://backend;
proxy_cache tmp-test;
proxy_cache_key $uri;
proxy_cache_valid 200 206 304 302 10d;
}
解释:
proxy_cache tmp-test
: 使用名为tmp-test
proxy_cache_valid 200 206 304
: 对httpcode 为缓存 10天;
proxy_cache_key $uri
:定义缓存唯一的key /通过唯一key来进行hash
最基本的配置往往不能满足我们的业务的需求。我们会提出一个小疑问?
提出一些疑问??
- 我需要主动清理缓存文件
- 写入路径是一个快磁盘,如果磁盘打满了怎么办
- 日志统计,如何配置命中和不命中 如何设计?
- 基于磁盘操作 。影响我们性能 怎么办?
解决
1)解决问题一: 主动清理缓存
采用: nginx proxy_cache_purge 模块 , 改模块与 proxy_cache 功能相反。
设计方法: nginx中 另外在启动一个servce 。当需要清理资源的时候呢。 访问一下
相关配置:
location /tem-test/{
allow 127.0.0.1;
deny all;
proxy_cache_purge tmp-test $uri;
}
解释:
allow:
谁能访问
deny all:
禁止其他所有ip3
proxy_cache_purge tmp-test $uri
缓存清理模块 tmp-test 指定的key_zone $uri key 参数
2) 解决问题二: 缓存文件磁盘打满怎么办?
由于写入路径唯一单一目录。 只能写入一块磁盘, 一块磁盘很快就满了 。 解决这种问题
1. 将多个磁盘做个磁盘阵列?缺点事减少了实际的存储空间。
2. 通过软链的方法。 实现 将不同盘下的目录作为真正的存放数据的路径。 就解决了多盘利用, 单盘被打满的问题/。
3) 解决问题三? 日志统计
nginx 提供了 upstream_cache_status 这个变量 来显示我们缓存的一个状态。 我们可以配置在http头 :
location / {
proxy_pass http://backend;
proxy_cache tmp-test;
proxy_cache_key $uri;
proxy_cache_valid 200 206 304 302 10d;
add_header Nginx-Cache "$upstream_cache_status";
}
缓存状态
HIT: 缓存命中
MISS: 为命中 请求被传送到后端
EXPIRES: 缓存过期被床送到后端
UPDATING: 正在更新缓存。
STALE: 后端得到的过期的应答
4) 解决问题四? 基于磁盘操作
使用 shared dic: 共享内存字典, 所有的worker进程都可见 其中使用的淘汰算法事 LRU算法
我们必须要用OpenResty 由nginx核心加上很多第三方的模块。默认集成 Lua 开发环境。
nginx 对lua模块得支持
模块语法lua指令:
set_by_lua
设置nginx变量 可以实现复杂赋值逻辑
set_by_lua_file
设置nginx变量 可以实现复杂赋值逻辑
access_by_lua
请求访问阶段处理。用户访问控制
access_by_lua_file
请求访问阶段处理。用户访问控制
content_by_lua
内容处理器。 处理接受和响应输出
content_by_lua_file
内容处理器。 处理接受和响应输出
nginx lua api
ngx.var
nginx变量
ngx.req.get_headers
获取请求头
ngx.req.get_uri_args
获取url请求参数
ngx.redirect
重定向
ngx.print
输出响应内容体
ngx.say
和
ngx.print
最后会输出一个换行符
ngnx.header
输入响应头
Nginx Lua 插载点
init_ by_ _lua:
系统启动时调用
init_worker_by_lua: worker
进程启动时调用
set_by_lua: nginx
变量用复杂lua return
rewrite_by_lua:
重写url规则
access_by_lua :
权限验证阶段
content_by_lua:
内容输出节点
Lua helloworld:
修改nginx.confg文件在server新增规则:
location /lua{
default_type "text/html";
content_by_lua_file /usr/local/openresty/lua/hello.lua;
}
hello.lua
ngx.say("hello lua ")
Lua shared dict缓存
我们介绍一下有什么缓存方式: Lua shared dict (字典表) , Lua redis
我们来看下代码:
-- 获取缓存
function get_from_cache(key)
local cache_ngx = ngx.shared.my_cache
local value = cache_ngx:get(key)
return value
end
-- 设置缓存
function set_from_cache(key,value,exptime)
if not exptime then
exptime = 0
end
local cache_ngx = ngx.shared.my_cache
local succ,err,forrcible = cache_ngx:set(key,value,exptime)
return succ
end
--如果controller接口是 占位符得话
--示例 http://localhost:8080/getDog/1
-- local request_uri = ngx.var.request_uri
-- local id= string.match(arg, ".+/(.+)")
-- 获取url请求参数
local args = ngx.req.get_uri_args()
-- 获取参数id得值
local id = args["id"]
-- 获取缓存
local goods = get_from_cache("goodsdetail_"..id)
if goods == nil then
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri("http://192.168.66.32:8080",{
method = "GET",
path = "/getDog?id="..id
})
goods = resp.body
set_from_cache("goodsdetail_"..id,goods,1*60)
end
ngx.say(goods)
这里我们使用 ngx.shared.my_cache ,奇怪 my_cache 那里来得这个东西我们需要在nginx.conf 文件里面做修改:
lua_shared_dict my_cache 128m;
首先我们要使用http模块我们得安装http模块包
1.git clone https://github.com/ledgetech/lua-resty-http.git
如果说出现了这句话 -bash: git: command not found
2. yum install -y git
3. cd lua-resty-http
4. 把这个目录里面得lib\resty目录下得http.lua http_headers.lua 复制到/usr/local/openresty/lualib/resty
如果选择?
shared dict 使用得共享内存 。 每次操作都是全局锁。
api比较少 get set delete.
使用 openResty 对redis缓存支持
脚本如下:
local args = ngx.req.get_uri_args()
local id = args["id"]
--redis 链接
local redis = require "resty.redis"
local cache = redis:new()
local ok,err = cache:connect("192.168.66.33",6379)
local dogmodel = cache:get("getDog_"..id)
if dogmodel == nil then
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri("http://192.168.66.32:8080",{
method = "GET",
path = "/getDog?id="..id
})
dogmodel = resp.body
end
ngx.say(dogmodel)
基于Lua完成商品id的定向流量分发策略
问题
- 缓存命中率低
缓存数据生产服务那一层已经搞得了。 相当于做了4层缓存架构中 本地堆缓存 + redis分布式缓存我们搞定了。
一般来说我们是不是都会去配置负载均衡。 在这个里面会放入一些缓存。 在默认情况。 此时缓存命中率比较低。
-
如何提高缓存命中率
分发层 + 应用层 ,双层nginx
分发层nginx , 负责流量分发的逻辑和策略,这个里面我们可以定义规则。 比如说我们根据商品id进行hash。然后对后端的服务器
数量进行取模。 将某个商品的访问的请求 。就固定在一个nginx的后端服务器上。 保证只会从redis中获取一次数据。后面全部都是走的 本地热点缓存 后端服务器。称之为应用服务器。 最前端nginx 服务器我们称为分发服务器。
步骤:
1) 获取请求参数。比如商品id
2)对商品id进行hash
3)hash值对应用服务器数量进行取模。 获取一个应用服务器
4) 利用http模块发送请求到应用层nginx
5) 获取响应后返回
具体流量分发脚本;
local uri_args = ngx.req.get_uri_args()
local id = uri_args["id"]
-- 后端服务器的列表
local host = {"192.168.66.31:8080","192.168.66.32:8080"}
local hashid = ngx.crc32_long(id)
hashid = (hashid % 2) + 1
local backend = "http://"..host[hashid]
local method = uri_args["method"]
local requestBody = "/"..method.."?id="..id
local http,err = require("resty.http")
local httpc = http.new()
local resp,err = httpc:request_uri(backend,{
method = "get",
path = requestBody
})
if not resp then
ngx.say("reques error : ",err)
return
end
ngx.say(resp.body)
-- 关闭http链接
httpc:close
页面缓存
由于并发瓶颈在数据库,所以要想办法来减少对数据库的访问,可以加若干缓存来解决,通过各种粒度的缓存,最大粒度的页面级缓存到最小粒度的对象级缓存
步骤:
-
取缓存( 缓存里面存的是html )
-
手动渲染模板
-
结果输出(html 代码)
@Autowired
private ThymeleafViewResolver thymeleafViewResolver;
@GetMapping(value = "list",produces = "text/html")
@ResponseBody
public String miaoShaGoodsList(MiaoShaUser user, ModelMap map, HttpServletRequest request, HttpServletResponse response, Model model){
if (user == null){
return "login" ;
}
// 1. 取缓存
String miaoShaGoodsHtml = iMiaoShaGoodsService.getMiaoShaGoodsHtml(MiaoShaGoodsHtmlKey.miaoShaGoodsHtmlKey.getPrefix());
if (!StringUtils.isEmpty(miaoShaGoodsHtml)){
return miaoShaGoodsHtml;
}
//获取商品列表
List<MiaoShaGoodsVo> miaoShaGoodsVoList = iMiaoShaGoodsService.findAll();
model.addAttribute("goodsList",miaoShaGoodsVoList);
model.addAttribute("user",user);
//手动渲染
IWebContext ctx = new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
//渲染数据模板
miaoShaGoodsHtml = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
//写缓存
if (!StringUtils.isEmpty(miaoShaGoodsHtml)){
iMiaoShaGoodsService.setMiaoShaGoodsHtml(MiaoShaGoodsHtmlKey.miaoShaGoodsHtmlKey.getPrefix(),miaoShaGoodsHtml);
}
return miaoShaGoodsHtml;
}
页面URL缓存
如何做好动静分离
- URL唯一化。 商品详情系统天然的做到url唯一话。 每个商品都是有id 来标识。 http://localhost:8080/getGoods?id=xxx 就可以用url 来作为缓存key。
- 分离浏览器相关因素。 是否已经登录。 登录身份 单独拆出来动态获取
- 去掉cookie 。信息json化。 以方便前端获取。
@GetMapping(value = "to_detail/{goodsId}" ,produces = "text/html")
@ResponseBody
public String to_detail(@PathVariable Long goodsId, MiaoShaUser user, Model map,HttpServletRequest request
,HttpServletResponse response){
if (user == null){
return "login";
}
//取缓存
String html = iMiaoShaGoodsService.getMiaoShaGoodsHtml(MiaoShaGoodsHtmlKey.getGoodsDetail + ":" + goodsId);
if (!StringUtils.isEmpty(html)){
return html;
}
MiaoShaGoodsDetailVo byGoodsIdDetail = iMiaoShaGoodsService.findByGoodsIdDetail(goodsId);
//开始时间
long startDate = byGoodsIdDetail.getStartDate().getTime();
//结束时间
long sendDate = byGoodsIdDetail.getEndDate().getTime();
//现在时间
long nowDate = System.currentTimeMillis();
//秒杀状态
int miaoshaStatus = 0;
//倒计秒
int remainSendconds = 0;
if (nowDate < startDate){//秒杀还没开始 。进行倒计时
miaoshaStatus = 0;
remainSendconds = (int)((startDate - nowDate)/1000);
}else if (nowDate > sendDate){//秒杀结束了
miaoshaStatus = 2 ;
remainSendconds = -1;
}else {//秒杀正在进行中
miaoshaStatus = 1;
remainSendconds = 0;
}
map.addAttribute("user",user);
map.addAttribute("goods",byGoodsIdDetail);
map.addAttribute("miaoshaStatus",miaoshaStatus);
map.addAttribute("remainSendconds",remainSendconds);
//手动渲染
IWebContext ctx = new WebContext(request,response,request.getServletContext(),request.getLocale(),map.asMap());
//渲染数据模板
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
if (!StringUtils.isEmpty(html)){
iMiaoShaGoodsService.setMiaoShaGoodsHtml(MiaoShaGoodsHtmlKey.getGoodsDetail + ":" + goodsId,html);
}
return html;
}
对象缓存
对象缓存相比页面缓存是更加细粒度的缓存。在实际项目,不会大规模使用页面缓存技术,对象缓存就是当用户到用户数据据的时候。可以直接从缓存中取出, 比如说更新用户密码 。 根据token来获取用户缓存对象。
本项目中对MiaoShaUser对象使用缓存代码如下:
/**
* 根据id获取用户
* @param id
* @return
*/
@Override
public MiaoShaUser getMiaoShaUser(Long id) {
String user = stringRedisTemplate.opsForValue().get(MiaoShaUserKey.getById.getPrefix() + ":" + id);
if (!StringUtils.isEmpty(user)){
return JSON.parseObject(user, MiaoShaUser.class);
}
MiaoShaUser miaoShaUser = miaoShaUserDao.findById(id);
if (miaoShaUser != null){
String userjson = JSON.toJSONString(miaoShaUser);
stringRedisTemplate.opsForValue().set(MiaoShaUserKey.getById.getPrefix() + ":" + id,userjson);
}
return miaoShaUser;
}
MiaoShaUserKey 作为对象缓存的key前缀, 这里我们设置对象缓存没有有效期。永久有效
package com.etc.redis;
/**
* @Author kalista
* @Description
* @Date 2020/7/21 16:28
**/
public class MiaoShaUserKey extends BasePrefex{
public static final int TOKEN_EXPIRE = 3600 * 24 * 2;
public MiaoShaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public MiaoShaUserKey(String prefix) {
super(prefix);
}
public static MiaoShaUserKey token = new MiaoShaUserKey(TOKEN_EXPIRE,"tk");
public static MiaoShaUserKey getById = new MiaoShaUserKey(0,"id");
}
更新用户密码时我们要注意数据库缓存,一定要保证数据一致性。修改token关联对象以及id关联对象。 先更新数据库后删除缓存。 不能直接取删除token, 删除后就不能登录了。 在将token以及对应的用户信息在写入进去。
关于缓存我们一些思考?
- 为什么不能先处理缓存在更新数据库?
- 为什么缓存更新策略是先更新数据库在删除缓存?
- 怎么才能保持缓存和数据库一致性?
要解决这些问题,数据和缓存不一致。 大概3种
数据库有数据,缓存没有数据
数据库有数据。缓存也有数据。 但是不一致
数据库没有数据。但是缓存有数据
解决的办法:
首先尝试取缓存读数据,读到数据则直接返回,如果读不到,就读数据库,并把数据库的缓存起来,如果需要更新数据时,先更新数据库,然后把缓存对应的数据删除掉。
商品详情页的静态化
<!DOCTYPE HTML>
<html>
<head>
<title>商品详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<!-- jquery -->
<script type="text/javascript" src="/js/jquery.min.js"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
<!-- jquery-validator -->
<script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
<script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
<!-- layer -->
<script type="text/javascript" src="/layer/layer.js"></script>
<!-- md5.js -->
<script type="text/javascript" src="/js/md5.min.js"></script>
<!-- common.js -->
<script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀商品详情</div>
<div class="panel-body">
<span> 您还没有登录,请登陆后再操作<br/></span>
<span>没有收货地址的提示。。。</span>
</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td id="goodsName" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="3"><img id="goodsImg" width="200" height="200"/></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td id="startTime"></td>
<td id="miaoshaTip">
<input type="hidden" id="remainSeconds"/>
<span id="countDown"></span>
</td>
<td>
<button class="btn btn-primary btn-block" type="button" onclick="do_miaosha()" id="buyButton">立即秒杀
</button>
<input type="hidden" id="goodsId"/>
</td>
</tr>
<tr>
<td>商品原价</td>
<td id="goodsPrice" colspan="3"></td>
</tr>
<tr>
<td>秒杀价</td>
<td id="miaoshaPrice" colspan="3"></td>
</tr>
<tr>
<td>库存数量</td>
<td id="goodsStock" colspan="3"></td>
</tr>
</table>
</div>
</body>
<script>
<!-- 页面初始化完成的函数-->
$(function () {
// countDown();
getDetail()
});
//渲染整个页面
function getDetail() {
//1. 首先从url中获取到goodsid 值
var goodsId = g_getQueryString("goodsId")
$.ajax({
url: "/miaoshagoods/detail/" + goodsId,
type: "get",
success: function (data) {
console.log(data)
if (data.code == 200) {
render(data)
}
},
error: function () {
}
});
}
function render(detail) {
var obj = detail.data
var miaoshastatus = obj.miaoshaStatus
var remainSendconds = obj.remainSendconds
var goods = obj.miaoShaGoodsDetailVo
$("#goodsName").text(goods.goodsName)
$("#goodsImg").attr("src", goods.goodsImg)
$("#startTime").text(new Date(goods.startDate).format("yyyy- MM- dd hh:mm:ss"))
$("#goodsPrice").text(goods.goodsPrice)
$("#miaoshaPrice").text(goods.miaoshaPrice)
$("#goodsStock").text(goods.goodsStock)
$("#remainSeconds").val(remainSendconds)
countDown()
}
function countDown() {
var remainSeconds = $("#remainSeconds").val();
console.log(remainSeconds)
var timeout;
if (remainSeconds > 0) {//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
timeout = setTimeout(function () {
$("#countDown").text(remainSeconds - 1);
$("#remainSeconds").val(remainSeconds - 1);
countDown();
}, 1000);
} else if (remainSeconds == 0) {//秒杀进行中
$("#buyButton").attr("disabled", false);
if (timeout) {
clearTimeout(timeout);
}
$("#miaoshaTip").html("秒杀进行中");
} else {//秒杀已经结束
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
}
}
function do_miaosha() {
//获取商品id
var goodsid = $("#goodsId").val()
$.ajax({
url: "/miaoshagoods/do_miaosha",
type: "POST",
data: {
"goodsId": goodsid
},
success: function (data) {
console.log(data)
if (data.code == 200) {
alert("秒杀成功 去订单页面查看")
} else {
alert(data.msg)
}
},
error: function () {
}
});
}
</script>
</html>