查询性能优化,本地热点缓存,实现对象缓存,页面URL缓存,页面缓存,基于Lua完成商品id的定向流量分发策略,Lua shared dict缓存

四、查询性能优化

  • 多级缓存
  • redis 缓存 , 本地缓存
  • 热点nginx lua 缓存

1)怎么设计缓存效率高?

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

2)多级缓存设计

  1. redis 缓存
  2. 热点本地缓存
  3. nginx proxy cache缓存
  4. nginx lua 缓存

多级缓存架构每一层的意义
  1. nginx 本地缓存,抗的是热数据的高并发访问。 一般来说,商品的购买热点数, 比如每天购买 苹果手机 华为手机 海尔 知名品牌总是比较多。

这就是一些热点数据,利用nginx本地缓存,由于我们经常访问。 所以可以被锁定到nginx的本地缓存内。

2.redis 分布式缓存 , 主要抗的是很高的离散访问。 支撑海量的数据。 高并发的访问。 高可用的服务

redis缓存大量的数据。 最完整的数据和缓存。 可用性 非常好的提供稳定的服务 。

  1. nginx 本地内存有限,能缓存热点数据,有限。 能缓存一些 苹果手机 耐克鞋子。。 相对与不是热点的数据。可能流量就走到了。 redis那里。 redis 利用 cluster 多个master写入。 读的话我们从slave 读。 横行一种扩容。

  2. 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缓存

  1. 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. 我需要主动清理缓存文件
  2. 写入路径是一个快磁盘,如果磁盘打满了怎么办
  3. 日志统计,如何配置命中和不命中 如何设计?
  4. 基于磁盘操作 。影响我们性能 怎么办?
解决
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.sayngx.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的定向流量分发策略

问题

  1. 缓存命中率低

缓存数据生产服务那一层已经搞得了。 相当于做了4层缓存架构中 本地堆缓存 + redis分布式缓存我们搞定了。

一般来说我们是不是都会去配置负载均衡。 在这个里面会放入一些缓存。 在默认情况。 此时缓存命中率比较低。

  1. 如何提高缓存命中率

    分发层 + 应用层 ,双层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

页面缓存

由于并发瓶颈在数据库,所以要想办法来减少对数据库的访问,可以加若干缓存来解决,通过各种粒度的缓存,最大粒度的页面级缓存到最小粒度的对象级缓存

步骤:

  1. 取缓存( 缓存里面存的是html )

  2. 手动渲染模板

  3. 结果输出(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缓存

如何做好动静分离

  1. URL唯一化。 商品详情系统天然的做到url唯一话。 每个商品都是有id 来标识。 http://localhost:8080/getGoods?id=xxx 就可以用url 来作为缓存key。
  2. 分离浏览器相关因素。 是否已经登录。 登录身份 单独拆出来动态获取
  3. 去掉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以及对应的用户信息在写入进去。

关于缓存我们一些思考?

  1. 为什么不能先处理缓存在更新数据库?

  2. 为什么缓存更新策略是先更新数据库在删除缓存?

  3. 怎么才能保持缓存和数据库一致性?

要解决这些问题,数据和缓存不一致。 大概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>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值