0%

Java 并发解决方案

bing

概念

吞吐量

吞吐量是指系统在单位时间内处理请求的数量

QPS

每秒查询率

响应时间

响应时间是指系统对请求作出响应的时间

并发数

并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量

为什么需要高并发

  1. 业务需求,比如秒杀购物;
  2. 性能需求;
  3. 可简化任务调度,任务调度是指基于给定时间点,给定时间间隔或给定执行次数自动执行任务;
  4. 并行程序在多核CPU有优势:

高并发带来的问题

事务隔离级别带来的并发问题

  1. 脏读
    事务T1修改一个数据,T2读取该数据后事务T1回滚,T2读取到的就是脏数据

    pic

  2. 不可重复读
    事务T1和T2读取一个数据,事务T1修改数据后事务T2再次读取该数据,导致两次读取不一致

    pic

  3. 幻读
    事务T1读取数据,事务T2修改数据后事务T1再次读取该数据,导致前后读取不一致

    pic

事务的隔离级别(√表示存在问题,×表示不存在问题)

隔离级别 脏读 不可重复读 幻读
read uncommitted
read committed (oracle) ×
repeatable read (mysql) × ×
serializable × × ×

高并发问题解决方案

悲观锁

在数据处理过程中,数据处于锁定状态。悲观锁的实现往往需要依靠数据库提供的锁机制。

mysql 中提供了行级锁和表级锁两种封锁粒度。锁定的数据越少并发性越高,但是锁操作需要消耗资源,封锁粒度越小开销越大。

mysql 中有读写锁和意向锁。当使用读写锁时,事务T想对表A加表级锁,就需要检查是否有其他事务对表A加了表级锁或对表A的某行加了行级锁,那么就需要对表A的每一行都检查,这是非常耗时的。使用意向锁可以更容易地支持多粒度加锁。

意向锁在原来的排它锁和共享锁之上引入了意向排它锁和意向共享锁,意向排它锁和意向共享锁都是表锁,用来表示一个事务想要在表中的某个数据行上加排它锁或共享锁。有以下两个规定:

  • 一个事务对数据对象 A 加了排它锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
  • 一个事务对数据对象 A 加了共享锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加共享锁,但是不能加排它锁。
排它锁 意向排它锁 共享锁 意向共享锁
排它锁 × × × ×
意向排它锁 × ×
共享锁 × ×
意向共享锁 ×
1
SELECT ... LOCK IN SHARE MODE;

上述方式对行加共享锁,对表加意向共享锁

1
SELECT ... FOR UPDATE

上述方式对行加排它锁,对表加意向排它锁

乐观锁

乐观锁,大多是基于数据版本 Version 记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时对此版本号加1。此时将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号则予以更新,否则认为是过期数据。

如果需要非常高的访问速度,建议使用乐观锁;如果冲突严重,建议使用悲观锁;如果重试的代价比较大,建议使用悲观锁

CAS

Compare And Swap,是一种实现并发算法时常用到的技术

CAS 有3个操作数:

  • 内存地址V
  • 旧的预期值A
  • 新的目标值B

只有当地址指向的值等于旧值时,才进行更新,否则说明其他线程对改值进行了更改

缺点:

  • 循环时间长开销很大
  • 只能保证一个共享变量的原子操作
  • ABA问题

面试必问的CAS,你懂了吗?

Java 分段锁

消息队列

在高并发环境下,可以使用消息队列异步处理请求,从而缓解系统的压力。

  1. 解耦

    o_jieou6.png

  2. 异步
    o_yibu3.png

  3. 削峰

    o_xuefeng2.png

数据库缓存

缓存数据是为了让客户端很少甚至不访问数据库服务器进行数据的查询,在高并发下能最大程度的降低对数据库服务器的访问压力。

CDN加速

CDN的全称是Content Delivery Network,即内容分发网络,尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络。CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

负载均衡

负载均衡服务器使用负载均衡算法将请求转发到集群中不同的主服务器。

Redis缓存

  1. 单线程,利用redis队列技术并将访问变为串行访问,消除了传统数据库串行控制的开销
  2. redis具有快速和持久化的特征,速度快,因为数据存在内存中
  3. 分布式 读写分离模式
  4. 支持丰富数据类型
  5. 支持事务,操作都是原子性,所谓原子性就是对数据的更改要么全部执行,要不全部不执行
  6. 可用于缓存,消息,按key设置过期时间,过期后自动删除

分布式锁

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
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

public class RedisTool {

// 加锁
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX"; // 保证互斥性
private static final String SET_WITH_EXPIRE_TIME = "PX"; // 保证不会发生死锁

// 解锁
private static final Long RELEASE_SUCCESS = 1L;

/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁,唯一key
* @param requestId 请求标识,保证解铃还须系铃人
* @param expireTime 超期时间,保证不会发生死锁
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}

/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); // jedis.eval() 保证执行的原子性
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}

限流

  • 计数器
    • 使用计数器限制一秒内能够通过的请求数,到达阈值后拒绝其他请求,一秒结束后再重新计数
    • 缺点:如果请求集中在前几毫秒,之后将无法响应请求,这种现象称为“突刺现象”
  • 漏桶算法
    • 类比:相当于睡倒入漏斗,不管倒入多少水,下边流出的速度始终保持不变
    • 可以维护一个消息队列,将请求放入消息队列,然后通过一个线程池定期的从消息队列中读取请求。当桶达到最大容量时,拒绝请求
    • 缺点:无法处理突发流量并响应时间要求比较短的场景
  • 令牌桶算法
    • 令牌桶能限制平均请求速度的同时允许一定程度的突发请求
    • 使用一个桶存放固定数量的令牌,算法以一定的速率向桶中存放令牌,存放令牌持续不断的进行,当桶满时丢弃令牌。每次请求需要先获取令牌,拿到令牌才能执行,否则等待或拒绝请求
    • 令牌桶算法运行一定程度的突发请求,比如桶初始化时允许存放100个令牌,那么这时运行100个请求同时调用

限流不仅可以限制请求数,还可以限制单个用户的上传下载的速度。

服务降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也可以随机服务。

参考文献

  1. 数据库系统原理
  2. Java高并发,如何解决,什么方式解决
  3. 别吵吵,分布式锁也是锁
  4. Redis分布式锁的正确实现方式