用 redis 实现和保护 12306

标签: 工作日志 | 发表时间:2012-01-17 12:20 | 作者:唐福林
出处:http://blog.fulin.org

网上大家都在流传  如何更好的实现一个 12306 ,作为一个技术人员,碰到如此难得的机会,忍不住也想跟着忽悠一把。

纯粹从技术角度出发,来设计一个架构和实现方案是非常容易的。但我相信,真实的 12306 的架构师,需要考虑的不仅仅是技术实现,还有数不清的历史包袱,现实约束,甚至人为限制。所以,这里的描述,只是技术人员关起们来,自己的 YY ,大家姑且看看就行,别太当真。

12306 当前的问题,无非是僧多粥少加定时放票引起秒杀效应,造成拥堵。只要解决了这个问题,大家的意见就不会那么强烈了。

首先来看僧多粥少的问题。车票一共就那么多张,这个系统只需要考虑如何将这些票安全,公平的卖出去。至于是不是还有人没有买上票,这就不用技术人员考虑了。所以,我们的问题其实只有一个:如何解决拥堵问题。

在这个神奇的国度,每个地方都有拥堵问题,公交站台,景区门口,银行柜台,甚至食堂窗口,大家都用同一种办法来解决这个问题:排队。所以我一直很纳闷,12306 为什么一开始没有实现排队的功能,是领导拍了脑袋?还是有其它不为人知的原因?

废话少扯,还是说正经的吧。

Redis 用在这里,不仅仅是一个 NoSQL 存储工具,简直是一个完美的解决方案。下面我就来详细描述一下我想象中的一个架构:

1. LVS 接入,同时将 https 协议转成 http 协议。 按照网上流传的 2kw 的预算,F5 恐怕是用不起了,不过也没事,LVS 加硬件 SSL 卡,50w 预算绝对拿下。

2. varnish 反向代理。cache 住大部分的静态资源,每台机器 3w/s 的 rps 应该可以比较轻松的应对。即使按照 30w/s 的峰值能力计算,也就 10 台机器,30w 预算

3. Nginx 服务静态资源,有 2 台足够,6w 预算

4. Tomcat 服务动态请求,这个量会比较大。但可以从这几个方面入手去减少资源消耗:第一,使用 ajax 请求,而不是页面刷新,减少单个请求的 size;第二,做好频率检测和控制,对于部分用工具来刷的用户,甚至可以采取惩罚措施,以减少无谓的资源消耗。保守估计,20 台机器左右,这个数字要看领导们对于“用户体验”的要求程度。

5. Redis 服务器,96G / 台,价格也就 3w 出头。预算 10 台,将 2kw 注册用户全部加载到内存,将 2k 车次全部加载到内存,将每个车次的每个座位的状态都加载到内存,将每个在线用户都加载到内存,应该都还有内存剩余。为什么要写磁盘呢?大不了给 Redis 服务器配置双电源,再配一个电池!

6. DB 服务器。最重要的 db 服务器,“铁道部”的系统,如果不是 DB2 那就应该是 Orcale 了,应该是连硬件到软件一起买的解决方案吧。这个我没有用过,甚至都没有见过,所以不好估计预算了。如果用我们比较熟悉的 mysql,用户系统应该是一个单独的库;车次信息应该是一个单独的库,而且基本上是只读;车票信息应该有区分当前库和历史库,当前库用来存未售出的车票信息,或者近期刚刚售出的车票信息,数据量小,支持快速读写,以及更新,历史库用来存历史上的车票信息,存档性质;订单信息单独一个库,读写量都不会太大。除了车票信息的当前库建议用 SSD ,其它的库采用普通的硬件就行,预算不会超过 5w/台,按照 1M3S 的标准配置,总体的机器台数在 20 以内,预算 100w 左右。值得一提的是,车票信息的当前库,需要按照车次(甚至是车次+日期)进行分库分表。想提高实时性,提升用户体验,减少投诉?增加当前库的机器投入即可。

硬件设备应该就是这样了,接下来说说一些值得一提的实现细节。

当前 12306 系统一个很受人诟病的实现就是无法登录。用户打开登录页,输入了用户名密码,还耐心的填好了验证码,点击提交,再耐心的等了 30 秒,结果,弹出一个无比丑陋的对话框,说“当前访问用户过多,请稍后尝试”,太坑人了!以至于某公司甚至给做了一个“12306登录助手”出来。让用户登录进来,给他们能买到票的希望,是减少投诉的一个很重要的方面。这个其实一点也不难:将用户信息都加载到 Redis 内存,简单点,key 就是 email,value 就是密码加密串(亲,不能是明文哦!),用 cookie 而不是 session 进行身份验证,用 ajax 而不是刷新页面的方式提交数据和返回应答,这么一来,即使 2 kw 用户同时都登录进来,也只需要三五台 tomcat ,20分钟就搞定了。

用户登陆进来之后,不能立即就给出太多的输入框,让用户输入查询,那样对系统的消耗比较大,不划算。应该先让用户通过出发站和到站,查询好车次(单独的车次信息库,全部加载到内存供查询),选择好乘车日期,然后——点击“取号”按钮,拿一个号,进行排队。是不是很熟悉?去银行开卡,汇款啥的,不都是这么办的么!将每个“车次+发车日期”作为一个单独的队列进行排队,其实就是将火车站售票窗口给移动到互联网页面上而已!想提升用户体验?那就每个登录用户可以同时排 3 个队吧,比如我,想买 18 号或者 19 号的 K21,那我就同时在 “K21+18号发车” 和 “K21+19号发车” 2 个队伍里排着好了。

用户既然已经在排队了,那后台处理就很灵活了:根据 db 的承载能力,将用户有序的带到查余票和下订单的页面(后面命名为 “售票厅” 吧)即可。用户在排队页,用 ajax 请求定期轮询,server 端返回有 3 种:当还没有轮到该用户时,返回当前队列前面还有多少人,这个车次还剩多少张卧铺,多少张硬座等等,当然,这些信息都不是实时更新的,而是定期由后台线程或进程进行更新。读取的时候,只是一个缓存 get 操作而已。另一种就是已经轮到该用户,这个时候,给一个30秒或者60秒的订票时间窗口,要求在这个时间内完成查询和下单操作:查询的时候并不做任何的 lock 操作,而是仅仅显示 db select 的结果(这个地方,可能需要 select master 库),填写完票数,点击提交的时候,才做真正的 lock,随机选择座位,update db,update redis 。这个步骤是直接操作 db,不做任何的缓存,但因为是根据 db 的能力放进来的量,所以性能风险反而最小。还有一种状态,就是该用户的号已经过期,或者订票时间窗口过期,很简单,重新取号,重新排队。

具体到 Redis 操作,大约会是这样:

用户:key 为 user:[email protected] ,value 就是 md5(email + password) ,用户登陆的时候,只需要做一次 get ,即可验证登陆是否成功。

用户参与排队的时候,给用户分配一个 token,token 中建议包含加入排队的时间。

为每个“车次+日期”建立一个队列:key 为 train:queue:k21:20120118 , value 是一个 list ,用户点击“排队”按钮,加入队尾,用 lpush token,后台进程根据 db 负载,从队伍头取出一个用户带入查票和下单页面用 rpop token 即可。后台一个定时任务,定期扫描队伍,更新每个人前面的人数,供查询使用。

为每个“车次+日期”建立一个“售票厅”:key 为 train:selling:k21:20120118 , value 是一个 hash ,field 即 token,value 为过期时间。后台进程定期从队列中 rpop 出一个或多个用户,hset 到 hash 里。并且还需要一个后台进程,定期从售票厅里清除出过期的 token

用户在排队页面,使用 token 作为参数来查询自己的状态:首先 hget 售票厅,确认是否已经轮到自己了,如果没有,则再获取前面还有多少人,预估还需要排队多久,车次还剩下多少票等等信息。如果已经轮到自己,则显示“下一步”按钮,将用户带到售票厅。这一步需要做好安全验证,防止有人采取技术手段绕过排队,直接进入售票厅。

如果不希望用户不停的按刷新按钮,消耗服务器资源?那就来一招狠的:用户每次刷新,则将他在排队中的位置往后排一个!不过采取这样的措施之前,别忘了在醒目位置提醒用户“注意节约我们的带宽资源”!

用户真正下单买票,这是纯粹的业务逻辑,这里就不再细说。下单买票完成后,记得更新一下余票信息缓存即可。

这样的系统很难做么?难,也不难。有的地方因为业务逻辑太复杂,不得不做一些改变,比如亚马逊为了应对圣诞购物季,建了一块云,比如淘宝为了双11双12的秒杀,买了一堆的机器;也有的地方因为各种其它的原因,无法做到完美,比如 12306,比如新浪微博。不过无论现实如何,在我们技术人员的心里,永远保留着一颗追求完美的心,永远保留一股追求更好的劲头,就够了。

谨以此献给我在 12306 上失败的购票经历,和明天的 K21 旅程。

用bShare分享或收藏本文

相关 [redis] 推荐:

Redis 负载监控——redis-monitor

- - ITeye资讯频道
redis-monitor是一个Web可视化的 redis 监控程序. 使用 Flask 来开发的,代码结构非常简单,适合移植到公司内网使用. redis 服务器信息,包括 redis 版本、上线时间、 os 系统信息等等. 实时的消息处理信息,例如处理 command 数量、连接总数量等. 内存占用、 cpu 消耗实时动态图表.

Redis 起步

- - 博客园_首页
Rdis和JQuery一样是纯粹为应用而产生的,这里记录的是在CentOS 5.7上学习入门文章:. Redis是一个key-value存储系统. 和Memcached类似,但是解决了断电后数据完全丢失的情况,而且她支持更多无化的value类型,除了和string外,还支持lists(链表)、sets(集合)和zsets(有序集合)几种数据类型.

redis 配置

- - 谁主沉浮
# 当配置中需要配置内存大小时,可以使用 1k, 5GB, 4M 等类似的格式,其转换方式如下(不区分大小写). # 内存配置大小写是一样的.比如 1gb 1Gb 1GB 1gB. # daemonize no 默认情况下,redis不是在后台运行的,如果需要在后台运行,把该项的值更改为yes. # 当redis在后台运行的时候,Redis默认会把pid文件放在/var/run/redis.pid,你可以配置到其他地址.

Cassandra代替Redis?

- - Tim[后端技术]
最近用Cassandra的又逐渐多了,除了之前的360案例,在月初的QCon Shanghai 2013 篱笆网也介绍了其使用案例. 而这篇 百万用户时尚分享网站feed系统扩展实践文章则提到了Fashiolista和Instagram从Redis迁移到Cassandra的案例. 考虑到到目前仍然有不少网友在讨论Redis的用法问题,Redis是一个数据库、内存、还是Key value store?以及Redis和memcache在实际场景的抉择问题,因此简单谈下相关区别.

redis 部署

- - CSDN博客云计算推荐文章
一、单机部署 tar xvf redis-2.6.16.tar.gz cd redis-2.6.16 make make PREFIX=/usr/local/redis install  #指定安装目录为/usr/local/redis,默认安装安装到/usr/local/bin. # chkconfig: 2345 80 10       #添加redhat系列操作系统平台,开机启动需求项(运行级别,开机时服务启动顺序、关机时服务关闭顺序) # description:  Starts, stops redis server.

nagios 监控redis

- - C1G军火库
下载check_redis.pl. OK: REDIS 2.6.12 on 192.168.0.130:6379 has 1 databases (db0) with 49801 keys, up 3 days 14 hours - connected_clients is 1, blocked_clients is 0 | connected_clients=1 blocked_clients=0.

转 redis vs memcached

- - 数据库 - ITeye博客
传统MySQL+ Memcached架构遇到的问题.   实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:.   1.MySQL需要不断进行拆库拆表,Memcached也需不断跟着扩容,扩容和维护工作占据大量开发时间.

Redis优化

- - 数据库 - ITeye博客
键名:尽量精简,但是也不能单纯为了节约空间而使用不易理解的键名. 键值:对于键值的数量固定的话可以使用0和1这样的数字来表示,(例如:male/female、right/wrong). 当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能,不过一般都要持久化比较安全,而且是快照和aof同时使用比较安全.

笔记--redis

- - 移动开发 - ITeye博客
接着准备面试内容,今天学习了下redis,继续我的笔记加深印象. 1.为什么要使用redis.  答:主要是 性能和 并发两个方面,另外redis也可以做分布式锁和消息队列等其他功能. 但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis.

[转]Redis作者谈Redis应用场景

- notsobad - heiyeluren的blog(黑夜路人的开源世界)
文章来源:http://blog.nosqlfan.com/html/2235.html. 毫无疑问,Redis开创了一种新的数据存储思路,使用Redis,我们不用在面对功能单调的数据库时,把精力放在如何把大象放进冰箱这样的问题上,而是利用Redis灵活多变的数据结构和数据操作,为不同的大象构建不同的冰箱.