集合框架
jdk-1.8.0_211
List
允许重复元素(包括 null)、支持顺序存储的集合
ArrayList
基于数组实现、可自动扩容、非线程安全的 List
原理
- 主要属性
transient Object[] elementData;元素数组private int size;元素数量
- 自动扩容
- 每次添加元素时,判断
elementData长度是否足够
ensureCapacityInternal(size + 1); - 若长度不够,则执行扩容
if (minCapacity - elementData.length > 0) grow(minCapacity); - 非临界情况下,新数组的容量为原数组容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1); - 复制原数组中的元素,并替换原数组
elementData = Arrays.copyOf(elementData, newCapacity);
- 每次添加元素时,判断
优点
- 实现了
RandomAccess接口,即支持快速随机访问 (for循环比Iterator快) - 不考虑扩容的情况下,添加元素效率非常高(非插入元素)
缺点
- 保存元素的数组 (
elementData) 可能会占用额外的空闲空间 - 扩容时,需要全量复制,所以对于所需容量可预测的场景,最好初始化时指定容量大小
- 指定位置插入/删除元素时,需要整体移动元素 (向后/向前),效率较低
适用场景
- 随机访问较多的场景
- 所需容量可预测的场景
- 不需要或很少在指定位置插入/删除元素的场景
LinkedList
基于双向链表实现、非线程安全的 List
原理
- 主要属性
transient int size = 0;元素数量transient Node<E> first;头节点transient Node<E> last;尾节点
- 查找指定位置的元素
如果索引小于 size / 2,从头节点开始遍历,否则从尾节点开始遍历
优点
- 无需扩容
- 实现了
Deque接口,可当作队列、栈来使用- 队列:
offer- 入队,poll- 出队 - 栈:
push- 入栈,pop- 出栈
- 队列:
- 指定位置插入/删除元素时,只需要修改节点指针即可,无需移动元素
缺点
- 不支持快速随机访问
- 每个节点都需要浪费一定的空间来存放前后节点的指针
适用场景
- 随机访问较少的场景
- 频繁插入/删除的场景
- 需要使用队列、栈的场景
CopyOnWriteArrayList
基于数组 + 读写分离思想实现、线程安全的 List
原理
- 主要属性
final transient ReentrantLock lock = new ReentrantLock();写锁private transient volatile Object[] array;元素数组
- 写入时复制
- 添加、修改、删除元素时,先获取写锁
- 将元素数组复制成新的数组,在新的数组上执行操作
- 用新数组替换原数组
- 释放锁
优点
- 实现了
RandomAccess接口,即支持快速随机访问 (for循环比Iterator快) - 支持了线程安全的情况下,保证了读的效率
缺点
- 无法保证读取的数据一定是最新的(弱一致性)
- 每次添加、修改、删除元素时,都需要全量拷贝数组(写性能低)
适用场景
- 需要保证线程安全,且对数据一致性要求不是很高的场景
- 多读少写的场景
Set
不允许重复元素的集合
Java 中的
Set一般都是基于对应的Map实现,如HashSet是基于HashMap实现,通过将元素作为Map的key来实现元素不重复。并且可以通过Collections.newSetFromMap将一个Map转换为Set
HashSet
基于 HashMap 实现,内部维护一个 HashMap 对象
LinkedHashSet
保存了插入顺序的 HashSet,原理参考 LinkedHashMap
继承自 HashSet,初始化时调用特殊的构造函数
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
TreeSet
基于 TreeMap 实现,内部维护一个 NavigableMap 对象
CopyOnWriteArraySet
原理、优缺点和适用场景与 CopyOnWriteArrayList 相同,区别是,CopyOnWriteArraySet 不允许重复元素,在添加、插入元素时需要判断元素是否存在
Map
HashMap
基于数组 + 链表/红黑树实现,使用链地址法解决 hash 冲突、key、value 都允许为空、不保证顺序、非线程安全的键值对集合
原理
- 主要属性
transient Node<K,V>[] table;Hash 数组transient Set<Map.Entry<K,V>> entrySet;键值对缓存,方便keySet和values方法transient int size;键值对数量int threshold;扩容的阈值,一般等于capacity * loadFactorfinal float loadFactor;扩容的加载因子,默认0.75f
- hash 算法
- hash 扰动
static final int hash(Object key) { int h; // 低 16 位与高 16 异或,减少 hash 碰撞的概率 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } - 位置计算
i = (n - 1) & hash高效取余操作,前提 `n` 是 `2` 的次幂 若 n = 16; hash = 21 n - 1 00001111 hash 00010101 & index 00000101 = 5
- hash 扰动
- 链表树化
- 键值对的数量小于
MIN_TREEIFY_CAPACITY(64)的情况下,会进行扩容,而不是树化 - 链表长度不小于
TREEIFY_THRESHOLD(8)时,树化为红黑树 - 红黑树节点数量不大于
UNTREEIFY_THRESHOLD(6)时,退化为链表
- 键值对的数量小于
- 自动扩容
- 键值对的数量大于
threshold时,执行扩容 - 创建新的 Hash 数组,非临界情况下,新数组的长度为原数组长度的两倍
newCap = oldCap << 1; - 将原数组中的键值对复制到新数组中,如果
(e.hash & oldCap) == 0则分配到低位(原位置),否则分配到高位(原位置+原容量)无需 rehash 的分配算法 若 oldCap = 16, newCap = 32, hash1 = 5, hash2 = 21,原位置都为 5 oldCap 00010000 | 0010000 hash1 00000101 & | 0010101 00000000 = 0 | 0010000 != 0 低位 5 | 高位 5 + 16 = 21
- 键值对的数量大于
优点
存取效率相对都较高,在不需要排序的情况下,都推荐使用
缺点
无重大缺陷
适用场景
大部分存储键值对的场景
LinkedHashMap
保存了插入顺序的 HashMap,继承自 HashMap,通过在节点中 before、after 两个指针保存插入顺序
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
TreeMap
基于红黑树实现,自动排序,非线程安全的 Map
原理
- 主要属性
private final Comparator<? super K> comparator;排序比较器,若为null,则按key自然排序private transient Entry<K,V> root;红黑树的根节点private transient int size = 0;键值对数量
优点
- 可指定元素排序方式(
Comparator) - 可按
key自然排序 (Comparable),此时key不允许为null
缺点
性能较 HashMap 差,在不需要排序的场景,建议使用 HashMap
适用场景
存储需要排序的键值对的场景
ConcurrentHashMap
原理
优点
缺点
适用场景
Queue
并发编程
网络编程
JVM
MySQL
常用框架
Spring
Mybatis
Netty
中间件
Zookeeper
Redis
Kafka
高并发解决方案
分布式解决方案
系统架构
RPC 架构设计
RPC 框架示意图
RPC 主要角色
- 服务消费者:远程方法的调用方,即客户端,一个服务既可以是消费者也可以时提供者
- 服务提供者:远程方法的提供方,即服务端,一个服务既可以是消费者也可以时提供者
- 注册中心:保存服务提供者的服务地址等信息
- 监控运维(可选) :监控接口的响应时间、统计请求数量等,及时发现系统问题并发出告警通知
RPC 调用过程:
- 客户端处理过程中调用 Client Stub (本地方法),传递参数
- Client Stub 将类信息、方法信息、参数序列化为消息,通过 Socket 发送给服务端
- 服务端收到数据包后,Server Stub 将消息反序列化为类信息、方法信息、参数
- Server Stub 调用对应的本地方法,并将执行结果返回给客户端
注册中心
目前较成熟的注册中心有 Zookeeper,Nacos,Consul,Eureka 等,其主要比较如下:
| Zookeeper | Nacos | Consul | Eureka | |
|---|---|---|---|---|
| 一致性协议 | CP | CP + AP | CP | AP |
| 雪崩保护 | 无 | 有 | 无 | 有 |
| 多数据中心 | 不支持 | 支持 | 支持 | 支持 |
| 自动注销实例 | 支持 | 支持 | 支持 | 支持 |
负载均衡算法
随机、轮询、hash 和一致性 hash
通信框架
Netty 是一个高性能事件驱动型的非阻塞的 IO(NIO) 框架,非常适合作为 RPC 框架的通信框架
通信协议
TPC 通信过程中,会根据 TCP 缓存区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。所以需要对发送的数据包封装到一种通信协议里
业界的主流协议的解决方案可以归纳如下
- 消息定长,例如每个报文的大小固定为固定长度 100 字节,如果不够用空格补足
- 在包尾以特殊结束符分割
- 将消息分为消息头和消息体,消息头中包含消息总长度(或者消息体长度)的字段
很明显,1、2 两种方法都有些局限性,一般采用方案 3,具体协议设计如下
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| Byte | | | | | | | ... |
+---------------------------------------------------------------------------------------------------
| magic | version| type | content length | content byte[] |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
- 第一个字节是魔法数,比如定义为 0X35
- 第二个字节代表协议版本号,以便对协议进行扩展,使用不同的协议解析器
- 第三个字节是请求类型,如 0 代表请求,1代表响应
- 第四到七个字节表示消息长度,即此四个字节后面该长度的内容是消息体
序列化协议
4 种序列化协议,JavaSerializer、Protobuf、 Hessian 和 Kryo 优缺点比较
| 优点 | 缺点 | |
|---|---|---|
| JavaSerializer | 使用方便,可序列化所有类 | 速度慢,体积大 |
| Protobuf | 速度快 | 需静态编译 |
| Hessian | 默认支持跨语言 | 较慢 |
| Kryo | 速度快,序列化后体积小 | 跨语言支持较复杂 |
秒杀系统
需求分析
- 定时开始
- 限量
- 限购
流程图
flowchart LR
U(用户);
Q1{是否开始?};Q2{已抢完?};Q3{库存锁定\n成功?};Q4{按时支付?}
P1[商品购买页];P2[点击购买按钮];P3[创建订单];P4[支付倒计时];P5[释放库存];P6[扣减库存];
F1{{展示倒计时\n抢购按钮置灰}};F2{{秒杀结束}};F3{{下单失败}};F4{{购买失败}};F5{{购买成功}};
U --访问--> P1 --> Q1 --> |是| Q2 --> |是| P2 --> P3 --> Q3 --> |是| P4 --> Q4 --> |是| P6 --> F5
Q1 --> |否| F1
Q2 --> |否| F2
Q3 --> |否| F3
Q4 --> |否| P5 --> F4
系统要求
- 瞬时大流量高并发 - 服务器、数据库等能承载的 QPS 有限,如数据库一般是单机 1000QPS,需要根据业务预估并发量
- 有限库存,不能超卖、少卖 - 库存是有限的,需要精确的保证不超卖,不少卖
- 黄牛恶意请求 - 使用脚本模拟用户购买,模拟十几万个请求去抢购
- 固定时间开启 - 时间到了才能购买
- 严格限购 - 一个用户只能购买一件
单体架构设计
|-----------------|
| |-----------| |
| | 秒杀模块 | |
| |-----------| |
| |
| |-----------| |
| | 商品信息和 | |
| | 库存模块 | |
网关 | |-----------| | |---------|
Gateway --------- | | ----------- | 数据库 |
| |-----------| | |---------|
| | 订单模块 | |
| |-----------| |
| |
| |-----------| |
| | 支付模块 | |
| |-----------| |
|-----------------|
- 前后端耦合,服务压力较大 -- 前后端分离
- 各功能模块耦合严重
- 系统复杂,一个模块的升级需要整个服务都升级
- 扩展性差,难以针对某个模块单独扩展
- 开发协作困难,各个部门的人都使用同一个代码块
- 级联故障,一个模块的故障可能会导致整个系统不可用
- 只能使用同一种技术和语言
- 数据库崩溃导致整个服务崩溃
微服务架构设计
|-----------| |------------|
|----- | 秒杀服务 | ----------- | 秒杀数据库 |
| |-----------| |------------|
|
| |-----------| |-----------|
|----- | 商品信息和 | | 商品与库存 |
| | 库存服务 | ----------- | 数据库 |
网关 | |-----------| |-----------|
Gateway ---|
| |-----------| |------------|
|----- | 订单服务 | ----------- | 订单数据库 |
| |-----------| |------------|
|
| |-----------| |------------|
|----- | 支付服务 | ----------- | 支付数据库 |
|-----------| |------------|
表结构设计
商品信息表(t_commodity) 商品库存表(t_stock)
|--------------------------------------------| |----------------------------|
| 商品 ID | 商品名称 | 商品描述 | 商品价格 | | 库存 ID | 商品 ID | 库存 |
| id | name | desc | price | | id | comm_id | stock |
| 189 | IPhone 11 | xxxxxxxx | 5999 | | 1 | 189 | 10000 |
|--------------------------------------------| |----------------------------|
订单表(t_order) 秒杀表(t_sekill)
|-----------------------------------| |--------------------------------------------------------|
| 订单 ID | 商品 ID | 用户 ID | | 秒杀 ID | 秒杀名称 | 商品 ID | 秒杀价格 | 秒杀数量 |
| id | comm_id | customer_id | | id | name | comm_id | price | stock |
| 189 | 189 | 9 | | 28 | 618秒杀活动 | 189 | 4000 | 100 |
|-----------------------------------| |--------------------------------------------------------|
库存扣减时,使用 TRANSACTION + FOR UPDATE 保证原子性
START TRANSACTION;
SELECT stock FROM `t_stock` WHERE `comm_id` = 189 FOR UPDATE;
UPDATE `t_stock` SET stock = stock - 1 WHERE `comm_id` = 189;
COMMIT;
架构优化
前端优化
- 使用前后端分离架构,前后端部署到不同的服务器,分担服务器压力。必要的情况下,可以使用 CDN 缓存静态资源
- 用户点击购买后,短时间内按钮置灰或多次点击后,弹出验证码,防止恶意刷单
后端优化
-
网关拦截恶意 IP 地址和用户 ID
- 黑名单
- 布隆过滤器
-
使用 Nginx 做负载均衡分散瞬时压力
- 加权随机
- 加权轮询
- 最小连接数
- 一致性哈希
-
使用 Redis 缓存分担数据库的压力
SET sekill_stock:sekill_id:comm_id 500 // 缓存预热,这里的数量可以稍大于真实数量 // 可能需要考虑少卖的问题 DECR seckill_stock:28:189 // 扣减库存,这里并不是真的扣减库存,最终是否扣减,需要从数据库来判断 // 当返回值小于 0 时,表示没有库存,此时不需要再请求数据库 // 或者使用 lua 脚本,先查询再减库存,保证原子性 // 限购方案 SADD sekill_customer:sekill_id:comm_id SISMEMBER sekill_customer:sekill_id:comm_id customer_id -
Redis 性能依然有限
- Redis 库存小于 0 时,后续请求直接拒绝
- 服务限流,计数器、漏桶、令牌桶、消息队列
- 服务降级,跳转繁忙页
-
商品信息等短时间内变化不大的信息也可以缓存到 Redis
-
考虑缓存雪崩、缓存穿透、缓存击穿等问题
-
使用分布式事务保证数据一致性
- 三阶段提交
- TCC
- Seata
-
使用服务熔断防止服务雪崩
- Netflix Hystrix
- Alibaba Sentinel