我们的角度是:如何实现一个高并发的投票系统?
我们需要一个怎样的投票系统?笔者分别浏览了目前创造101的各个投票通道,基本的要求有以下几个
1、只有登录用户才能投票 2、每个用户投票数有限 3、不同用户可投票数不同,如Vip会比普通用户的票数多 4、投票是限时的,只有在有效时段内才能投票
除了以上几个功能性要求外,作为一个开发人员,还需要考虑以下几个非功能性需求:
1、计数要准确 2、可以处理高并发投票 3、可以处理大量的投票数据 4、要有很好的可用性 5、要把每个人的投票记录下来
登录验证:
投票网站都是需要登录验证的,用户想要进行投票,需要先登录。目前很多大型网站的登录都采用单点登录(SSO,Single Sign On)技术,SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它是目前比较流行的企业业务整合的解决方案之一。
实现SSO的技术主要有以下几种:
(1)基于cookies实现。 最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。但是存在几个问题;1、cookies并不安全,2、cookies本身不跨域。3、浏览器对cookies个数和大小有限制。但是,如果想要解决的话,以上三个问题还是可以找到方案的。只不过会让整个方案变得复杂而已。
(2) Broker-based(基于经纪人),例如Kerberos等;这种技术的特点就是,有一个集中的认证和用户帐号管理的服务器。经纪人给被用于进一步请求的电子的身份存取。中央数据库的使用减少了管理的代价,并为认证提供一个公共和独立的”第三方”。例如Kerberos,Sesame,IBM KryptoKnight(凭证库思想)等。Kerberos是由麻省理工大学发明的安全认证服务,当前版本V5,已经被UNIX和Windows作为默认的安全认证服务集成进操作系统。
(3) Agent-based(基于代理人)在这种解决方案中,有一个自动地为不同的应用程序认证用户身份的代理程序。这个代理程序需要设计有不同的功能。比如,它可以使用口令表或加密密钥来自动地将认证的负担从用户移开。代理人被放在服务器上面,在服务器的认证系统和客户端认证方法之间充当一个”翻译”。例如SSH等。
(4) Token-based,例如SecurID,WebID,现在被广泛使用的口令认证,比如FTP,邮件服务器的登录认证,这是一种简单易用的方式,实现一个口令在多种应用当中使用。
(5) 基于安全断言标记语言(SAML)实现,SAML(Security Assertion Markup Language,安全断言标记语言)的出现大大简化了SSO,并被OASIS批准为SSO的执行标准。开源组织OpenSAML 实现了 SAML 规范。可参考http://www.opensaml.org。
目前,很多大型网站都采用一个开源的SSO解决方案——CAS,CAS由耶鲁大学开发的单点登录系统(SSO,single sign-on),应用广泛,具有独立于平台的,易于理解,支持代理功能。
权限控制:
在投票网站验证完用户的登录信息之后,紧接着会判断用户的权限,然后根据不同的权限来给用户分配不同的可投票次数。
关于权限的设计,一直是很多网站都要关心的问题。几乎所有的网站都会有一定的权限要求。
目前,关于权限设计大部分均采用RBAC理论(Role-Based Access Control),即基于角色的权限访问控制。
RBAC认为权限授权实际上是Who、What、How的问题。在RBAC模型中,Who、What、How构成了访问权限三元组,也就是“Who对What进行How的操作”。
一个简单的权限系统应该包含以下几个基本元素:
用户、角色、权限、资源、操作:。
【用户】可以属于多个【角色】。【角色】可以认为是【权限】的合集。【权限】描述的是对【资源】的可【操作】能力。
比如在“pick王菊”这件事上,虽然大家都是“陶渊明”(王菊的粉丝自称陶渊明,因为陶渊明爱菊花),但是有些用户的角色是VIP用户,有些用户的角色是普通用户。VIP角色的用户在投票权限上就比普通用户更高。
限时开启:
几乎所有的投票活动都是有一个起止时间的,只有在有效时间段内用户才可以参与投票。也就是说,当活动未开始的时候,用户来到投票页面,投票按钮应该是置灰无法点击的,有些网站还会给出倒计时的提示。
这其中就涉及到很多问题了。如何在时间到达时将按钮点亮呢?用户通过非法手段在有效时间外点击按钮如何处理?到时间后按钮又如何置灰?
一般情况下,用户访问的投票页面被设计为静态页面,被缓存在 CDN 与反向代理服务器中,甚至在用户的浏览器上。所以在投票活动未开始时,用户的刷新页面请求是不会到达应用服务器。同样,在后端的投票接口中,在接受到用户的投票请求时,也要做时间有效性的校验。
我们在秒杀商品的静态页面中加入一个 JavaScript 文件引用,它包含投票是否已开始的标志。秒杀开始时,系统会生成一个新的 JavaScript 文件,它会被浏览器加载(刷新页面或定时脚本),这样就能点亮页面中的购买按钮。这个 JavaScript 文件使用随机版本号,软币网认为确保它不被浏览器、CDN 和反向代理服务器缓存。
同理,到达活动截止时间的按钮置灰也通过js引用的方式可以解决。
准确计数:
对于一个投票系统来说,最重要的就是计数了。要保证在高并发的情况下用户的投票既不能多也不能少这是一个很大的挑战。
在“pick王菊”的投票中,计数场景有多处需要。比如对于王菊的票数需要准确的记录下来。还有,会员的已投票次数也要准确的记录下来。这里我们就拿被投票者的票数来举例。
基于MySql计数
我们能想到的最简单的方法就是在MySql数据库表中创建一条记录,记录当前被投票者的票数即可。如:
CREATE TABLE IF NOT EXISTS `table_user_polls`(
`id` INT UNSIGNED AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`polls` bigint NOT NULL
PRIMARY KEY ( `runoob_id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
当然,这只是一张记录总票数的表。当有用户投票的时候,我们通过修改表中的polls的值来进行:
update table_user_polls set polls = polls +1 where user_id = "1000";
当采用以上的update语句时,基本可以满足简单的票数的递增。但是,一旦有高并发场景的话,就无法满足了。因为多人同时执行这个语句的时候,就会有的人的更新结果丢失。
这时候,就需要引入锁机制来处理。比如我们增加乐观锁,改进后的update语句如下:
update table_user_polls set polls = polls +1 where user_id = "1000" and polls = 1000000;
在更新table_user_polls表的polls字段的时候,我们先把表中当前的polls值取出,然后作为乐观锁来控制更新,如果发生并发,只会有一个请求可以更新成功。其他请求就会更新失败。
但是,紧接着又有两个问题来了。
数据量太大:
如果数据量太大怎么办?比如这场“全民Pick王菊”的行动,参与的用户可能有很多很多,而我们要对每个用户的投票数据都持久化下来。这就涉及到一旦数据量过大,就会导致数据库的读写性能急剧下降。
传统的做法是进行分库分表,把投票表按照一定的维度进行拆分。比如这种投票的场景,我们可以按照投票的用户的userId拆分,对userId取模,根据结果存储到不同的表中。如把所有投票数据拆分到8个库128张表中,路由规则就是userId%128。
其实,这种单纯的分库分表还有一个另外的问题,虽然投票场景中可能涉及不到。
1、热点数据问题。如多个热点用户的数据都分到了同一个库甚至同一张表中,就会又给数据库带来巨大压力。
2、数据的二次分表问题。一旦分表数量不够了,就要再次分表。比如把128张表扩展到256张表,这就麻烦了,需要对原来的1287张表中的数据重新进行拆分,数据迁移到新的256张表中。
访问量太大:
通过分库分表之后,我们暂时解决了数据量大的问题,那么如果遇到访问量大的问题怎么办。无论是我们的应用、服务器还是数据库等,能承受的QPS(TPS)都是有限的。如果峰值的qps超过了限制,就有可能导致整个系统瘫痪。
那么如何提高一个服务或者接口的QPS呢,其实一个最简单的方法就是降低响应时间(RT)。当然RT和QPS的关系还会受最佳线程数等影响,但是降低RT也是一个比较有效的办法。如果一个请求,在执行过程中,什么都不需要需要等待,每个操作都可以快速执行(如单纯的在内存中取数、计算等),那么RT就会低很多。
在程序中,导致请求阻塞的愿意可能有很多,数据库操作可能是其中比较重要的一部分。虽然我们通过分库的方式增加了数据库的连接数,但是直接操作数据库还是有很大的性能损耗的。
这时候,就要考虑在持久化存储前面增加缓存了。在访问数据库之前先访问缓存,如果缓存中没有的话再访问数据库。这样可以减少请求的响应时间,从而提高QPS,进而承载更大的访问量。
这样做有很大的好处,但是也不是完美的方案。比如这种投票系统,计数更新是非常频繁的,所以要经常失效缓存在重新缓存,缓存和数据库之间的数据一致性问题就体现出来了。
以上,是通过MySql来进行计数的方案,总结一下:
优点:便于理解、学习成本低、开发成本也低。 缺点:对大数据量和高并发量支持不友好。
基于Redis计数:
Redis 是目前 NoSQL 领域的当红炸子鸡,它象一把瑞士军刀,小巧、锋利、实用,特别适合解决一些使用传统关系数据库难以解决的问题。
如果我们使用Redis来实现计数器的话,就相对来说简单一些了。因为Redid提供了一个INCR命令,其使用方法如下:
redis> SET wangju_polls 20
OK
redis> INCR wangju_polls
(integer) 21
redis> GET wangju_polls
"21"
INCR key语法,可以将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。最重要的是INCR是一个原子性的自增操作。非常适合用来实现计数器。
引入了Redis之后,遇到高并发和大数据量的问题解决起来就简单了——堆机器。
当然,这个方案虽然在很大程度上解决了大数据量和高并发的问题。但是,如果真的是业务量特别巨大,总不能无限制的增加通过增加机器来解决问题吧,机器就是成本啊。
关于这种问题,微博就遇到了,因为微博的点赞功能和我们的投票功能其实是类似的。明星的一条微博的点赞数可能有几十万,甚至百万以上。有人算过一笔帐:
假设 key 为8字节,value为 4字节,通过incr存储的话:
一个 value 通过 createStringObjectFromLongLong 创建一个robj,由于value在LONGMIN 和LONGMAX 之间,所以可以将value用 ptr指针来存储,需要占用 sizeof(robj) = 16 字节;
一个key(即微博id) 最长64位数字(Eg: 5612814510546515491),但通过 sdsdup 以字符串的形式存储,至少需要 8(struct sdshdr)+19+1 = 28字节;
为了存到Redis 的dict里面,需要一个dictEntry对象,继续 3*8 = 24字节;
放到db->dict->ht[0]->table中存储dictEntry的指针,再要 8个字节; 存储一个64位key,32位value的计数,Redis也至少需要耗费: 16 + 28 + 24 + 8 = 76 字节。
1000亿个key全内存的话,就至少需要 100G * 76 = 7.6TB 的内存了(折算76G内存机器也需要100台!)。
我们的有效数据其实是 1000亿32位 =400GB,但是却需要7.6TB来存储,内存的有效利用率约为: 400GB/7600GB = 5.3%.
总的来说,Redis做为优秀的内存数据结构,接口方便,使用简单,对于小型数据量的中高访问量的计数类服务来说,是一个很不错的选择,但是对于微博计数器这种极端的应用场景,成本还是无法接受!
所以,微博的点赞功能,其实是在Redis的基础上进行了二次开发。如在数据机构优化、转发和评论数 Value的优化、key的优化、数据的持久化、一致性保证等方面做了很多事情。这里不详细介绍了。
其它注意:
如何避免刷投票
在创造101的投票规则中,明确规定了:请公平参与点赞,如采用违法或违反赛事规则的点赞行为,将会被收回相关点赞数并追究责任。
那么,如果我们是这个投票系统的开发,如何有效的避免刷票行为呢?
首先,我们要通过设计一个很好的计数器,能够有效的避免高并发请求带来的计数错误。因为投票时可能有很多人使用脚本等构造多条请求,试图来突破限制来多投票。
其次,还可以通过一些其他限制手段来防止恶意刷票,如限制同一IP的投票次数、限制同一帐号的投票频率等。
避免数据溢出
据说,在前段时间的MSI比赛前期,MSI的助威活动中,人气选手uzi的票数达到了网站开发人员设置的int的最大值。
以上只是个传说,我并没有去辩证他的真伪,但是这至少给我们一个提醒,在设计投票系统的时候,要充分的考虑到粉丝们的热情和实力!
持久化数据的备份
无论最终选用那种方式进行计数,数据的持久化问题都至关重要,一定要做好数据存储的容灾工作。避免由于系统问题导致数据丢失。
PS:作者并没有在工作中开发过实际的投票系统,以上总结均是基于日常工作中的积累及一些参考资料总结得出。欢迎大家指正与讨论。
整理来自:www.ruanally.com