系统设计总结

系统设计的题目,感觉不用做非常多,但要善于总结,相似的问题可以总结成一类,用一个模板来解答,然后加一些小细节加以区分。

如何将一个信息同时传播给多个关注该事务的client

定时任务

Event Sourcing

Event sourcing就是说我们不存最后的状态,而是把每一个action都存起来,然后需要结果的话,我们得到所有的action,然后derive出最后的状态是什么。那什么时候需要event sourcing呢?跟钱打交道的时候,比如说Auction system需要我们prove为什么是这个人赢得了最后的bid等。另一个就是,如果很多人都在竞拍一个产品,如果我们只存最终结果(在这种情况下是update Auction表里的winner),在高并发的情况下会一直修改同一个数据,这要比往bid table里不停append给bid table的的性能要差很多。

Lease机制

Lease机制是facebook设计memcached的时候,为了解决数据一致性的问题解决的,非常适合高并发场景。那么是如何工作的呢?
– 同一个cache key,memcached维护一个当前有效的lease token,不管多少请求都拿到这个token
– server A和server B都来读取数据,拿到相同token,然后server A先过来更新
– Memcached对比了token,有效,就把对应的值改掉,lease token也发生了变化
– server B再过来更新的时候因为token invalid,所以更新被拒绝

确保任务不会被重复执行

这是经典的分布式系统并发控制问题。例如,a cluster of worker nodes会不停的扫描数据库来发现下一个要做的task。如何保证node A take该task之后,别的node不会重复去执行呢?

-- 使用 SELECT FOR UPDATE 锁定记录
BEGIN TRANSACTION;
SELECT * FROM tasks WHERE field = 'target_value' AND status = 'pending' 
FOR UPDATE SKIP LOCKED 
LIMIT 1;

-- 更新状态表示正在处理
UPDATE tasks SET status = 'processing', worker_id = 'node_a', updated_at = NOW() 
WHERE id = ?;
COMMIT;

这是悲观锁方案

-- 第1步:普通查询(不在事务中)
SELECT id, field, status, version, data
FROM tasks 
WHERE field = 'target_value' AND status = 'pending'
ORDER BY created_at 
LIMIT 1;

-- 第2步:单独的原子更新操作
UPDATE tasks 
SET status = 'processing',
    worker_id = 'node_001',
    version = version + 1
WHERE id = 123
  AND version = 5        -- CAS检查
  AND status = 'pending';

这是乐观锁方案,用的CAS, 基于version。也可以基于时间戳,设一个last_modified column。

两个方案各有利弊。如果是高冲突环境下,悲观锁比较好,skip locked可以避免无效等待,每次查询基本都可以获得结果,不需要不停的retry。低冲突环境下,乐观锁比较好,没有锁开销,并发读取效率比较高。

Payment Service里的ledger

中文可以叫做账本。这是payment system里的核心组件,用来记录和追踪所有的交易,会记录每一笔交易的详细信息,将来审计的时候得用,非常的重要。这些记录是不能改变的,一旦记录就不能修改。通过累计计算所有相关交易,Ledger能够实时或准实时地维护每个账户的余额状态。简单来说,Ledger就是支付系统的”真相来源”,所有的资金变动都必须在这里得到准确、完整的记录。然后发生一笔transaction,就要在数据库里出现两条记录:

-- 转账示例:A账户转给B账户100元
-- Entry 1: A账户借方(扣款)
INSERT INTO ledger_entries (transaction_id, account_id, entry_type, amount, ...) 
VALUES ('tx_123', 'acc_A', 'DEBIT', 100.00, ...);

-- Entry 2: B账户贷方(入账)  
INSERT INTO ledger_entries (transaction_id, account_id, entry_type, amount, ...)
VALUES ('tx_123', 'acc_B', 'CREDIT', 100.00, ...);

Reconcile Service

各种资源/服务的QPS

关系型数据库 – 1000
redis – 10万

秒杀系统里的防止超卖

数据存储一般是redis + sql的组合。redis可以用lua脚本来实现原子性,redis放行可以在sql里再做一次校验。

Idempotency

Idempotency is the key to ensuring at-most-once guarantee. From an API standpoint, idempotency means clietns can make the same call repeatedly and produce the same result.
那如何防止user双重click呢?举个例子,在支付系统里,client call我们的service的时候需要一个idempotency key,例如shopping cart id当做我们的checkout id。然后当我们往sql table里写的时候,发现插入失败了,就证明前面已经有一个checkout action了,所以就直接返回前面那个checkout action的状态就好了。总之还是要靠数据库来保证idempotency。

Cache的读机制

Read Through Cache
Cache is responsible for retrieving the data from the underlying data store when a cache miss occurs. In this strategy, the application requestes data from the cache instead of the data store directly. If the data requested is not found in the cache, the cache retrieves data from data store, updates the cache with the retrived data, and returns data to the application.
优点:
1. 简化应用层逻辑,因为只需要负责和cache交互。
2. 一致性较好,因为是缓存层处理数据加载逻辑。
缺点:
1. 灵活性较低。
2. 缓存层逻辑较为复杂。

Read Aside Cache
Also known as cache aside/lazy loading, where the application is responsible for retrieving the data from underlying data store when a cache miss occurs. Application负责更新cache,如果cache hit,直接拿。如果cache miss,从data store拿数据并更新cache。
优点:
1. 控制粒度更为精细。应用层可以决定往cache里存什么。
2. 错误处理更为灵活。
缺点:
1. 代码复杂度高。
2. 缓存逻辑可能存在不同的应用里,容易出现不一致问题。

Cache的写机制

Write Through Cache
Data is written into the cache. Cache会负责更新db,Cache会先更新db,写入成功后再更新自己。优点是minimize risk of data loss,缺点是latency较高。

Write Around Cache
Data is written directly to permananet storage and bypassing the cache. 最大的问题是数据不一致,解决方法是写入数据库后把对应的key从cache里删除掉。

Write Back Cache
Data is written to cache alone, and completion is immediately confirmed to the client. The write to the permanent storage is done after specific intervals or under centain condition。优点是low latency和high throughput for write-intensive applications。缺点是如果cache挂了就会有data loss.

一种不太常见的做法叫Dual Write Pattern,就是application负责往DB和Cache里写。现在有个问题就是往DB里写成功了但往Cache里写失败了该怎么办?一种方法是直接删除cache里的旧数据,这样下次cache要从db里面读。另一种是retry,尝试再往cache里面写。

Scroll to Top