Web页面缓存

采用nginx+lua+redis进行页面缓存

1.配置nginx+lua+redis环境

安装lua解释器

wget http://luajit.org/download/LuaJIT-2.0.2.tar.gz

配置lua Libinclude/luajit-$version目录为环境变量

下载ngx_devel_kitlua-nginx-module

https://codeload.github.com/simpl/ngx_devel_kit/tar.gz/v0.2.19
https://codeload.github.com/openresty/lua-nginx-module/tar.gz/v0.9.10

安装配置nginx

http://nginx.org/download/nginx-1.4.7.tar.gz(http://nginx.org/en/download.html

nginx configue时候,将ngx_devel_kitlua-nginx-module两个编入到nginx

make 和make install

使用redis

lua解析json格式的数据 http://www.kyne.com.au/~mark/software/lua-cjson.php(默认安装就行)

lua封装redis调用接口,https://github.com/openresty/lua-resty-redis,安装,(并且redis.lua上层目录要有resty,因为require “resty.redis”,看代码,负责会出现找不到库)

lua_package_path "/opt/vendor/lua/?.lua;;";//指定redis.lua路径,;;指定默认路径  加在http段里
 
init_by_lua_file /usr/local/nginx/html/init.lua;

lua_code_cache off;//避免lua代码缓存,更改代码执行失效  

location /lua {  
      content_by_lua_file /usr/local/nginx/html/content.lua;  
} 加在加入一个location

拦截请求保存相应的页面缓存

location ~ /(index\.html|(goods|event)/[0-9]+\.html|video/list\.html|channel/class\.html)? {
    content_by_lua_file /app/nginx/conf/content.lua;
}     

加在加入一个location,拦截相应的请求,保存页面到redis缓存

缓存的更新与删除

新增定时项目,定时循环页面缓存redis的Key值,根据key值访问相应接口,返回的结果值MD5之后保存到redis的不用区。

添加定时任务定时扫描保存MD5缓存的key,分页面进行操作,比较接口返回结果的MD5值和redis缓存是否一致,不一致删除相应的redis页面缓存,对于商品的特别处理,商品下市删除相应的redisMD5缓存和redis页面缓存。

2016/4/15 posted in  others

秒杀系统架构分析与实战

秒杀技术挑战

对现有网站业务造成冲击

将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离。

高并发下的应用、数据库负载

重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态话,用户请求不需要经过应用服务。

突然增加的网络及服务器带宽

因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。

直接下单

下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。

需要将该URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。方法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。

如果控制秒杀商品页面购买按钮的点亮

在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含秒杀开始与否标志。这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器,也不会对服务器集群和网络带宽造成太大压力。

如果只允许第一个提交的订单被发送到订单子系统

假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。

在还没有人提交订单成功之前,如果一个服务器已经有10单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面,可以考虑通过cookie的方式来应对,符合一致性原则。当然可以采用最少连接的负载均衡算法,出现上述情况的概率大大降低。

如何进行下单前置检查

  • 下单服务器检查本机已处理的下单请求数目

如果超过10条,直接返回已结束页面给用户

如果未超过10条,则用户可进入填写订单及确认页面

  • 检查全局已提交订单数目

已超过秒杀商品总数,返回已结束页面给用户

未超过秒杀商品总数,提交到子订单系统

秒杀一般是定时上架

减库存的操作

有两种选择,一种是拍下减库存;另一种是付款减库存

库存会带来‘超卖’的问题

采用乐观锁

UPDATE auction_auctions SET
quantity = #inQuantity#
WHERE auction_id = #itemId# and quantity = #dbQuantity#

秒杀器的应对

秒杀器一般下单购买及其迅速,根据购买记录可以甄别出一部分。可以通过校验码达到一定的方法。

秒杀架构原则

尽量将请求拦截在系统上游

读多写少的多使用缓存

秒杀架构设计

  • 秒杀系统的页面设计尽可能简单
  • 购买按钮只有在秒杀活动开始的时候才变亮
  • 下单表单也尽可能简单;只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面

前端层设计

秒杀页面的展示

各类静态资源首先应分开存放,然后放到CDN节点上分散压力

倒计时

可能出现客户端时钟与服务器时钟不一致,另外服务器之间也是有可能出现时钟不一致。

浏览器层请求拦截

  • 产品层面:用户点击后,按钮置灰
  • JS层面:限制用户在x秒之内只能提交一次请求

站点层设计

  • 同一个uid,限制访问频率:做页面缓存,x秒内到达站点层的请求,均返回同一个页面
  • 同一个item的查询,均返回同一个页面

服务层设计

并发队列的选择

  • ArrayBlockingQueue是初始容量固定的阻塞队列,我们可以用来作为数据库模块成功竞拍的队列。比如有10个商品,那么我们就设定一个大小为10的数组队列。
  • ConcurrentLinkedQueue使用的是CAS无锁队列,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。
  • LinkedBlockingQueue也是阻塞的队列,入队和出队都加了锁,当队空的时候线程会暂时阻塞。

由于我们的系统入队需求要远大于出队需求,一般不会出现队空的情况,所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现。

数据库模块数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。

数据库设计

设计思路

如何保证数据的可用性?

冗余。

如何保证数据库‘读’高可用?

冗余读库

如何保证数据库‘写’高可用?

冗余写库。采用双主互备的方式。

双写同步,数据可能冲突(例如‘自增id’同步冲突),有两种常见解决方案:

  • 两个写库使用不同的初始值,相同的步长来增加id:写库1的id为0,2,,4,6……;写库2的id为1,3,5,7……
  • 不使用数据的id,业务层自己生成唯一的id。保证数据不冲突。

如何扩展读性能

  • 第一种是建立索引:不同的库可以建立不同的索引 - 线上读库建立线上访问索引,例如uid - 线下读库建立线下访问索引,例如time
  • 第二种是增加从库
  • 第三种是增加缓存

如何保证一致性

主从数据库的一致性,通常有两种解决方案:

  • 中间件:如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。
  • 强制读主

DB与缓存间的不一致:有可能“从库读到旧数据,旧数据进入cache”

写操作时顺序升级为:

  • 淘汰cache
  • 写数据库
  • 在经验‘主从同步延时窗口时间’达到了以后,再次发起一个异步淘汰cache的请求。

作弊的手段:

同一个帐号,一次性发出多个请求

在程序入口处,一个帐号只允许接受1个请求,其他请求过滤。

可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功),成功写入的则可以继续参加。

或者自己实现一个服务,将同一个帐号的请求放入一个队列中,处理完一个,再处理下一个。

多个帐号,一次性发送多个请求

可以通过检测机器IP请求频率

  • 弹出验证码
  • 直接禁止IP

多个帐号,不同IP发送不同请求

通过帐号行为的‘数据挖掘’来提前清理掉它们。

高并发下的数据安全

悲观锁思路

在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

FIFO队列思路

强行将多线程变成单线程。

我们直接将请求放入队列中,采用FIFO。

乐观锁思路

这个数据的所有请求都有资格去修改,但是会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。


Reference

http://www.importnew.com/18920.html

2016/4/5 posted in  others

分布式Unique ID的生成方法一览

2016/4/3 posted in  others

`Local Storage`缓存

因为WAP触屏版的面向对象都是使用智能手机浏览器访问的用户,而基本上所有智能手机的浏览器都支持Local Storage,这样就可以使用Local Storage缓存一些东西。

之前都是使用Local Storage缓存一些用户信息,当做cookies使用,甚至还有一段时间Local Storage里的内容不能通过浏览器‘清楚访问痕迹’的功能删除,就使用Local Storage作为存储用户唯一标识的地方。

现在为WAP触屏版的优化思路就是将jscss文件存到Local Storage中,用户之后访问需要jscss的时候都从Local Storage中取,而不从网络中拉了。

美团的WAP触屏版是一个做的比较极端的例子

第一阶段

其实对于规模不是很大的网站,上线的流程不会特别严格,特别规范,那么对cssjs经常的修修补补是不可避免的。如果使用Local Storage缓存所有的cssjs,那么就还要制定一套Local Storage缓存失效的规则。所以我们暂时先只对用到的第三方jscssjqueryFramework7等)进行缓存

参考了《Web移动端使用localStorage缓存Js和css文件》1,这套代码基本实现了使用Local Storage缓存jscss文件的基本需求,对于简单的网站页面效果也是明显的。

但是有两个问题:

1、不能跨域。程序是用ajax请求cssjs的,由于ajax不能跨域,所以必须保证静态资源文件和网站要在同一个域名下,这个跟动静分离的思想有所违背。比较糙的解决方法就是将要缓存的文件映射一份在网站同域名下。

2、文件不能顺序加载。程序在拿到cssjs文件后,将这些文件的内容head.appendChild(js/css)html中,这样会让这些文件同步加载到html中,而缓存的文件加载的顺序不能保证一定在非缓存文件之前,如果之后的js的文件依赖于缓存的js先加载,那么就会报错。

举个例子:

<head>
    <script>
        whir.res.loadJs("jquery", __BASE_SERVER__ + "/js/jquery-1.8.3.min.js");
    </script>
    <script type="text/javascript" src="${__static_server__}/js/needsJQUERY.js"></script>
</head>

第二个文件需要jquery先加载,然后使用jquery的函数,但是jquery被缓存,且不能保证会在第二个文件处理前加载好。这样就会报错。

程序对顺序加载的解决方案是使用回调函数,保证数序:

whir.res.loadJs("jquery", __BASE_SERVER__ + "/js/jquery-1.8.3.min.js"), function(){
    whir.res.loadJs("needsJQUERY", "/needsJQUERY.js");  
});

这种解决的方法对于结构简单的页面是有效的,但是我们的页面都是依赖一个公用的宏,在公用的宏中申明了所有地方放的jscss,每个页面自己又有一套本页面有效的jscss,对于这种callback随页面变化的情况,前面说的解决方法不太适用。

比较糙的解决方法就是在加载最后一个缓存js文件的时候,callback方法中写location.reload();,页面强行刷新下

第二阶段

第二个问题相对来说更棘手,所以我们先要解决这个问题。

我们的思路是这样的:

  • 对于第一次来网站的用户,Local Storage为空,我们先使用正常的jscss引用的方法,然后再将jscss放入Local Storage中,但这一次不用Local Storage中缓存的文件
  • 对于之前来过网站的用户,Local Storage是有缓存的jscss,所以直接使用这些缓存的文件

判别用户之前有没有来过网站,我们就拿一个cookie来记录,如果用户有这个cookie值,则判断用户来过;如果没有这个值,则判断用户没有来过。

再者,如果用户的这个cookie值和我们pageVersion不一样,说明我们要缓存的js或者css版本号改变了,这时候,我们会先让cookie为空,将用户理解为一个全新的、第一次来的用户,然后继续整个流程。

这样好像就解决了用户第一次来,还要强制刷新页面的问题。直到我们用safari或者uc浏览器无痕浏览的功能访问网站的时候

safari的无痕浏览模式不支持Local Storage,并且会在localstorage.setItem的时候报错,自动停止程序,导致咱们缓存jscss时,用户访问的cookie写成功了,但是缓存没写进去。

uc浏览器的无痕浏览模式也不支持Local Storage,并且在将cookie值更改以后,新旧两个cookie都会同时存在,这样我们的程序就变成了死循环,检测到用户访问过,以为有缓存,实际没有。

所以我们对程序又做了修正:

  • 将添加cookie操作加入到loadJs的回调函数中
  • 在保证localstorage.setItem成功运行之后,我们才会把cookie值记录进去
<script type="text/javascript" src="${__static_server__}/js/localstorage.js"></script>
<#if local_storage_version != ''>
    <script>
        var lsv = "";
        var name = "lsv=";
        var ca = document.cookie.split(";");
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == " ") {
                c = c.substring(1);
            }
            if (c.indexOf(name) == 0) {
                lsv = c.substring(name.length, c.length);
            }
        }
        if (lsv != whir.res.pageVersion || (window.localStorage && lsv != localStorage.getItem("version"))) {
            var d = new Date();
            d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
            var expires = "expires=" + d.toUTCString();
            document.cookie = "lsv=;" + expires;
            location.reload();
        }
    </script>
<#else>
<script type="text/javascript" src="${__static_server__}/js/jquery-1.8.3.min.js"></script>
<script type="text/javascript" src="${__static_server__}/js/framework7/js/framework7.min.js"></script>
</#if>
<script>
    whir.res.loadJs("jquery", __BASE_SERVER__ + "/js/jquery-1.8.3.min.js");           whir.res.loadJs("framework7js", __BASE_SERVER__ + "/js/framework7/js/framework7.min.js", function () {
            myApp = new Framework7();
            $$ = Dom7;
            $.cookie('lsv', whir.res.pageVersion, {path: '/', expires: 365});
        });
</script>

localstorage.js

var whir = window.whir || {};
var refreshYN = false;
whir.res = {
    pageVersion: "121", //页面版本,由页面输出,用于刷新localStorage缓存
    //动态加载js文件并缓存
    loadJs: function (name, url, callback) {
        if (window.localStorage) {
            var xhr;
            var js = localStorage.getItem(name);
            if (js == null || js.length == 0 || this.pageVersion != localStorage.getItem("version")) {
                refreshYN = true;
                if (window.ActiveXObject) {
                    xhr = new ActiveXObject("Microsoft.XMLHTTP");
                } else if (window.XMLHttpRequest) {
                    xhr = new XMLHttpRequest();
                }
                if (xhr != null) {
                    xhr.open("GET", url);
                    xhr.send(null);
                    xhr.onreadystatechange = function () {
                        if (xhr.readyState == 4 && xhr.status == 200) {
                            js = xhr.responseText;
                            localStorage.setItem(name, js);
                            localStorage.setItem("version", whir.res.pageVersion);
                            // 确保浏览器支持localStorage.setItem
                            if (localStorage.getItem("version") == whir.res.pageVersion) {
                                if (callback != null) {
                                    callback(); //回调,执行下一个引用
                                }
                            }
                        }
                    };
                }
            } else {
                whir.res.writeJs(js);
                if (callback != null) {
                    callback(); //回调,执行下一个引用
                }
            }
        } else {
            whir.res.linkJs(url);
        }
    },
    loadCss: function (name, url) {
        if (window.localStorage) {
            var xhr;
            var css = localStorage.getItem(name);
            if (css == null || css.length == 0 || this.pageVersion != localStorage.getItem("version")) {
                if (window.ActiveXObject) {
                    xhr = new ActiveXObject("Microsoft.XMLHTTP");
                } else if (window.XMLHttpRequest) {
                    xhr = new XMLHttpRequest();
                }
                if (xhr != null) {
                    xhr.open("GET", url);
                    xhr.withCredentials = true;
                    xhr.send(null);
                    xhr.onreadystatechange = function () {
                        if (xhr.readyState == 4 && xhr.status == 200) {
                            css = xhr.responseText;
                            localStorage.setItem(name, css);
                            localStorage.setItem("version", whir.res.pageVersion);
                        }
                    };
                }
            } else {
                css = css.replace(/images\//g, "style/images/"); //css里的图片路径需单独处理
                whir.res.writeCss(css);
            }
        } else {
            whir.res.linkCss(url);
        }
    },
    //往页面写入js脚本
    writeJs: function (text) {
        var head = document.getElementsByTagName('HEAD').item(0);
        var link = document.createElement("script");
        link.type = "text/javascript";
        link.innerHTML = text;
        head.appendChild(link);
    },
    //往页面写入css样式
    writeCss: function (text) {
        var head = document.getElementsByTagName('HEAD').item(0);
        var link = document.createElement("style");
        link.type = "text/css";
        link.innerHTML = text;
        head.appendChild(link);
    },
    //往页面引入js脚本
    linkJs: function (url) {
        var head = document.getElementsByTagName('HEAD').item(0);
        var link = document.createElement("script");
        link.type = "text/javascript";
        link.src = url;
        head.appendChild(link);
    },
    //往页面引入css样式
    linkCss: function (url) {
        var head = document.getElementsByTagName('HEAD').item(0);
        var link = document.createElement("link");
        link.type = "text/css";
        link.rel = "stylesheet";
        link.rev = "stylesheet";
        link.media = "screen";
        link.href = url;
        head.appendChild(link);
    }
};

第三阶段

对于跨域的处理,我们采用了CORS的方式,主要就是在Nginx服务器上配置一下,比较简便。

2016/3/29 posted in  others

应用多级缓存模式支撑海量读服务

2016/3/27 posted in  others