第一种划分,能减少数据争用,编码实现也更简单,只需要识别有限的竞争,就能让系统工作的很好,缺点是任务的工作量很可能不同,有可能导致有些worker忙碌而另一些空闲。
第二种划分,优点是能均衡,缺点是编码复杂性高,数据竞争多。
有时候,我们会综合上述两种模式,比如让单独的线程去做IO(收发包)+反序列化(产生protocol task),然后启动一批worker线程去处理包,中间通过一个task queue去连接,这即是经典的生产者消费者模型。
协程是一种用户态的多执行流,它基于一个假设,即用户态的任务切换成本低于系统的线程切换。
通知替代轮询轮询即不停询问,就像你每隔几分钟去一趟宿管那里查看是否有信件,而通知是你告诉宿管阿姨,你有信的时候,她打电话通知你,显然轮询耗费CPU,而通知机制效率更高。
添加缓存缓存的理论依据是局部性原理。
一般系统的写入请求远少于读请求,针对写少读多的场景,很适合引入缓存集群。
在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求,因为缓存集群很容易做到高性能,所以,这样的话,通过缓存集群,就可以用更少的机器资源承载更高的并发。
缓存的命中率一般能做到很高,而且速度很快,处理能力也强(单机很容易做到几万并发),是理想的解决方案。
CDN本质上就是缓存,被用户大量访问的静态资源缓存在CDN中是目前的通用做法。
消息队列消息队列、消息中间件是用来做写请求异步化,我们把数据写入MessageQueue就认为写入完成,由MQ去缓慢的写入DB,它能起到削峰填谷的效果。
消息队列也是解耦的手段,它主要用来解决写的压力。
IO与逻辑分离、读写分离IO与逻辑分离,这个前面已经讲了。读写分离是一种数据库应对压力的惯用措施,当然,它也不仅限于DB。
批处理与数据预取批处理是一种思想,分很多种应用,比如多网络包的批处理,是指把收到的包攒到一起,然后一起过一遍流程,这样,一个函数被多次调用,或者一段代码重复执行多遍,这样i-cache的局部性就很好,另外,如果这个函数或者一段里要访问的数据被多次访问,d-cache的局部性也能改善,自然能提升性能,批处理能增加吞吐,但通常会增大延迟。
另一个批处理思想的应用是日志落盘,比如一条日志大概写几十个字节,我们可以把它缓存起来,攒够了一次写到磁盘,这样性能会更好,但这也带来数据丢失的风险,不过通常我们可以通过shm的方式规避这个风险。
指令预取是CPU自动完成的,数据预取是一个很有技巧性的工作,数据预取的依据是预取的数据将在接下来的操作中用到,它符合空间局部性原理,数据预取可以填充流水线,降低访存等待,但数据预取会侵害代码,且并不总如预期般有效。
哪怕你不增加预取代码,硬件预取器也有可能帮你做预取,另外gcc也有编译选项,开启它会在编译阶段自动插入预取代码,手动增加预取代码需要小心处理,时机的选择很重要,最后一定要基于测试数据,另外,即使预取表现很好,但代码修改也有可能导致效果衰减,而且预取语句执行本身也有开销,只有预取的收益大于预取的开销,且CACHE-MISS很高才是值得的。
2、算法优化数据量小的集合上遍历查找即可,但如果循环的次数过百,便需要考虑用更快的查找结构和算法替换蛮力遍历,哈希表,红黑树,二分查找很常用。
哈希(HASH)哈希也叫散列,是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值,也叫摘要。比如把一篇文章的内容通过散列生成64位的摘要,该过程不可逆。
这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值,但如果输出的位数足够,散列成相同输出的概率非常非常小。
字符串的比较有时会成为消耗较大的操作,虽然strcmp或者memcpy的实现用到了很多加速和优化技巧,但本质上它还是逐个比较的方式。
字符串比较的一个改进方案就是哈希,比较哈希值(通常是一个int64的整数)而非比较内容能快很多,但需要为字符串提前计算好哈希值,且需要额外的空间保存哈希值,另外,在哈希值相等的时候,还需要比较字符串,但因为冲突的概率极低,所以后续的字符串比较不会很多次。
这样不一定总是更高效,但它提供了另一个思路,你需要测试你的程序,再决定要不要这样做。
另一个哈希的用法是哈希表,哈希表的经典实现是提前开辟一些桶,通过哈希找到元素所在的桶(编号),如果冲突,再拉链解决冲突。