大概一年前,我写过一篇文章聊了聊电影《摇滚学校》——从小就玩摇滚乐,长大不会被人欺。那么今天,就要来聊聊这部电影的升级版《摇滚学校》音乐剧。
杜威·芬恩是一个拥有摇滚梦的“LOSER”,不仅被自己组建的乐队开除,还丢掉了唱片店工作。他常年赖在好友奈德家里,在被好友女朋友催缴房租后,他冒名顶替奈德来到一所传统的精英学校代课。在这里他发现了孩子们的音乐天赋,带领他们玩起了摇滚,并且组建了一支乐队去参加比赛。
The Original Broadway Cast Of School Of Rock – School of Rock: The Musical (Original Cast Recording)
这部音乐剧改编自 2003 年理查德•林克莱特执导、杰克•布莱克主演的电影,在 2015 年正式登陆百老汇。受到音乐剧巨擘安德鲁·劳埃德·韦伯(《猫》《剧院魅影》)的邀请,《唐顿庄园》的主创朱利安•费列斯主笔撰写了剧本,韦伯亲自为它创作了 12 首原创曲目。
韦伯曾表示,“这是一个关于音乐的力量的故事,是一个关于音乐如何改变孩子们的生活的故事。”
他还谈到改编的想法:“相比于《摇滚学校》电影把故事做成以杜威为主角的喜剧,我更希望在音乐剧中,探索杜威学生们的故事,尤其是孩子们和家长的关系。”而他自己操刀了剧中这些动听的音乐,也是因为“孩子们在音乐中寻找自我的过程,让我极受感动”。
不过,这部舞台剧能够打动所有观众,显然不止关于音乐和寻找自我。在这个剧里,主人公和孩子们一遍一遍的探讨着什么是摇滚:摇滚的精神在于愤怒,摇滚的意义在于自由和反抗,摇滚不是为了获奖和成功,摇滚就是摇滚。
与其说这是对摇滚的定义和普及,不如说是创作者们借摇滚对人生发表的“鸡汤”:一种对世俗价值观的反抗、一种对自由不羁的追求、一种对成功学的不屑和嘲讽,当然,还有对生活和家人的尊重与爱。
对于在极度压力束缚下成长前行的中国观众而言,这碗鸡汤也许相比西方观众远更治愈吧。重要的是,这碗鸡汤是如此浓烈可口、温暖人心。就着摇滚和喜剧坐在剧场里来一碗,也许是这不见太阳的日子里最暖心愉悦的慰藉。
在原版电影的基础上,这部剧里又做了对原有剧情的一些扩充,增加了一些桥段以展示群像剧中其他角色更多心理活动,让角色更加生动鲜活。剧中小演员们的唱段,探索了孩子们自己的内心故事,尤其是孩子和家长的关系;莫林斯校长的唱段,则展示了成年人在社会工作的压力下,戴上面具,藏起羽翼,成为社会机器下一个工整的零件的精神状态。
剧中小演员的唱段——The Original Broadway Cast Of School Of Rock – School of Rock: The Musical (Original Cast Recording)
剧中莫林斯校长的唱段——The Original Broadway Cast Of School Of Rock – School of Rock: The Musical (Original Cast Recording)
下面我想讲讲剧中的一些彩蛋和笑点,涉及剧透。
1、物价真的涨了,2003 年出品的电影中,代课老师的周薪是 650 美元,剧中变成了 950 美元;学校一年的学费是 15000 美元,剧中是 50000 美元。学费涨得比工资多得多,美国人的压力也不小啊,难怪川普要 Make America Great Again …
2、小女孩托米卡的双亲由电影中正常的夫妇变成了夫夫,嗯……这很政治正确。
3、莫林斯校长给孩子们上课时的音乐选段是莫扎特的《魔笛》中的夜后咏叹调。
4、第一个小高潮“You’re in the band”,吉他手扎克弹的 Riff 是 The Rolling Stones 的《Satisfaction》,贝斯手凯迪弹的 Riff 是 Deep Purple 的《Smoke on the water》。视频体验一下。
5、选歌手时,班长夏莫在原版电影中和剧中都是唱走调的《Memory》。当现场观众们听到这个不伦不类的《Memory》时,笑声和掌声不断——韦伯自己黑自己也是难得啊。
6、杜威问学生们知不知道乐队大赛的时候,莫名地cue到了 Earth Wind & Fire,该乐队曾经出席过上海爵士音乐节,是非常著名的Soul、Funk、Jazz、Disco乐队。
7、当孩子们问杜威,我们能不能赢的时候,杜威说道:“有一位先贤这样说过,我们是……冠军。”这回cue到了 Queen 。Queen的传记电影《Bohemian Rhapsody》今日在奥斯卡拿了不少奖项,资源早已放出,3月份还将引进国内,各位乐迷们不要错过。
8、杜威给孩子们布置音乐作业发CD的时候,致敬了 Jimi Hendrix,Eric Clapton,灵魂乐女王 Aretha 等人。
9、中文字幕做了不错的本地化,如果有观剧的朋友,可以稍微注意一下。比如——“哦,这只粉笔成了精啊”,“入洞房,进课堂,头一个月最慌张”,有意思的地方很多。
还有很多的彩蛋和笑点等待各位观众观影时自行发现。
《摇滚学校》真的是太好看了。值得和家人一起买票走进剧场,笑一场,燃一把。用三个小时,抵抗这依然追求成功的世俗,抵抗那个内心逐渐向现实低头的自己,同时去拥抱并倾听身边最爱的人。
《摇滚学校》音乐剧在2月22日至3月17日在上海大剧院出演,随后将转战北京、广州、厦门等城市,当地的音乐剧爱好者、乐迷、有过乐队经历的朋友们千万不要错过。
本期节目就到这里,我是体型和杜威老师完美匹配,随时可以COS杜威的蔡老湿。我们下期再见~
古德拜~
]]>那么大家先想一个问题,比如说你在某一个风格上,会比较喜欢一个 artist。例如 Pop 风格下的 Michael Jackson。世界上有千千万万的人喜欢 MJ。
那么我们先“假设” MJ 确实是好的,那我们能不能说喜欢 MJ 的人就有一个好的“品味”呢?
好像不能,是吧?
那么什么才是一个好的“品味”呢?
我觉得是这样——要审美,先审丑。就是说,你得先知道什么是不好的,才能知道什么是好的。所以我觉得重要的不是你看到一个好的东西就能辨别出它是好的,而是你看到一个东西你能说出——“哦,我觉得它不够好”。这就是一个“好品味”的一部分。这是第一点。
第二点在审丑这一方面,你需要能够区分你的主观喜好和这个东西的客观好坏。我们常常说音乐是没有好坏之分的(这是真的),音乐的风格也没有好坏之分;但是歌曲创作的水平,整体的制作水平(歌曲的编曲、混音等等)都是存在一定程度的好坏的。比如说我主观上不是特别喜欢电子尤其是 EDM 类的东西,但是并不是说我不能欣赏它。
作为一个“音乐爱好者”或者“经过系统训练过的玩音乐的人”,不可能喜欢所有的音乐风格,肯定会有一些偏好,比如我就偏好 Jazz,Funk,R&B 之类,对于 EDM,Hip Hop之类就没那么喜欢,但是我还是能够区分出“这是一个好的 Hip Hop,那是一个不那么好的 Hip Hop”。大家应当将自己的主观偏好和作品的客观好坏剥离开来,不能一开口就是“啊,这首歌就是垃圾”。
第三点则是,在某一首作品上,你能够把其中好的部分和不好的部分剥离开。比如你觉得某一首歌不咋滴,你能够确认这首歌的 songwriting 是好的,但是这首歌的制作上方向性和 songwriting 不是很符合,结果导致了最终成品不是那么的好,这种情况在 Pop 制作中是比较常见的;或者说这首歌制作非常好,编曲非常漂亮,混音非常好,而这首歌本身是有缺陷的;或者说这首歌它不应该做成这种风格,不适合这个人唱,这些情况都是有可能的。如果你能够剥离出这些情况,那你就具备了一个比较好的或者说是比较准确的“品味”。
在你具有了这个“品味”的前提下,就能明白——既然它这部分是好的,那部分是不好的,只要把这些不好的部分给替换掉,这个作品就能变得更好。除了对作品进行鉴赏之外,还能够对作品提出一些建设性的建议,那你的“品味”就不单单是停留在欣赏的层面上,更能够对你的音乐创作、制作提供一些帮助,我觉得这样才是一个好的“品味”。
总结来说就是三点:
1、要审美,先审丑。
2、区分个人主观的偏好和作品客观的好坏。
3、将作品内部好的东西和不好的东西剥离开来。
今天的就讲这么多,希望我清晰地向大家传达了内容。
Love & Peace
]]>1、考出 PMP 证书。
2、好好锻炼身体。
结果过去这么长一段时间内的高强度加班,我只能在每周末,稍微做一些有氧运动,维持基本的锻炼,不至于让自己真的变成一个毫无运动习惯的肥宅,这第二点我稍微做到了一些;但是这第一点,我目前还没有做到。
订好的 PMP 目标,到现在还没达成。
不过我现在暂时先买了一本管理学教材开始自学,在时间不充裕到足以准备 PMP 考试的情况下,先通过自学积累一定的管理学知识。
证书不是学习的目的,有效的方法论和知识储备才是我终生学习的目标。
不只是技术人员,所有人都需要保持终生学习。
不要只低头干活,更要抬头看路。
]]>题目大概是这样的:
有一张表 rail 存储着上海地铁线路的信息。line 表示线路,stop 表示站点名称,sequence 表示站点在线路上的顺序。(为了简洁,我直接用 SQL 语句表示数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
题目的要求有两个:
1、查询经过站点最多的站点。
2、给出任意两条线路,查询这两条线路换乘的站点。
第一个要求很简单,直接用下面的语句完成就可以了。
1 2 |
|
倒是第二个要求,我在第一次思考的时候就踩坑了,我写了一个非常低级错误的 SQL。
1
|
|
刚写完最后的分号我就反应过来了,这里不对,正确的做法是得让 rail 表自己和自己 join 之后查询。于是思考了一小会儿写下了第二个 SQL。
1 2 |
|
题目是挺简单的,只是我今后不能直接靠直觉去想解题的思路,还是要多思考一会儿,把解决问题的步骤想清楚。
]]>在大数据、高并发、集群等一些名词唱的火热之年代,select 和 poll 的用武之地越来越有限了,风头已经被 epoll 占尽。
select 的缺点:
单个进程能够监视的文件描述符的数量存在最大限制,通常是 1024,当然可以更改数量,但由于 select 采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
内核/用户空间内存拷贝问题,select 需要复制大量的句柄数据结构,产生巨大的开销;
select 返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO,那么之后再次 select 调用还是会将这些文件描述符通知进程。
相比于 select 模型,poll 使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。
拿 select 模型为例,假设我们的服务器需要支持 100 万的并发连接,则在 _FD_SETSIZE 为 1024 的情况下,则我们至少需要开辟 1K 个进程才能实现 100 万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于 select 模型的服务器程序,要达到 10 万级别的并发访问,是一个很难完成的任务。
由于 epoll 的实现机制与 select/poll 机制完全不同,上面所说的 select 的缺点在 epoll 上不复存在。
设想一下如下场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 连接。而每一时刻,通常只有几百上千个 TCP 连接是活跃的。如何实现这样的高并发?
在 select/poll 时代,服务器进程每次都把这 100 万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll 一般只能处理几千的并发连接。
epoll 的设计和实现 select 完全不同。epoll 通过在 linux 内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+ 树)。把原先的 select/poll 调用分成了 3 个部分:
1)调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中为这个句柄对象分配资源)
2)调用 epoll_ctl 向 epoll 对象中添加这 100 万个连接的套接字
3)调用 epoll_wait 收集发生的事件的连接
如此一来,要实现上面说的场景,只需要在进程启动时建立一个 epoll 对象,然后在需要的时候向这个 epoll 对象中添加或者删除连接。同时,epoll_wait 的效率也非常高,因为调用 epoll_wait 时,并没有一股脑的向操作系统复制这 100 万个连接的句柄数据,内核也不需要去遍历全部的连接。
上面的 3 个部分非常清晰,首先要调用 epoll_create 创建一个 epoll 对象。然后使用 epoll_ctl 可以操作上面建立的 epoll 对象,例如,将刚建立的 socket 加入到 epoll 中让其监控,或者把 epoll 正在监控的某个 socket 句柄移出 epoll,不再监控它等等。
epoll_wait 在调用时,在给定的 timeout 时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
从上面的调用方式就可以看到 epoll 比 select/poll 的优越之处:因为后者每次调用时都要传递你所要监控的所有 socket 给 select/poll 系统调用,这意味着需要将用户态的 socket 列表 copy 到内核态,如果以万计的句柄会导致每次都要 copy 几十几百 KB 的内存到内核态,非常低效。而我们调用 epoll_wait 时就相当于以往调用 select/poll,但是这时却不用传递 socket 句柄给内核,因为内核已经在 epoll_ctl 中拿到了要监控的句柄列表。
]]>有兴趣的可以参考官方的基准程序测试《How fast is Redis?》https://redis.io/topics/benchmarks
Redis快的主要原因是:
完全基于内存
数据结构简单,对数据操作也简单
使用多路 I/O 复用模型
下面主要围绕第三点采用多路 I/O 复用技术来展开。
多路 I/O 复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了 redis 具有很高的吞吐量。
和 Memcached 不同,redis 并没有直接使用 libevent,而是自己完成了一个非常轻量级的对 select、epoll、evport、kqueue 这些通用的接口的实现。在不同的系统调用选用适合的接口,linux 下默认是 epoll。因为 libevent 比较重更通用代码量也就很庞大,拥有很多 redis 用不上的功能,redis为了追求“轻巧”并且去除依赖,就选择自己去封装了一套。
至于为什么 redis 要使用单进程单线程:
代码更清晰,处理逻辑更简单
不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
不存在多进程或者多线程导致的切换而消耗CPU
至于弊端,那也是显而易见的——无法发挥多核 CPU 性能,不过可以通过在单机开多个 redis 实例来完善。
]]>千万不能迷信“有条件要上,没有条件创造条件也要上”。
思路要开阔点,一个项目成功可不仅仅是写代码这么简单。你也做过很多项目效益分析,你自己想想,有几个项目是真的死于技术不行的。如果老板期望过高呢?如果给的资源不够呢?如果其他部门不配合你呢?你做精准营销,人家一线销售根本不鸟你,你怎么精准?如果你能从结果出发,倒推做成项目需要什么。用这些分析去影响业务部门,岂不是比别人挖好了坑,自己边骂边填更好?
确实,为什么要一辈子做爬坑的那个,而不试着拿过挖坑的锹呢?
上一个台阶看问题,就发现影响项目成功的因素有很多,技术只占其中一部分。特别是销售、市场、运营类项目。这些项目本质上还是要和人打交道。无论是做经营分析、做精准营销、做个性化推送、最后还是要靠各部门通力合作。从做好项目的角度来看,还是有很多发挥余空间的。
最后写了一张图。
可能还会有缺陷,希望每当我跨上一个新的台阶的时候,我能够将这张图做得更加完善。
]]>Leetcode
,是不是有点太晚了?
目前只能从简单的题目开始入手,周末主要把数据库和Bash的题目刷掉了,算法和数据结构的题目还是慢慢入手,毕竟已经不用这些很久了。
Leetcode
账号在这里:https://leetcode-cn.com/caiknife/
源代码在这里:https://github.com/caiknife/leetcode
题目描述和单元测试都有,我觉得我做得还行。
加油。
]]>多年以来一直在看coolshell的文章,从很早就在Google Reader里订阅了RSS。直到Google Reader死掉,coolshell都没停更过。我一直从这里面吸取着营养。
这篇文章真的给我不少启示,确实可以从此思考一下,作为一个普通人的发展到底可以做成什么样?
]]>农夫有一只羊,这只羊在第2、3年会生一只小羊,第4年不会生小羊,第5年时羊会死亡,生出来的小羊也是这个规律。求问50年后会有多少只羊?
这道题最直接的办法就是用递归来做,不过我暂时还没想到递归怎么做,所以先用最笨的方法——遍历50年,每一年遍历羊的数组,根据年纪来进行对应的处理。详细见下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
但是这个方法是有问题的,那就是数组占用的内存太大,用PHP来跑的话,50的数据量根本就没办法解决。
1 2 3 4 5 6 7 8 9 10 11 |
|
下面尝试一下用递归的解法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
最后再使用一个移动窗口的解法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
下面我们来看看到底怎么样用redis来实现分布式锁。
第一个是正确性,这个众人皆知。就像Java里的synchronize,就是用来保证多线程并发场景下,程序的正确性。在redis的场合下,并发访问的单位,不再是线程,而是进程。
举个例子,一个文件系统,为了提高性能,部署了三台文件服务器。当服务器A在修改文件A的时候,其他服务器就不能对文件A进行修改,否则A的修改就会被覆盖掉。
锁还有第二个用处——效率。比如应用A有一个耗时的统计任务,每天凌晨两点,定时执行,这时我们给应用A部署了三台机器,如果不加锁,那么每天凌晨两点一到,这三台机器就都会去执行这个很耗时的统计任务,而实际上,我们最后只需要一份统计结果。
这时候,就可以在定时任务开始前,先去获取锁,获取到锁的,执行统计任务,获取不到的,就直接结束。
单机,并发的单位是线程,分布式,并发的单位是多进程。并发单位的等级上去了,锁的等级自然也得上去。以前锁是进程自己的,进程下的线程都看这个锁的眼色行事,谁拿到锁,谁才可以放行。进程外面还有别的进程,你要跟别人合作,就不能光看着自己了,得有一个大家都看得到的,光明正大的地方,来放这把锁。
要怎么在redis里获取一把锁呢?貌似很简单,执行set命令就好了,还是上面文件系统的例子,比如你想修改文件id是9527的文件,那就往redis里,添加一个key为file:9527,value为任意字符串的值即可:
1
|
|
set成功了,就说明获取到锁。
这样可以吗?很明显不行,set方法默认是会覆盖的,也就是说,就算file:9527已经有值了,set还是可以成功,这样锁就起不到互斥的作用。
那在set之前,先用get判断一下,如果是null,再去set?也不行,原因很简单,get和set都在客户端执行,不具有原子性。
要实现原子性,唯一的办法,就是只给redis发送一条命令,来完成获取锁的动作。
于是就有了下面这条命令:
1
|
|
NX = If Not Existed 如果不存在,才执行set。
完美了吗?非也,这个值没有设置过期时间,如果后面获得锁的客户端,因为挂掉了,或者其他原因,没有释放锁,那其他进程也都获取不到锁了,结果就是死锁。
所以有了终极版的获取锁命令:
1
|
|
使用EX参数,可以设置过期时间,单位是秒,另一个参数PX,也可以设置过期时间,单位是毫秒。
好,最后再来看看释放锁。
有人说,释放锁,简单,直接del:
1
|
|
有问题吗?当然有,这会把别人的锁给释放掉。
举个例子:
1 2 3 4 5 6 |
|
所以,为了防止把别人的锁释放了,必须检查一下,当前的value是不是自己设置进去的value,如果不是,就说明锁不是自己的了,不能释放。
显然,这个过程,如果放在客户端做,就又不满足原子性了,只能整在一起,一次性让redis server执行完。
这下redis可没有一条命令,可以做这么多事情的,好在redis提供了lua脚本的调用方式,只需使用eval命令调用以下脚本即可:
1 2 3 4 5 |
|
了解完如何释放锁,再加上之前的获取锁,我们似乎已经可以用redis来实现分布式锁了。
但是,一如既往,问自己一句,完美了吗?没有漏洞了?嗯,很明显不是,上面讲的算法,都有一个前提:只有一台redis实例。
而生产环境里,我们是不可能只部署一个实例的,至少,我们也是主从的架构。redis的数据同步,不是强一致性的,毕竟作为一个缓存,要保证读写性能。
如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master crash,Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。
如何解决这个问题?
针对Redis集群架构,redis的作者antirez提出了Redlock算法,来实现集群架构下的分布式锁。
Redlock算法并不复杂,我们先简单描述一下,假设我们Redis分片下,有三个Master的节点,这三个Master,又各自有一个Slave。
好,现在客户端想获取一把分布式锁:
1 2 3 4 5 6 |
|
当然这个Redlock算法也并不是万能的,也会有缺陷,我也在思考在哪些场景下会有这样的问题。但是在目前绝大情况下来说,Redlock已经足够用了。
]]>那么今天思考一下,如何实现一个中小型FEED流系统。
在系统刚开始创建的时候,可以先考虑下面这两种方式:推模式和拉模式。
推模式,是发生在用户触发行为(发布新的动态,关注某个人,点赞)的时候。在触发时,用户的自身行为会记录到对应的行为表中,其次用户的行为也会记录到自己的粉丝对应动态表中。
上面的流程大概是:
1 2 3 |
|
使用推方式,对需求变更是易适应的。
因为用户每一次的行为,我们都有存储相应的数据。即使变更,只需更改逻辑层代码。另外性能较好,后台数据已经准备好了,无需复杂的SQL查询。当然这样做,也存在很多弊端。
1 2 |
|
拉方式,是发生在粉丝拉取FEED时。粉丝拉取自己的动态,首先会检索自己的关注用户(UID分表)。得到关注的UID之后,再根据UID去查询关注用户发布的帖子。
拉的模式相对是比较简单易实现的,另外对用户关系变更(新增,删除用户)是敏感的。其次也不存在数据存储压力。但在查询的时候,对帖子表本身压力是很大的。尤其是用户本身关注的人很多的话,会有很严重的性能问题。
下面可以使用另外一种方式来优化拉模式。
用户在登录APP时,会发送用户活跃态到服务端。活跃信号塞到队列中,消费者依次读取活跃态UID,得到用户的关注者列表。得到关注者列表后,会去帖子表,查询关注人的发布的帖子。写到用户自己的FEED中。
这种方式和对拉方式而言,能有效避免接口性能问题,相当于通过定时任务提前把用户的动态FEED跑出来。
和推方式比较,推是比较盲目的,这种方式只需针对活跃用户即可,能避免存储浪费。缺点在于实时性不好,用户登录APP后马上进入自己的FEED页,此时如果后台用户动态还没跑完,接口读取的就是历史数据了。当然这种方式不适合知乎,微博这种类型的APP的。
分区拉取,是为了避免频繁查询单一帖子表所采用的一种优化手段。通过对帖子按照时间片分表,每次查询都能均摊到不同的表中,以此减轻主表的压力。
定时推,是以常驻进程的方式读取用户的发帖行为,再批量写入到粉丝的动态表中。这种方式和推方式差不多,只不过可以对多个发帖行为做聚合。
特定用户推,是推方式的一种优化方法。用户发送帖子时,只对活跃的粉丝用户写入。当然活跃用户的判定策略,是需要商定的。
以上几种方案,都有自己的利弊和适用场景。选择最适合自己的就是最好的。
以上的方案还需要不断扩充完善。
]]>单库情况下,服务层在进行长时间的逻辑计算,在这个过程中,可能读到旧数据入缓存。
主从库+读写分离情况下,在主从同步延时过程中,可能读到旧数据入缓存。
第一个问题产生的原因可能是同时有一个请求A进行更新操作,另一个请求B进行查询操作,那么可能会出现下面的情况:
1 2 3 4 5 |
|
第二个问题产生的原因可能是,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,可能会出现下面的情况:
1 2 3 4 5 6 |
|
上面的这两种情况,都可以使用在写操作更新数据库之后,休眠一小段时间之后第二次再删除缓存。休眠的时间根据系统评估下来的写入时间或者是主从数据库的延迟时间上再加上几百毫秒即可。这样在增加一次cache miss的前提下,只花费了很少的成本,就避免了脏数据的长时间存在,保障了数据一致性。
而如果你并不想因为这个休眠时间导致影响到用户体验的话,其实可以使用异步的方式,通过队列来进行第二次删除,在队列里进行这段时间的休眠,从而保证用户体验,加大系统的吞吐量。
如果在第二次删除缓存时失败了怎么办?
假设有一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
1 2 3 4 5 6 |
|
即如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。这种情况下,可以往队列中插入要删除的缓存key,在队列中处理删除缓存直到成功为止,如果可以接受的话,这未尝不是一种可行的解决方案。
暂时先想到这么多,如果有更好的解决方案,我会继续更新。
]]>在时针走满一圈的情况下,也就是12小时内,时针、分针、秒针会重合多少次?
分析一下:秒针60s走完一整圈,每秒的旋转角度是6度,分针速度是秒针的1/60,每s的旋转角度是1/10度,时针速度是分针的1/12,每s的旋转角度是1/120度。那么就需要计算出12个小时的时间内,也就是12*3600秒内,每秒钟三个指针的位置即可。如果度数相同,那么就认为三针重合。
更简单地来说,假设把时针的最小步长定位1,那么分针的步长就是12,而秒针的步长就是720。再针对各自一圈的步长取模,就可以得到每秒时每个位置的偏移量,如果三个偏移量相等的话,
编写程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
所以,在12个小时内,三针重合只有在0点和12点的时候才会出现。这个时候是真正的停止重合。
而在其他的条件下,时针和分针只会出现划过重合,不会出现停止重合。
下面更新一个Golang写的版本,了解一下Golang时间处理的奇葩……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
项目的目的在于:通过框架作者或者框架的代表之间讨论,以最低程度的限制,制定一个协作标准,各个框架遵循统一的编码规范,避免各家自行发展的风格阻碍了 PHP 的发展,解决这个程序设计师由来已久的困扰。
目前的 PSR 中文翻译地址:https://laravel-china.org/docs/psr
基本的规范要求应该达到 PSR-4,随着项目工程的增长、人员的扩招,最终遵循的标准必须越来越高。
以下是从PSR-0到PSR-4的内容,需要详细了解。PSR-5已经废弃,PSR-6以上的有一定概念即可。
PSR-0 自动加载规范(已弃用)- https://laravel-china.org/docs/psr/psr-0-automatic-loading-specification/1603
PSR-1 基础编码规范 – https://laravel-china.org/docs/psr/basic-coding-standard/1605
PSR-2 编码风格规范 – https://laravel-china.org/docs/psr/psr-2-coding-style-guide/1606
PSR-3 日志接口规范 – https://laravel-china.org/docs/psr/psr-3-logger-interface/1607
PSR-4 自动加载规范 – https://laravel-china.org/docs/psr/psr-4-autoloader/1608
PSR-4 自动加载规范 – 示例文档 – https://laravel-china.org/docs/psr/psr-4-autoloader-example/1609
PSR-4 自动加载规范 – 说明文档 – https://laravel-china.org/docs/psr/psr-4-autoloader-meta/1610
面试官想考察我的真正场景是——如何设计一个直播平台的弹幕系统。
晚上回来赶紧再复盘一下这个问题。
我依稀记得面试官的问题是这样的:
不使用PUSH方式,不使用长连接的方案下,如何设计一个(直播平台的)弹幕系统,并且能够突出显示我自己发的弹幕。
在B站这样的弹幕网站里,除开直播频道之外,每个单独的视频应该都是把已有的弹幕都存储起来,而且由于B站每个视频的弹幕是有上限的,这样就保证了数据不会超载,所以最简单的方式就是可以直接使用 redis 的 list 来实现,单条数据存储的可能是像下面这样的数据结构:
1 2 3 4 5 6 7 |
|
当然如果稍做一些修改的话,也可以用 redis 的 sorted set 来实现。在这样的场景下,只需要在后端从存储中获取到每个视频对应的弹幕数据,排序好之后交给前端处理就好,甚至还可以不用后端做排序,让前端根据偏移时间自行做排序减少服务器的资源消耗。而要突出显示我自己的弹幕的话,只需要写完弹幕发送的时候,直接由前端处理先实时显示在屏幕上,然后再上报给后端接口存储起来就好。
但是,直播系统的弹幕和这上面的思路完全不一样!!!
直播间消息,相对于 IM 的场景,有其几个特点:1、消息要求及时,过时的消息对于用户来说不重要;2、松散的群聊,用户随时进群,随时退群;3、用户进群后,离线期间的消息不需要重发。
对于用户来说,在直播间有三个典型的操作:1、进入直播间,拉取正在观看直播的用户列表;2、接收直播间持续接收弹幕消息;3、自己发消息。
在这样的场景下,初步的设计可以做成这样——选择了 redis 的 sorted set 存储消息,基本操作如下:
用户发弹幕,通过 zAdd 添加数据,其中 score 是弹幕的发送时间;
接收直播间的消息,通过 zRangeByScore 操作,两秒一次轮询;
进入直播间,获取用户的列表,通过 zRange 操作来完成;
整个系统的流程应该是:
写流程是: 前端提交弹幕给后端 –> 后端将弹幕推入队列 –> 队列处理机进行处理 –> 存储到 redis
读流程是: 前端轮询请求后端 –> 后端使用 zRangeByScore查询 redis –> 前端按时间顺序显示弹幕
这个初步方案可能只能在直播人数较少的情况下起效,随着人数越来越多,瓶颈很快就能达到,会产生一些问题。
第一个问题——消息串行写入 redis,如果某个直播间消息量很大,那么消息会堆积在队列中,消息延迟较大。
这个问题需要使用合适的消息队列来进行处理,由于我目前使用的最多的消息队列只有基于 redis 的 resque 和基于 Golang 的 nsque。没有做过详尽的性能测试来确定这两种队列能处理多大的 QPS,如果可以的话那就自然最好;如果不行的话,那就要选择更高性能的比如 Kafka 或者其他的分布式消息队列。
第二个问题——用户轮询最新消息,需要进行 redis 的 zRangeByScore 操作,redis slave 的性能瓶颈较大。
解决这个问题可以额外增加一层缓存。后端每隔 1 秒左右取拉取一次直播间的弹幕,前端轮询数据时,从该缓存读取数据。弹幕的返回条数根据直播间的大小自动调整,小直播间返回允许时间跨度大一些的弹幕,大直播间则对时间跨度以及弹幕条数做更严格的限制。这里缓存与平常使用的本地缓存问题,有一个最大区别:成本问题。如果所有直播间的弹幕都进行缓存,假设同时有 1000 个直播间,每个直播间有5种弹幕类型,缓存每隔 1 秒拉取一次数据,40 台缓存处理机器,那么对 redis 的访问 QPS 是 1000 * 5 * 40 = 20W。成本太高,因此我们只有大直播间才自动开启缓存,小直播间不开启。
第三个问题——弹幕数据也支持回放,直播结束后,这些数据存放于 redis 中,在回放时,会与直播的数据竞争 redis 的 CPU 资源。
解决方案——直播结束后,数据备份到 MySQL;增加一组回放的 redis;增加回放的缓存。回放时,读取数据顺序是: 缓存 –> redis –> MySQL。缓存与回放 redis 都可以只存某个直播间某种弹幕类型的部分数据,有效控制容量;缓存与回放 redis 使用 sorted set数据结构,这样整个系统的数据结构都保持一致。
我个人能力有限,暂时只能想到这么多。除了上面这些之外,还需要考虑整个系统的高可用保障——包括机房部署、降级和熔断、全面的业务监控、轮询方案的优化等等。
暂时就这些,我的知识面还需要不断完善,业务场景还需要不断扩充。
]]>有个小孩正在上楼梯,楼梯有s阶台阶,小孩一次可以上1阶或2阶。实现一个算法,计算小孩有多少种上楼梯的方式。输入n,返回一个整数。
稍微想了一下,这个题目可以用递归来解决。根据题目的规则,最后一步只有两种走法:要么走1阶,要么走2阶。所以可以得出这么一个递归公式:f(s) = f(s-1) + f(s-2)
。好像就是一个斐波那契数列公式?
这个问题的边界值就是在只有1阶的情况下,只有1种方式(1);在只有2阶的情况下,有两种方式(1,1)、(2)。
编写程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
使用正常的递归,在递归层数过多的情况下,可能会有stack overflow的风险,所以可以改写为用循环处理的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
将循环中使用到的变量提取出来作为函数的参数使用,就成了尾递归。在方法最后被调用时,线程栈里面的临时变量与参数此时已经没任何用了,可以被GC回收,所以理论上就是同上面的循环方法是一致的,无论有多深,都不会发生内存异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这种情况下,该算法的时间复杂度是O(n)
。
下面扩展一下思路:
这小屁孩的老师作业留少了,闲着没事爬楼梯,楼梯有s阶台阶(steps),小孩一次可以上m阶(maxStep)。计算小孩有多少种上楼梯的方式。输入s,m,返回一个整数。
一样分析可以得到公式:f(s) = f(s-1) + f(s-2) + ... + f(s-m)
。
参照上例思路,我们可以在递归里分为两大部分,一部分是steps > maxStep时,参照f(s,m) = f(s-1) + f(s-2) + f(s-3) + … + f(s-m)进行累加。
我们现在看steps <= maxStep时,怎么给出类似上例里f(2),f(3)的返回值。其实,上例中的f(3),就是台阶一共3级,最大可以跨2步的值,即f(2) 就是f(2,2),f(3)就是f(3,2)。分析一下就得出下面的图解:
根据以上图解,分析如下:
1 2 3 4 |
|
编码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
打完收工。
]]>没关系,我一样写得很溜。
最近又开始接触Go了,应该是很早就已经开始接触Go了,看了一些教程之后,我真的想说:真的很棒!
我现在只想尽快成为一个优秀的Go工程师。
]]>做过这么多同类型的需求之后,我查阅了一些资料,总结了一下在设计一个秒杀系统中,可能会用到的一些知识点。
这个思维导图我还会不断完善,不断迭代。
无规矩不成方圆,方法论就是规矩,动手实操才能成方圆。
这就是知识带来的力量。
]]>所以今天就来解决这个问题。
你需要通过 brew 安装 imap-uw
1
|
|
注:在完成第一步之后,你需要安装 openssl,不管通过何种方式。这里简单介绍一下通过 brew 安装openssl,非常的简单。
1
|
|
这就是安装拓展的准备工作,接下来才开始:
你需要去官网下载跟你现在环境所对应的PHP版本,我的是 7.1 的,所以我下载了 PHP 7.1 的,然后解压。
1
|
|
解压之后进入到当前 PHP 文件夹的 ext/imap 文件夹当中。注意,解压是解压到当前目录下,当然,你也可以指定目录。
1
|
|
进入到该目录后,第一个命令就是:
1
|
|
如果你不执行该命令,是无法在这个目录下进行编译安装的。接下来就是编译安装了。
1
|
|
注:imap 的目录就是 imap-uw 的安装目录,凡是通过 brew 安装的都是在 /usr/local/Cellar 目录下,ssl 的目录就是你之前的 openssl 的目录。这些都要改为自己的。然后再是执行之前的编译。
1
|
|
执行完成之后,会在 imap 目录下生成许多的文件,此时需要创建一个文件夹。
1
|
|
这个文件夹用来存放刚才执行编译生成的 imap.so 文件。当前你所在的目录还是在 imap 里面,需要把 imap.so 文件拷贝到刚才创建的文件夹中。
1
|
|
最后你需要在你的 /usr/local/etc/php/7.1/conf.d/imap.ini 里面添加这样一句话即可:
1 2 |
|
检查是否安装好:
1
|
|
如果出现了 imap,那么大功告成。
]]>