一、高并发涉及到的知识点
线程安全,线程封闭,线程调度,同步容器,并发容器,AQS,J.U.C。
二、同步与异步
所谓同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其它的命令。
异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
同步在一定程度上可以看做是单线程,这个线程请求一个方法后就待这个方法给他回复,否则他不往下执行。
异步在一定程度上可以看做是多线程的,请求一个方法后,就不管了,继续执行其他的方法。
2.1举例
同步:吃饭和说话,只能一件事一件事的来,因为只有一张嘴。
异步:但吃饭和听音乐是异步的,因为,听音乐并不引响我们吃饭
2.2脏数据
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这
个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(Dirty Data),依据脏数据所做的操作可能是不正确的。
2.3不可重复读
不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
三、如何处理高并发问题
3.1硬件上@b@ @b@扩容:水平扩容、垂直扩容@b@ @b@ @b@3.2中间件上@b@ @b@缓存:Redis、Memcache、GuavaCache等@b@ @b@队列:Kafka、RabitMQ、RocketMQ等@b@ @b@ @b@3.3分布式拆分@b@ @b@应用拆分:服务化Dubbo与微服务Spring Cloud@b@ @b@限流:Guava RateLimiter使用、常用限流算法、自己实现分布式限流等@b@ @b@服务降级与服务熔断:服务降级的多重选择、Hystrix@b@ @b@ @b@3.4数据库层面@b@ @b@数据库切库,分库分表:切库、分表、多数据源@b@ @b@高可用的一些手段:任务调度分布式elastic-job、主备curator的实现、监控报警机制@b@ @b@ @b@3.5使用微服务@b@ @b@使用微服务拆分,划分模块、网关,熔断、限流、降级
四、并发的优势与风险
4.1并发的优势
(1)速度上可以同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行。@b@(2)设计上程序设计在某些情况下更简单,也可以更多的选择。@b@(3)资源利用上CPU能够在等待IO的时候做一些其他的事情。
4.2并发的风险
(1)安全性:多个线程共享数据时可能会产生于期望不相符的结果。@b@ @b@(2)活跃性:某个操作无法继续进行下去时,就会发生活跃性问题。比如死锁、饥饿等问题。@b@ @b@(3)性能:线程过多时会使得CPU频繁切换,调度时间增多;同步机制;消耗过多内存。
4.3如何处理并发和同步
4.3.1Java代码层面
java中的同步锁,典型的就是同步关键字synchronized。还有一个Atomic包,里面是基于乐观锁版本号CAS自旋实现并发控制。
4.3.2数据库层面
悲观锁(Pessimistic Locking):
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自 外部系统的事务处理)修改持保守态度,因此,
在整个数据处理过程中,将数据处于锁定状态。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统
中实现了加锁机制,也无法保证外部系 统不会修改数据)。
一个典型的倚赖数据库的悲观锁调用:
select * from account where name=”javagongfu” for update
这条 sql 语句锁定了 account 表中所有符合检索条件( name=”javagongfu” )的记录。
本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
乐观锁:
大多是基于数据版本 Version )记录机制实现。
何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并 从其帐户余额中扣除 $20 ( $100-$20 )。 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣 除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大 于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数 据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的 数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记 录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于version=1 的旧数据修改的结果覆盖操作 员 A 的操作结果的可能。 从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局 限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。
五、常见并发问题列举
案例一:订票系统案例,某航班只有一张机票,假定有1w个人打开你的网站来订票,问你如何解决并发问题。
问题描述:1w个人同时点击购买,到底谁能成交?总共只有一张票。
解决方案:
锁同步同步更多指的是应用程序的层面,多个线程进来,只能一个一个的访问,java中指的是syncrinized关键字。锁也有2个层面,一个是java中谈到的对象锁,用于线程同步;另外一个层面是数据库的锁;如果是分布式的系统,显然只能利用数据库端的锁来实现。
假定我们采用了同步机制或者数据库物理锁机制,如何保证1w个人还能同时看到有票,显然会牺牲性能,在高并发网站中是不可取的。
采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样即保证数据的并发可读性又保证保存数据的排他性,保证性能的同时解决了并发带来的脏数据问题。
在现有表当中增加一个冗余字段,version版本号, long类型
原理:
1)只有当前版本号》=数据库表版本号,才能提交
2)提交成功后,版本号version ++
5.1解决此问题还需考虑的因素
(1)可以考虑增加缓存,分布式缓存,实现读写分离,采用Redis作为缓存端,基础数据、常用业务数据直接从缓存取。@b@ @b@(2)增加网络带宽,DNS域名解析分发多台服务器。@b@ @b@(3)负载均衡,配置前置代理服务器nginx、apache。@b@ @b@(4)数据库查询优化,读写分离,分表等等.@b@ @b@(5)优化数据库结构,多做索引,提高查询效率。
六、线程安全
基于数组结构:ArrayList -> 线程安全:Vector, Stack
Vector中的方法使用synchronized修饰过,线程安全@b@ @b@Stack继承Vector@b@ @b@非线程安全:HashMap -> 线程安全:HashTable(key、value不能为null)@b@ @b@HashTable使用synchronized修饰方法@b@ @b@Collections.synchronizedXXX(List、Set、Map)@b@ @b@ConcurrentHashMap、ConcurrentLinkedList都是线程安全的。
6.1并发容器
ArrayList -> CopyOnWriteArrayList:相比ArrayList,CopyOnWriteArrayList是线程安全的,写操作时复制,即当有新元素添加到CopyOnWriteArrayList时,先从原有的数组里拷贝一份出来,然后在新的数组上写操作,写完之后再将原来的数组指向新的数组,CopyOnWriteArrayList整个操作都是在锁(ReentrantLock锁)的保护下进行的,这么做主要是避免在多线程并发做add操作时复制出多个副本出来,把数据搞乱了。第一个缺点是做写操作时,需要拷贝数组,就会消耗内存,如果元素内容比较多会导致youngGC或者是fullGc;第二个缺点是不能用于实时读的场景,比如拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到的数据可能还是旧的,虽然CopyOnWriteArrayList能够做到最终的一致性,但是没法满足实时性要求,因此CopyOnWriteArrayList更适合读多写少的场景。
CopyOnWriteArrayList设计思想:1读写分离 2最终一致性 3使用时另外开辟空间解决并发冲突。@b@ @b@HashSet -> CopyOnWriteArraySet@b@ @b@TreeSet -> ConcurrentSkipListSet@b@ @b@CopyOnWriteArraySet:底层实现是CopyOnWriteArrayList。@b@ @b@ConcurrentSkipListSet:和TreeSet 一样支持自然排序,基于map集合,但是批量操作不是线程安全的。@b@ @b@HashMap -> ConcurrentHashMap :不允许空值,针对读操作做了大量的优化,具有特别高的并发性。@b@ @b@TreeMap -> ConcurrentSkipListMap :内部使用SkipList跳表结构实现的,key是有序的,支持更高的并发。
6.2AQS同步组件
1 CountDownLatch:闭锁,通过计数来保证线程是否需要一直阻塞@b@ @b@2 Semaphore:控制同一时间并发线程的数目@b@ @b@3 CyclicBarrier:和CountDownLatch相似,都能阻阻塞线程@b@ @b@4 ReentrantLock@b@ @b@5 Condition@b@ @b@6 FutureTask
七、使用线程池
new Thread弊端:
1 每次new Thread新建对象,性能差
2 线程缺乏统一的管理,肯无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM
3 缺少更多功能,如更多执行、定期执行、线程中断
线程池的好处:
1 重用存在的线程,减少对象创建、消亡的开销,性能佳
2 可有效控制最大并发的线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞
3 提供定时执行、定期执行、单线程、并发数控制等功能
7.1ThreadPoolExecutor
ThreadPoolExecutor参数:
1 corePoolSize:核心线程数@b@ @b@2 maximumPoolSize:最大线程数@b@ @b@3 workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响@b@ @b@如果当前系统运行的线程数量小于corePoolSize,直接新建线程执行处理任务,即使线程池中的其他线程是空闲的。如果当前系统运行的线程数量大于或等于corePoolSize,且小于maximumPoolSize,只有当workQueue满的时候才创建新的线程去处理任务,如果设置corePoolSize和maximumPoolSize相同的话,那么创建的线程池大小是固定的,这时如果有新任务提交,当workQueue没满时,把请求放进workQueue中,等待有空闲的线程从workQueue中取出任务去处理。如果运行的线程数量大于maximumPoolSize,这时如果workQueue满,根据拒绝策略去处理。@b@ @b@4 keepAliveTime:线程没有任务执行时最多保持多久的时间终止@b@ @b@5 unit:keepAliveTime的时间单位@b@ @b@6 threadFactory:线程工厂,用来创建线程@b@ @b@7 rejectHandler:当拒绝处理任务时的策略
线程池方法:
八、分布式缓存
8.1Redis
8.2memcache
8.2memcache
8.3缓存一致性
8.4缓存并发
九、应用拆分
采用MyCat分库分表,读写分离。
十、使用微服务
使用Spring Cloud Alibaba,网关组件,Sentinel隔离、熔断、降级、限流。
来源:头条 | 2021-08-13 21:18·心有花宇龙传人