Showing posts with label Project Practice. Show all posts
Showing posts with label Project Practice. Show all posts

Tuesday, May 17, 2016

一种基于“哨兵”的分布式缓存设计



http://www.liuhaihua.cn/archives/86802.html
大促活动下,对于某些产品进行整点秒杀活动。预计流量是平时峰值 5+倍 。
商品计算逻辑比较复杂:某个最终展示的商品属性和价格,可能需要上亿次动态条件计算获得,动态条件每时每刻都在变化,并且商品的库存属性属于行业共有库存,每时每刻都在变化。
计算模型:前端机并发去后端获取实时计算数据,然后合并结果, 根据用户信息 给商品打属性,排序。

头脑风暴

针对这种场景,有很多方案可以尝试,不过总结起来,大概俩个方向: 扩容 和 缓存 。

扩容

扩容是最容易想到的方式,而且每年大促,根据压测和运营活动预期,都可能有相应扩容。扩容从某种程度上说,也是最简单的方式,如果应用规划足够好,没有状态,那么基本不用开发介入就能完成。
但是如果应用涉及状态信息,那么扩容就没有说的那么轻巧,扩容涉及到增加集群状态;活动结束后,机器下线涉及集群减少状态,这一增一减,增加了运维的成本和系统稳定性。
扩容还有一个不好的地方就是活动结束后,系统水位下降,闲下来4倍的机器,比较浪费。

缓存

相对扩容,缓存是一种从应用角度出发,优化系统的方案。缓存的方案可细分不同粒度,分别适用不同场景。

静态化

静态化能最大限度降低最大限度降低后端压力,一般静态内容可以定时或者通过数据更像触发生成,然后推送到CDN节点。静态化适用于1) 读多写少的数据 ,或者2) 能够容忍数据变化延迟 的场景。对于本文介绍的场景,并不适合,原因在于商品不满足前面说的两点,并且每个登陆用户看到的产品价格和属性是不同的。

缓存中间结果

静态化这种”一刀切”模式,不能满足针对每个用户的个性化展示需求,如果把每个用户看到的数据都静态化,那缓存的命中率有会很低,基本每个用户请求一两次就不会再来了。而且缓存数据量巨大。
由此想到把缓存粒度缩小,把缓存从展示层后推到 前端机 上。因为前端机负责汇总后端结算结果,并根据用户信息给商品打属性,排序。

缓存方案尝试

经过头脑风暴,最终确定采用 缓存中间结果 方式。接下来讨论一下方案细节。

简单粗暴方式

如果缓存有数据,取缓存数据,如果没有,请求后端并把结果更新到缓存。 这是一种最简单的缓存模式,但不幸的是不适合秒杀场景,因为秒杀开始的时候,缓存很能没有数据,请求会 穿透 到后端。

实时缓存,异步更新方式

实时请求数据来自缓存,缓存数据定时异步更新。 粗看起来,这个方案不存在 缓存穿透 的情况,因为数据不会实时从后端计算获取,而是从缓存获取,如果缓存数据存在,直接获取即可。缓存更新可以把用户请求汇总后去重,定时更新。
上面讨论的两种方式都一个共性问题: 第一批请求 问题: 如果第一批请求缓存没有数据怎么办?
简单粗暴 的方式会让这样的请求 穿透缓存 ,后端去处理并更新缓存。这样会给后端计算带来压力,秒杀开始那一刹那,很可能支撑不住。
而 实时缓存 的牺牲了这样的请求,因为这些请求根本看不到数据,所以请求失败。这两种方式在本文的应用场景都不合适。

为何不提前初始化缓存?

的确,上面两个方案如果能在 第一批请求 到达前初始化好缓存,那基本上可以满足本文的应用场景的。而且看起来也很容易做到,提前 模拟一次请求 或者 提前往缓存放一份数据 不就可以了吗?
不幸的是,本文场景因为涉及数据范围巨大,不能在较短时间内遍历缓存key,初始化好缓存,即使采取并发方式。而且,初始化缓存请求过多,也将给后端机器造成压力。

缓存失效又该怎么办?

根据需求,两种方案的缓存 不会永久有效 ,如果缓存失败了怎么办?
对于 简单粗暴 方式,如果缓存失效,又会遇到 第一批请求 问题,一批请求发现缓存失效,怎么办?看来即使解决了缓存初始化问题,还有可能导致缓存穿透。
实时缓存 模式也有类似问题,如果异步更新前数据已经失效了,那么将牺牲一批数据失效后到更新前这批用户。因为没有人去更新数据。

缓存更新问题

不管哪种方式,分布式缓存更新都存在并发问题,尤其在整点秒杀场景更为突出。对于 简单粗暴 方式,可以采用分布式解决:如果缓存穿透的一批请求只有一个会真正打到后端是不是就可以解决了?
实时缓存 也有同样的问题,只不过异步请求可以把一段时间内的重复请求合并成一个,从侧面避免了并发问题。

更好的缓存方式

把上面的讨论结合,可以得到一种更优雅的缓存方案,既不牺牲第一批请求,也不存在缓存穿透问题,同时避免并发更新问题。

哨兵

想象有这样一个哨兵线程,只有它能去后端请求实时数据,并更新缓存。
第一批请求场景

第一批请求中,选取最早的那个请求为哨兵,这个线程不会去读缓存,直接去后端获取计算结果并更新缓存。其他普通线程则自旋+sleep等待,直到哨兵更新缓存后,能拿到数据为止。
缓存失效场景



哨兵的作用是让缓存永不失效。哨兵线程 提前苏醒 ,去后端获取计算数据并更新缓存。这样,其他普通线程根本不会感知到缓存已经失效,他们能一直拿到最新的缓存。
例如,某个key的缓存失效时间的 12:00:00 ,那么哨兵可能在 11:59:55 的时候苏醒,请求后端并于 11:59:57 的时候完成缓存数据更新,后续请求线程感知不到数据的更新,一直能取到非过期的数据。

实现细节

哨兵:其实哨兵也是一个普通请求。可以用原子计数器(redis或tair)实现,一个数据有两个key:原子计数器key和数据缓存key,二者缓存时间一致,但是计数器key失效时间比数据key的要早( 至少提前一个后端请求RT时间 ,这样能保证哨兵更新缓存后,不被其他线程感知到)。当请求线程发现缓存没有数据的时候,每个线程去更新计数器,更新后,得到计数器为1的线程,被设置成哨兵线程,其他线程则等待哨兵。
普通请求没有获取到数据的时候,自旋+sleep应该有个超时时间,防止意外情况。如果超时了,根据业务场景选择请求后端数据还是处理失败。
http://yangxikun.github.io
通常的缓存访问如下,箭头表示访问量,且为同一时刻访问。
cache access
如果Redis缓存命中,那么web就不会访问数据库,否则,客户端有N个并发请求就会有N个对数据库的并发请求,伴随而来的可能会是N个Redis SET操作。
为了消除这种情况下多余的请求,减轻数据库压力,引入一个“哨兵”请求,即当缓存不命中时,只有一个请求能落到数据库上,其余请求等待缓存更新。
cache access
为了在并发请求中选出一个“哨兵”,对于一个缓存需要有一个计数器与其对应。以下是借助Redis实现的算法流程:
cache access
有一个需要注意的问题就是,上图中红框部分执行失败:例如MySQL无法访问了,那么count将会不断递增,即使MySQL恢复正常了也如此,因为没有请求的count会再次为1。解决办法:
  • 手动设置count为0;
  • 给count设置上限,当达到上限时设置count为0;
  • 将“=1”的条件判断改为类似“count%10=1”
http://m.blog.csdn.net/article/details?id=50915007

Monday, November 9, 2015

实时投票系统



http://segmentfault.com/a/1190000000495903
实时投票系统:数据类型上的差异:memcache和redis
最近要写一个类投票的系统:由于访问量可能会比较大,最好不直接使用mysql数据库,完全使用缓存的话,存在缓存失效等的风险,因此在mysql上面写个缓存中间过渡:
1.memcache
按照我的习惯,肯定是使用redis,但是公司目前这个项目线上还没有redis服务,只好凑合一下使用memcache。
第一次的时候使用memcache
这么写:
key为'13453' value为13920,
key为'13454' value为12989,
...
但是这么一来的话就不好排序了,每次想排序,都要从缓存中取出很多个值。
第二次这么写:
memcache中存储了一个数组: 'pid' =>count
array(
'13453' =>13920,
'13454'=>12989,
...
)
每次有人投票需要先取出该数组,然后再对应的pid上面加1,排序,再存入memcache中,代码如下:
[php] view plaincopyprint?在CODE上查看代码片派生到我的代码片
/**
* @brief 从memcache中取出数据,并相应增加一,然后排序
*/
private function _addCount($uid) {
if(empty($uid)) {
return false;
}
$mc_key = "MEM_KEY_SORT";
$list = self::memcache()->get($mc_key);
$list[$uid]++;
if(empty($list)) {
$list = $this->_getCount();//数据不存在则查询数据库(group by)
}
asort($list);
$ret = self::memcache()->set($mc_key, $list);
}
[php] view plaincopyprint?在CODE上查看代码片派生到我的代码片
/**
* @brief 从数据库获取投票次数
* @return array('uid' => '次数')
*/
private function _getCount() {
$sql = "select pid,count(pid) as count from tmp_ssjj_zqwz group by pid";
$query = self::db()->query($sql);
while(($row = self::db()->fetch_array($query))!==FALSE) {
$list[$row['pid']] = $row['count'];
}
return $list;
}
数据结构如下:
CREATE TABLE tmp_ssjj_zqwz (
id int(11) NOT NULL AUTO_INCREMENT COMMENT '自增编号',
uid int(11) NOT NULL COMMENT '投票id',
type int(11) NOT NULL DEFAULT '0' COMMENT '投票类型',
pid int(11) NOT NULL DEFAULT '0' COMMENT '被投票id',
PRIMARY KEY (id),
UNIQUE KEY unique_uid_type (uid,type),
KEY index_pid (pid)
) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=gbk;
加上索引,使用group by来统计数据:
select pid,count(pid) as count from tmp_ssjj_zqwz group by pid
2.使用redis中的有序集合,直接解决问题:
value为'13453' score为13920,
value为'13454' score为12989,
...
一段代码,直接搞定:
$list= $redis->zRange($_GET['key'], 0, -1);
最后,我是redis的fans。比较支持redis
http://segmentfault.com/a/1190000000510605
实战--积分投票系统中的细节:
好几天没有写博客了,一直忙这写这个积分投票-兑换礼包系统.有很多血泪的教训来分享下:
之前,我一直是写手机接口的,跟前端基本上没有交集,即使有也是给内部提供管理平台,这次可以说给我上了一堂课:
1.实时性:大量的操作是建立在memcache缓存的基础上的,mysql数据库是为了提供数据持久性和记录日志的.因此查询之前会先访问缓存,如果缓存不存在或者失效,去访问数据库.
2.数据量比较大:数据库的索引没有建立到最好.
3.权限的问题,不是所有人都拥有权限可以投票,于是我把拥有权限的用户id放入了一个数组中并放在缓存,同步到数据库里面,最后发现这次投票人数特别多,每次查询一个用户的权限,需要把所有人的权限都拿出来,使用in_array(),来判断,主管发现后,果断改啊,每个用户使用独立的key来保存是否拥有权限.
4.安全性:从前端传递过来的参数要假设是不可靠的,在兑换礼包的时候,我从前端传递过来了礼包的类型和要扣除的积分,放入数据库中,哎!首先悲剧的就是要扣除的积分不能是传递过来的,而是根据礼包的类型来判断.并且要把礼包的类型做出范围配置来检查是否合法.刚写完程序的时候,主管看了看然后请求了一个url: www.example.com/?type=6&score=1,本来第六个礼包要扣除积分80个,结果现在只扣除1个积分用户就领取到了第六个礼包.然后要判断礼包只有六中type应该大于1,小于7.写成配置文件来判断如下:
/**
 * @brief 将礼包对应扣除的积分写成配置,不要相信用户提交的数据,
 * @return array();
 */
public function getTypes() {
    return array(
            11 => 5,    //礼包1对应扣除的积分5,同下
            12 => 10,
            13 => 20,
            14 => 30,
            15 => 50,
            16 => 80
    );
}
5.防止刷积分的处理:如果用机器大量快速请求,有可能出现服务器挂掉,且出现一个人领取多个礼包,因此将用户id:uid和礼包类型:type做唯一索引:UNIQUE KEY unique_uid_type (uid,type),
在memcache中存入一个1s过期的key,保证一个uid每秒只能领取一次礼包.
6.一些非实时的数据,采用数据查询时从缓存中读取,不存在再从数据库读取;增删改时清除该key的缓存.下次从数据库读取.结果由于只改变了查询的时候的缓存的key,没有改 增删改 数据时缓存的key,导致数据全部从数据库中读取..因此一定要保证,写入缓存和读取缓存的key保持一致.否则...
7.由于有很多页面,需要共享用户的信息,而用户信息需要经过处理,就把用户的信息放到了__construct()里面,也就是说所有的页面都会处理用户信息,结果有几个页面是不使用用户信息的...
8.命名的规范:对缓存命名时必须遵循团队规范,否则可能会和其他开发人员发生缓存key冲突.
9.特殊字符的编码处理:用户名是utf-8编码的,而程序中是gbk编码的,结果转码后发现,用户名凌乱了.处理办法:copy到txt文档中使用记事本打开,重新复制一遍,很多的用户名中会出现特殊字符比如双引号等,使用:htmlspecialchars();
10.数据表中增加一个create_time,用来记录时间排除bug
11.缓存设置的时间要根据具体的业务逻辑来设计,比如下面的场景:用户投票和兑换积分一般时间不会超过十分钟,因此,一些非实时的缓存的过期时间设置成10分钟就够了
12.将所有权限的检测封装到一个函数中统一处理,是代码更简洁方便
13.json_encode()不能处理gbk编码:json_encode('gbk','utf-8',$array);

Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts