回目录 《concurrency并发》

# concurrency并发 VS Parallelism并行

首先要明确个词的意思,首先引用:

Concurrency means multiple tasks which start, run, and complete in overlapping time periods, in no specific order. Parallelism is when multiple tasks OR several part of a unique task literally run at the same time, e.g. on a multi-core processor. Remember that Concurrency and parallelism are NOT the same thing. https://howtodoinjava.com/java/multi-threading/concurrency-vs-parallelism/

"concurrent" is used only for events that occur over a period of time, whereas "simultaneous" can also be used for events that occur at a point in time.

可以看到,cocurrency并发是只在一段时间内很多events一起发生,但是在这段时间内的任意一个时刻可能只有一个event发生,而parallelism是指events发生是simultaneously,某个时刻他们是可以同时发生;

以吃饭和说话举例: 有些人吃饭的时候不喜欢说话,只有吃完饭才说话,所以不支持并发concurrency和并行parallelism; 有些人吃饭的时候说话比较优雅礼貌,每次都是先吞下嘴里的食物才说话,然后说了一句再吃一口,所以他支持并发concurrency但不支持并行parallelism; 有些人吃饭不讲究,嘴里还没咽下去,同时还高声阔谈,食物有时候喷的对方一脸,所以他同时支持并发concurrency和并行parallelism;

所以并发只是强调可以在一段时间内同时处理多个事务,并行是强调可以在某一个时刻处理多个事务,并发可以不并行,并行一定是并发;

比如早期一个核的cpu也可以处理多个任务就是属于并发但是不是并行,在任何一个cpu时刻,只能处理一件任务,而多核则同一个时刻,多个cpu并行处理不同任务; 关于cpu这个层面的概念可以参考被神话的Linux, 一文带你看清Linux在多核可扩展性设计上的不足 (opens new window)

所以我们并太关心并行,因为这个涉及到硬件和底层操作系统的处理,我们现在只关心并发的处理;

一般谈到并发我们基本都是暗指高并发,实际上也有低并发的情况需要处理,比如低并发但是每个任务都是需要消耗长处理时间,下面也会提到;

concurrent control或者并发控制是关乎系统的consistency一致性, 最直接的想法是先来后到,但是先来后到也有问题,先来的可能做了一系列的读写操作,后到的就要一直等着吗,显然这种处理很粗暴, 所以对于单机系统来说,比如数据库,一般都是通过隔离水平isolation level来控制不同的粒度; 对于分布式系统来说,不同的产品的promise和guarantee也是不同的,比如zookeeper通过ZAP协议保证顺序一致性sequential consistency,但是zookeeper并不保证每个客户端看到的东西是一致的, 这个可以看我的zookeeper讲解,再比如kafka提供了exactly once的语义,意思是不会重复或丢失消息,但是也是取决于client的实现,实际项目中因为会涉及到跟其他产品比如数据库交互, 其实从业务角度或者项目角度是难以实现exactly once的,所以要分清产品本身(server端和client端)能做到什么,以及结合到实际项目中又是会如何;

另外关于并发的一个误区:handle并发并不一定需要多线程,比如nodejs,redis都是单线程处理的,原理就算通过event loop,再比如Disruptor框架是通过ringbuffer

# 1. 数据库Database Isolation

Isolation (database systems) https://en.wikipedia.org/wiki/Isolation_(database_systems)#Read_committed

幻读

解决办法:整个区间加锁

不可重复读

解决办法: 记录级别加锁 select for update

脏读取

解决办法:commit之后才生效

Common scenarios

  1. no matter what the db level lock settings, it’s better to add extra lock when doing transaction update

  1. ‘duplicate’ issue

这里只举例一个用户手快点了多次的情况(真实发生过),解决方法是不要在function或者存储过程中生成id,而是从外部传入; 不过如果发生另外一个极端情况,不同用户同一时刻注册相同用户名,则需要另想办法,因为这样的话id肯定不同,username又不是主键不会冲突, 所以要么从username生成id,比如hash运算,要么去掉username的概念,只允许手机号或者邮箱注册;

# 2. 应用层面的并发

从单个应用来讲,有几种方式:

a.采用多线程,多开几个线程或线程池,充分利用cpu和内存,尤其是当遇到比较复杂的计算时,单个线程处理时间过长会阻塞影响性能,所以可以用java等forkjoinpool之类的方式处理;

b.采用多进程,多个应用协同,比如前面加个load balance反向代理分流比如ha proxy (opens new window) 或者nginx,比如淘宝CDN采用ha proxy, 当然如果应用之间需要业务层面的协同,比如用户session管理,几个应用之间可以采用share session之类的其他工具或框架;

c.采用队列,比如消息队列

实际上第一种方式基本都不是为了处理高并发,现在很少有给一个单体应用加cpu内存的这种处理方式,如上面举例,一般都是用来处理long processing time的问题(低并发但是处理时间较长的任务), 然后本质上b和c的原理差不多,只是观察的角度和粒度不同,实际ha proxy和nginx这些内部也是可能采用队列思想,这个我没有深入探索只是粗略知道;

最后换个角度从整体架构上看,第三种方式比较有优势,比如采用FIFO排队方式来处理高并发, 额外的好处:解耦了消息的生产者和消费者,生产者负责把消息放到队列尾部,消费者则从队列头拉取消息进行处理, 由于解耦了生产者和消费者,互相就不需要等待对方处理,所以是异步操作,生产者不需要等待消费者处理某条消息的结果;

如果有同步需求怎么办,比如有些场景生产者还是要等待消费者处理结果才能进入下一步的, 这种完全可以拆解成消费者处理完之后将处理之后的结果放入另一个队列,然后生产者再作为消费者去消费这个队列即可, 然后可以对刚才这个过程做一个封装,将第二个队列的生产消费做成回调,就可以做成同步请求,比如类似:

producer1.sendTransaction({from: '0x123...', data: '0x432...'})
.on('confirmation', function(confNumber, receipt){ //回调 })
.on('error', function(error){ ... })
.then(function(receipt){
});

架构也是建立在一个个等应用之上的,所以落实到对应架构中要直面高并发情况的某个应用程序,如何去实现这个队列呢?

java sdk默认提供了非线程安全的队列和线程安全的队列,实际项目多涉及到多线程,所以我们只说后者,线程安全队列又分为两类队列:

  • 无界的队列 不用锁的队列:LinkedTransferQueue,ConcurrentLinkedQueue,由于无界,所以有内存溢出的风险

  • 有界队列 加锁的队列: ArrayBlockingQueue,LinkedBlockingQueue,但是有锁就有阻塞,所以性能会比较低

但是,

# 多线程不代表高性能!

多线程有读写问题,需要加锁,则触发内核态,从而降低性能

要处理高并发,肯定要考虑性能,有没有性能高即无锁non-blocking并且有界的队列呢,LMAX开发的Disruptor就是这么一个无锁高性能有界循环队列,

“It ensures that any data is owned by only one thread for write access, therefore reducing write contention compared to other structures.”

现在Disruptor已经成为很多交易所的基础框架一部分,性能对比参考 (opens new window) 你应该知道的高性能无锁队列Disruptor (opens new window) 可以看到越来越多的框架集成了disruptor队列,比如log4j,storm,solr https://mvnrepository.com/artifact/com.lmax/disruptor/3.2.1/usages https://mvnrepository.com/artifact/com.lmax/disruptor/3.4.0/usages

NOTES: 虽然Disruptor的ring buffer队列可以处理高并发,但是有时候业务上对消息队列有更复杂的要求,比如可以pub sub,可以存储当做db,如果下游挂掉可以重新恢复到之前的位置重跑等等, 所以我们有kafka消息队列, kafka消息队列底层的基于sequential consistency的zab协议一定程度上保证了可以实现‘exactly-once’的语义: 消息生产者producer可以保证幂等性(kafka系统内的重发不会造成下游收到多条重复数据,当然也不会丢失), 消息消费者consumer可以通过自主管理offset和使用事务提交offset以及下游写入kafka的消息,可以保证不重复消费也不会丢失;

# IO的高并发发展

参考 BIO/NIO/多路复用/NETTY 100万级连接,爱奇艺WebSocket网关如何架构 (opens new window)

# 3.系统和框架层面的并发限制

比如linux句柄数 执行ulimit –n检查文件句柄数为1024,将该数值改为10240 /etc/security/limits.con

从框架层面可以参考我在network一文中的一个案例### 4.2 一次排查send-q

CopyRight 刘跃 LYHISTORY.COM


ref: