BASE基本可用

本篇文章以数据库研发的视角开始,角度可能些许刁钻或者偏激,还请指正。

问题背景

单机数据库

在以前的研发中,我们利用了单机数据库,并大胆地将事务逻辑下发到数据库中处理,因为我们可以充分依赖数据库的ACID特性来确保数据的完整性和事务的可靠性。

•原子性(Atomicity):事务中的操作要么全部完成,要么全部不发生,它保证了事务是不可分割的工作单位。如果事务中的任何操作失败,整个事务将被回滚,数据库状态不会发生变化,就像这个事务从未执行过一样。

•一致性(Consistency):事务将数据库从一个一致的状态转变到另一个一臀的状态。在事务开始之前和结束之后,数据库的完整性约束没有被破坏。这意味着数据库中的数据将始终满足所有预定义的规则,包括数据类型、约束、级联规则等。

•隔离性(Isolation):并发执行的事务之间是相互隔离的,事务的中间状态对其他并发事务是不可见的。这避免了多个事务同时执行时可能出现的问题,如脏读、不可重复读和幻读。数据库系统通常通过锁定对象来实现隔离性。

•持久性(Durability):一旦事务被提交,它对数据库的修改是永久性的,即使系统发生故障也不会丢失。这通常通过使用数据库日志实现,即使在系统崩溃后也能重新构建提交的事务。

但是随着数据量的提升,在数据量增长导致单机数据库性能瓶颈时,我们面临着扩展的需求。

单机数据库扩展

常见的扩展方式分两种:

•垂直扩展(Vertical Scaling):就是升级你的服务器硬件,比如增加更多的CPU、内存或存储空间。这种方式简单,但是成本高,而且最终会受到最大系统容量的限制。

•水平扩展(Horizontal Scaling):意味着通过增加更多服务器来分担负载,这种方式更灵活,但也更复杂。水平扩展可以通过功能分区和分片来实现。功能分区是指按功能将数据分组,并分布在不同的数据库中。分片是指在功能区域内将数据分布在多个数据库中。

这种扩展形式也形成了当今多种热门分布式数据库架构形态的本质区别。

个人而言可能更倾向于水平扩展,因为尽管垂直扩展简单直接,但成本高昂且存在物理极限,而且对性能的提升甚至。

但是水平扩展的场景意味着我们的数据库从单机变为了分布式的结构。然而分布式结构有一个亘古不变的CAP【一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)】定理。

强一致性的分布式数据库

CAP中,如果我们要求了强一致性(对数据不一致零容忍),我们可能会牺牲部分可用性的能力(分区容错性是必需的,因为网络分区是现实中不可避免的),常见的做法是我们可能会引入两阶段提交(2PC)或三阶段提交(3PC)等多种处理方案,协调每一个数据库节点的ACID来保证原子事务从而控制全局事务一致性。

无论什么方案,可能都是一种延迟决策的策略,是一种悲观的处理逻辑,这可能会增加响应时间,影响用户体验。

更重要的是,在分布式系统中,单个组件的高可用性并不直接等于整体系统的高可用性。如果每个数据库的可用性是99.9%,那么两个数据库的事务可用性就降低到了99.8%,这意味着每月会有额外的43分钟停机时间。

BASE模型

可如果我们各退一步是不是海阔天空了。数据库是不是可以试试BASE模型,这样就不会卡死了。

•基本可用(Basically Available):系统在面对故障时,仍然保证核心可用。这意味着在出现部分故障时,系统可能会降低性能(例如,响应时间变长,部分功能暂时不可用),但核心功能仍然是可用的。这与ACID中的可用性不同,ACID要求系统即使在出现故障的情况下也要保证所有功能的正常响应。

•软状态(Soft state):系统的状态不需要时刻保持一致,而是可以有一段时间的不一致。在分布式系统中,由于复制延迟或网络分区,不同节点上的数据副本可能会暂时出现不一致的情况。软状态的概念接受这种暂时的不一致性,并认为这是正常现象。

•最终一致性(Eventually consistent):系统保证在没有新的更新操作的情况下,最终所有的数据副本都将达到一个一致的状态。这并不保证立即一致,但是保证在一定时间后,数据的一致性将被达成。这个时间可能是几毫秒,也可能是几分钟,甚至更长,取决于系统的设计和网络条件。

BASE模型是乐观的,接受数据库一致性会有变化。这种方式可以通过支持部分失败而不导致整个系统失败来提高可用性。比如,如果一个用户数据库服务器出现问题,只影响该服务器上20%的用户。

BASE应用

但是有人会说,数据库退一步了,业务怎么退?数据不一致的后果谁来承担。

为了在应用中实现BASE,需要业务方对事务操作进行更深入的分析。如果需要保证更新操作的幂等性(即多次应用同一个操作结果不变),可以使用消息队列和事件驱动架构来确保用户表的一致性。

举一个例子吧:

创建必要的表结构:

CREATE TABLE transactions (
    id UUID PRIMARY KEY,
    from_account_id INT,
    to_account_id INT,
    amount DECIMAL(10, 2),
    status VARCHAR(10), -- 'PENDING', 'COMPLETED', 'FAILED'
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE accounts (
    account_id INT PRIMARY KEY,
    balance DECIMAL(10, 2)
);

接下来,我们来看一下转账操作的SQL逻辑:

-- 准备一些变量作为示例
DECLARE @TransactionId UUID = NEWID(); -- 假设这是应用程序生成的唯一事务ID
DECLARE @FromAccountId INT = 123; -- 转账发起账户ID
DECLARE @ToAccountId INT = 456; -- 接收转账的账户ID
DECLARE @TransferAmount DECIMAL(10, 2) = 100.00; -- 转账金额

-- Step 1: 检查是否已经存在相同的事务ID
IF NOT EXISTS (SELECT 1 FROM transactions WHERE id = @TransactionId)
BEGIN
    -- Step 2: 开始事务
    BEGIN TRANSACTION
    
    -- 尝试从发起账户扣款
    UPDATE accounts SET balance = balance - @TransferAmount
    WHERE account_id = @FromAccountId AND balance >= @TransferAmount;
    
    IF @@ROWCOUNT > 0
    BEGIN
        -- 如果扣款成功,尝试给接收账户加款
        UPDATE accounts SET balance = balance + @TransferAmount
        WHERE account_id = @ToAccountId;
        
        IF @@ROWCOUNT > 0
        BEGIN
            -- 如果加款成功,记录这次转账为'COMPLETED'
            INSERT INTO transactions (id, from_account_id, to_account_id, amount, status)
            VALUES (@TransactionId, @FromAccountId, @ToAccountId, @TransferAmount, 'COMPLETED');
            
            -- 提交事务
            COMMIT TRANSACTION
        END
        ELSE
        BEGIN
            -- 如果接收账户加款失败,回滚事务,并记录转账为'FAILED'
            ROLLBACK TRANSACTION
            
            INSERT INTO transactions (id, from_account_id, to_account_id, amount, status)
            VALUES (@TransactionId, @FromAccountId, @ToAccountId, @TransferAmount, 'FAILED');
        END
    END
    ELSE
    BEGIN
        -- 如果发起账户扣款失败,回滚事务,并记录转账为'FAILED'
        ROLLBACK TRANSACTION
        
        INSERT INTO transactions (id, from_account_id, to_account_id, amount, status)
        VALUES (@TransactionId, @FromAccountId, @ToAccountId, @TransferAmount, 'FAILED');
    END
END
ELSE
BEGIN
    -- 如果事务ID已存在,不执行任何操作,保持幂等性
    PRINT 'Transaction already processed';
END

此外,要实现高交易量的系统扩展,需要采用新的资源管理方式。传统的事务模型在负载需要分布在大量组件上时会出现问题。通过解耦操作并依次执行它们,可以提高可用性和扩展性,尽管这样做会牺牲一致性。BASE提供了一个思考这种解耦的模型。

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值