人人都是架构师 - 分布式系统架构落地与瓶颈突破
分布式系统应对高并发、大流量的常用手段:
- 扩容
- 动静分离
- 缓存
- 服务降级
- 限流
限流
常见算法:
- 令牌桶,
Nginx
限流模块用的是这个:限制的是流量的平均流入速率,允许一定程度上的突发流量。 - 漏桶:限制的是流出速率,并且这个速率还是保持不变的,不允许突发流量。
Nginx 限流
http {
# 每个 IP 的 session 空间大小
limit_zone one $binary_remote_addr 20m;
# 每个 IP 每秒允许发起的请求数
limit_req_zone $binary_remote_addr zone=req_one:20m rate=10r/s;
# 每个 IP 能够发起的并发连接数
limit_conn one 10;
# 缓存还没有来得及处理的请求
limit_req zone=req_one burst=100;
}
消峰
- 活动分时段
- 答题验证
高并发读
“马某出轨王某”、“iPhone SE 2020 发布” 等这种热点新闻的 key
会始终落在同一个缓存节点上,分布式缓存一定会出现单点瓶颈,其资源连接容易瞬间耗尽。有如下两种方案解决这个问题:
- 基于 Redis 的集群多写多读方案。
- 多写如何保持一致性:将
Key
配置在ZooKeeper
,客户端监听ZNode
,一旦变化,全量更新本地持有的Key
- 多写如何保持一致性:将
LocalCache
结合 Redis 集群的多级Cache
方案。LocalCache
拉取下来的商品数量有 5 个,但是实际上只有 4 个了,怎么解决?对于这种读场景,允许接受一定程度上的数据脏读,最终扣减库存的时候再提示商品已经售罄即可。
实时热点自动发现
交易系统产生的相关数据、上游系统中埋点上报的数据这两个,异步写入日志,对日志进行次数统计和热点分析
高并发写
InnoDB 行锁
乐观锁扣减:
SELECT stock, version FROM item WHERE item_id = 1;
UPDATE ITEM SET version = version + 1, stock = stock - 1 WHERE item_id = 1 AND version = version;
引入条件 “实际库存数 >= 扣减库存数”:
UPDATE item SET stock = stock - 1 WHERE item_id = 1 AND stock >= 1;
查询队列中等待拿锁的线程:
SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK_WAIT';
Redis
Redis 读写能力远胜任何类型的关心型数据库。使用 Redission
实现分布式锁,避免超卖:
RedissionClient redission = null;
try {
redission = Redission.create(config);
RLock lock = redission.getLock("testLock");
// lock(long leaseTime, TimeUnit unit)
// 某个线程没有获取到锁,那么这个线程只能在队列中阻塞等待,与 InnoDB 如出一辙
lock.lock(20, TimeUnit.MILLISECONDS);
lock.unlock();
// tryLock(long waitTime, long leaseTime, TimeUnit unit)
// 并发较大的情况下,建议使用这个
boolean result = lock.tryLock(10, 20, TimeUnit.MILLISECONDS);
if (result) {
lock.forceUnlock();
}
} finally {
if (null != redission) {
redission.shutdown();
}
}
扣除库存成功后的消息,通过消息队列写入到数据库中,由于才用了排队机制,并发写入数据库的流量可控,数据库负载压力始终保持在一个恒定的范围内。
批处理
如何有效减少获取锁的次数,提升系统整体的 TPS?
批量提交扣减商品库:先收集扣减请求,达到某个阈值,对请求进行合并,获取一次分布式锁。缺点:库存不足,这一批全部扣减失败。
控制单机并发写
- 单机排队串行写
- 抢购限流
分布式 SequenceID 生成
Shark(一款开源的 MySQL 分库分表中间件)内部提供了生成 SequenceID 的 API (底层支持数据库和 ZooKeeper 作为申请 SequenceID 的存储系统):
CREATE TABLE shark_sequenceid(
s_id INT NOT NULL AUTO_INCREMENT COMMENT '主键',
s_type INT NOT NULL COMMENT '类型',
s_useData BIGINT NOT NULL COMMENT '申请占位数量',
PRIMARY KEY(s_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_bin;
通过如下 API
获取:
// (int idcNum, int type, long memData)
SequenceIDManager.getSequenceId(100, 10, 5000);
第一个参数:IDC 机房编码,第二个参数:业务类别,第三个参数:向数据库申请的 ID 缓存数,返回一个长度为 19 位的 SequenceID。
Shark 只是负责封装 ID 的生成逻辑,真正保证唯一性和连续性的还是单点数据库。
多维度复杂查询
Solr
的目的就是要替换 SQL 中的 like '%香水%'
这种模糊查询,因为数据库会采用全表扫描。