人人都是架构师 (一)

人人都是架构师 - 分布式系统架构落地与瓶颈突破

分布式系统应对高并发、大流量的常用手段:

  • 扩容
  • 动静分离
  • 缓存
  • 服务降级
  • 限流

限流

常见算法:

  • 令牌桶,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 '%香水%' 这种模糊查询,因为数据库会采用全表扫描