【IT168技术分析评论】
我个人很喜欢反范式这种应用,每次数据库设计用到时心里都有点欣喜,或许是因为它之间夹杂着那么点美感吧。时而提醒我计算机不仅仅是一门科学,也是一门艺术。We are not just Coder. We are all Designer! 不是吗?谢谢Dylan Tang带来的的好文章。下面通过Dylan的文章来侧重讨论一下反范式的使用,同时也谈了点memcached使用的个人想法。首先申明,对于Dylan的最终方案我是赞同的。
反范式确实违反了三个范式的关系数据理论,那我们为什么不做个乖孩子而要去违反范式设计理论呢?下面从Dylan举的那个例子来说起什么场景下应该使用反范式。假如我们现在要查询大众点评网社区里面最新的10个帖子,如果按照范式设计,那么可能要关联两个表,一张是帖子表,另外一张是会员表,整个查询如下:
SELECT TOP 10 N.帖子标题, U.会员昵称,N.会员ID FROM 帖子表 N
JOIN 会员表 U
ON N.会员ID=U.会员ID
ORDER BY N.帖子ID DESC
上面的sample如果你懒的仔细看或者没怎么看懂,那么我再给你简单解释一下,讲的就是当我拿出来的10个最新帖子以后,在显示这几个帖子的时候我也要显示会员的昵称,但是帖子那张表没有昵称,只要再次到会员表拿用户昵称了,所以这里就要涉及到两张表了。常用的方法就是像上面sql语句一样用一个自然连接查询来搞定。作为一个solution,这个方案挺好。如果拿出10个新帖子,然后根据每个帖子的会员ID再去会员表拿会员昵称,因为会员ID是加了index的,性能应该还不错。但是Dylan gg嫌这个还是太慢了。当我们的业务逻辑涉及的表要在百万数量级以上时候,当我们在写sql语句时候就一定要小心了,慢点的操作可能几个小时结果都出不来。特别是遇到像join, order, group by, distinct这样的操作,一定要小心一点。这时对算法的设计要求还是挺高的。但实际上上面这个查询应该是挺快的,所以说这个sample举的不大好,虽然可以很清楚让人明白而且它还是个实际的问题,但是它对性能提升的要求还不够强烈。我们姑且假设用这个join慢的如老牛吧,那我们该怎么办?
这不,Dylan Tang提出了第二种解决方案,这次是利用反范式,在帖子表里面添加冗余字段——会员昵称,这样我们就可以通过下面的查询达到同样的目的:
SELECT TOP 10 帖子标题, 会员昵称,会员ID FROM 帖子表 ORDER BY 帖子ID DESC
这个方案不错,把反范式的大概使用场景说出来了。反范式在这种场景下,对于boost performance忒有用。你可以避免连接两张表才就完成了这种要经常操作的查询。仅仅通过一张表就可以搞定,这对于性能提升是很显著的。但是Dylan还是不大满意,他说,”每当一个会员去更新自己的昵称的时候,我们会执行一个存储过程,这个存储过程的目的就是去更新大量的会员昵称的冗余字段,这些更新对于一个活跃会员来说,将是非常耗时的,因为需要更新的数据实在太多,而Web 2.0的精髓之一就是个性化,这样的设计对于个性化来说,有着不可调和的矛盾”,这个理由讲的很清楚。他担心的不是一致性,而是维护一致性需要block修改昵称用户太长时间。这里我们是不是可以深入的讨论一下,有没有其它的方案来解决这个问题?我们是不是可以有选择性的来同步更新影响比较大的冗余字段,其它的就可以通过异步操作来解决。这样我们就不会长时间blocked住修改昵称的用户。实际上这里是表现了一个经典的理论就是Dylan希望通过类似采用事务一样的ACID(Atomicity, Consistency, Isolation, Durability)来保证一致性,但是对于高可用架构网站的架构师更多的考虑应该是采用BASE(Basically Available, Soft-state, Eventual Consistency)吧。BASE 策略是 Inktomi 公司的 Eric A. Brewer 在 1988 年提出的。更多关于BASE请查看文章后面的参考文章。可以通过暂时的不一致来保证用户的可用性,这对于用户来说也是基本可以接受的,对吧。所以这里的反范式方案的Core Problem也从作者遇到的事务操作的可用性问题转变成反范式带来的潜在的不一致风险问题。
为了对比,我说一个新的的反范式使用场景,就举一个我们都熟悉的场景,你现在已经可以看到我这篇文章有多少人阅读了,在博客园后台服务器上有个记录我这篇文章信息的数据库表,这个表里面可能有个字段来统计阅读人数。这时候同样会遇到上面说的问题,当你读我这篇文章的时候,是不是应该在那个字段上加1然后再返回给你我这篇文章内容的响应?我想博客园也同样是屈服于BASE而放弃ACID。它通过ajax来发异步request到一台新的域名服务器上来统计阅读数,它后台可能采用延迟写来避免高频度的写操作。只有当阅读计数器到一定阀值或者每隔x分钟后,才将内存中这个计数flush到那个字段里。这样就可以减轻反范式可能遇到的频繁写操作。我想延迟写算是最常用的性能调优的方法之一吧,提前读也算一种,google ditu里面就充分应用了。
这里插一个问题,为什么cnblogs要用ajax来记录阅读数?关于这个话题,你还可以参考这篇文章<< 网站日志收集方式简介>>。使用这种方式的确有很多优点,首先新的域名是的requests可以突破常用浏览器同一域名的并发数的限制,其次可以使得这种记log操作和后台逻辑脱离开来,类似AOP的思想。再者也可以在爬虫访问时不会被统计,还有就是这种方式容易scale out,可以放在专门的机器上来做这种统计,因为统计的计算比较简单,所以主要遇到的问题还是I/O瓶颈。
遇到上述这个I/O瓶颈怎么办?加新的机器来分摊I/O操作?我们先可以延缓这个加机器的欲望,而采用延迟写来减少一点运营成本。就是我上面说的,当阅读计数器到一定阀值或者每隔x分钟后,才将内存中这个计数flush到统计阅读数那个字段中。你可以自己实现内存管理或者使用memcached等软件来帮助你来管理阅读计数器的存储,但由于memcached重启或者其它操作可能使得内存中的那些缓冲会丢掉,这里你就需要自己来选择符合你需求的方案了。对于memcached你可以在每次重启前先把这些数据flush到DB中。这个方案对统计阅读数应该是可以接受的,毕竟memcached的重启不会那么频繁吧,而且如果阀值不是太高,即使丢了某些数值,还是能够接受的。如果能够给memcached加上persistent功能就可以轻松搞定这个问题了,国内sina做了一个开源的项目Memcachedb,感兴趣可以了解一下,它是结合BDB做的。Sina的blog的访问数好像就这么做的,也佐证了我这个想法。总之,如果我们能够接受带来的不一致风险的话,可以考虑采用延迟写来提升性能。
现在你对反范式使用优缺点应该有更为清晰的了解了吧。小结一下,反范式主要问题是采用ACID时候可用性有问题,而采用BASE时候,可能导致冗余字段的不一致性。优点当然是能够避免很多实时计算来提高性能。后台还可能有这么一个表来详细记录哪些用户看了我这篇文章,它的用途可以是避免重复记录阅读数来保证阅读数的准确,这估计也要记录阅读者的IP吧,不然很难防止用户作弊,同时也可以为网站的数据挖掘提供点基础数据。如果要统计我的文章被阅读次数每次都从这个阅读者记录表重新统计一次,这成本根本没有办法接受,没有人会傻的这么干的。依赖于反范式可以很好解决这个问题,它仅仅通过一点的存储成本就节省了很多的计算,把统计需要的计算细分到每个用户访问时进行的。这里也有可能ajax的http request发送失败。同样,这里可能会出现结果最终不一致。但是我们并不care,丢一个两个没关系。所以我想说反范式某些场景很有用的。Dylan使用反范式的遇到问题你知道怎么克服了吗?什么?你还不知道,我要打你pp。哎,那我再说一遍,选择BASE, not ACID。记住了。我们可以异步更新帖子表那个字段,名字丢失概率应该很小很小,而且我不知道有几个人闲着没事干天天改名字。同时对于这种想保证结果的一致性,你可以在后台有专门的服务来验证表之间的一致性。我想我们是不是应该避免使用外键,避免使用存储过程,数据库表的一致性由外围服务来保证。这应该也是未来的计算模式吧。
由于这一篇文章是想向大家推荐反范式的应用,所以写的比较多。希望没有思考过这种方案的同志们可以正视这个解决方案。偶真的真的很喜欢她,希望你的审美观和我类似。
下面继续分析分析那篇文章,看看作者提出的”王者归来” 解决方案。他的意思是把帖子先拿出来,然后看每个帖子的会员号,然后看他们memcached里面有没有,有就直接拿不需要通过db,没有就需要从DB取,然后塞入memcached,同时得到会员昵称。这个方案挺好,很容易理解。这个方案的确是比那个反范式方案完美的多。我承认这一点。特别是对于涉及会员表的这个应用很合适,为什么说呢,因为用户表是Identity系统以及整个网站应用的核心表,使用很频繁,很多应用都得依赖于用户基本信息。但是我对于他所说的,” 通过缓存系统封装的批量读取的方法得到这些会员的昵称,再显示到网页上”有点疑惑,这个”批量”是什么意思?这对于memcached的命中率应该是影响不小吧,难道是单独用个memcached机器来干这个。我说说我思考的两种方案。第一,如果内存比较紧张的话,可以把最近活跃的用户放在memcached,采用类似LRU的算法,这应该是memcached内存利用很好的方案吧。第二,点评网应该有不少收入了,有银子当然要多买点机器了,那内存就不是问题了。那么我的想法是把所有用户的基本信息load到memcached中,这样就不存在到底该把哪些用户信息放到memcached中的问题了。这时候设计到用户操作都可以飞快,降低了I/O的负载。点评网的餐馆等信息也可以这样干,当然关于memcached的更新策略问题也就出现了,不过还算比较简单的。我来大概把整个用户表放到内存中需要的大概内存量,假设是500万用户,打算放用户ID,假设是个int, 4个字节,用户昵称假设16个字节,汉字得看编码情况了,英文平均8个字母绰绰有余吧,你可以统计一下长度然后调整为更为合适的数值。主要用到的就是这两个字段吧,其它的看情况再加上把。(4+16)*500*10^4bytes/(1024*1024)结果大约95M左右。才用了这么点内存。。。。。。
最后,为了检验一下大家是不是还记得清楚三大范式的定义和统计一下有多人认真看完了这篇文章,请大家跟贴说出你对三个范式的理解。
作者:刘守照
出处:http://liushouzhao.cnblogs.com