场景

短链接系统实现 如何保证生成短链接不重复 如何存储短链接 用302(临时)还是301(永久)重定向 https://cloud.tencent.com/developer/article/1858351 https://blog.csdn.net/codejas/article/details/106102452 https://juejin.cn/post/7312353213348741132 秒杀 使用redis(保证秒杀效率)的lua脚本(保证原子性)进行库存扣减,使用分布式事务的二阶段消息解决事务数据一致性。二阶段消息适用于无需回滚的这一类数据一致性问题,主要是为了保证第一阶段操作执行成功后,后续阶段一定能感知并执行。 二阶段消息的回查操作,主要还是依赖事务中第一阶段使用的数据库,来保证第一阶段整体操作的原子性以及幂等。 无论是请求执行lua脚本的服务端宕机,还是redis服务本身宕机,lua脚本都保证原子性,即写操作均无效 库存扣减lua脚本: -- KEYS[1] 库存 -- KEYS[2] 事务当前操作 -- KEYS[3] 如果事务当前操作是回滚操作,则为回滚所对应的操作 local v = redis.call('GET', KEYS[1]) -- 库存 local e1 = redis.call('GET', KEYS[2]) -- 事务当前操作的状态 if v == false or v + ARGV[1] < 0 then -- 库存不足 return 'FAILURE' end if e1 ~= false then -- 当前状态不为空,幂等退出 return 'DUPLICATE' end -- 设置当前操作为已完成 redis.call('SET', KEYS[2], 'op', 'EX', ARGV[3]) if ARGV[2] ~= '' then local e2 = redis.call('GET', KEYS[3]) if e2 == false then -- 如果是回滚操作,将回滚对应操作的状态设置为已回滚 redis.call('SET', KEYS[3], 'rollback', 'EX', ARGV[3]) return end end -- 库存扣减 redis.call('INCRBY', KEYS[1], ARGV[1]) 回查lua脚本: local v = redis.call('GET', KEYS[1]) -- 扣减库存操作的状态 if v == false then -- 为空则直接回滚 redis.call('SET', KEYS[1], 'rollback', 'EX', ARGV[1]) v = 'rollback' end -- 如果阶段1是回滚,直接返回事务失败 if v == 'rollback' then return 'FAILURE' end -- 如果不是回滚,说明事务成功 以下是可能出现的各个场景。 ...

十一月 6, 2024 · by NOSAE

clickhouse ClickHouse 的高性能主要来自以下几个方面的设计特点: ​ 1. 列式存储:ClickHouse采用列式存储(Columnar Storage),在查询时可以只读所需的列,而不是整个行。这极大减少了磁盘I/O,尤其适合分析型查询。 ​ 2. 数据压缩:列式存储便于数据压缩,ClickHouse内置了多种压缩算法(如LZ4、ZSTD等),根据数据特征选择最佳压缩方式,减少了存储空间和I/O开销。 ​ 3. 向量化引擎:ClickHouse的数据处理是基于向量化引擎的。向量化处理数据意味着每次处理一个数据块,而不是单条数据,充分利用了CPU的指令集,极大提升了数据处理速度。 ​ 4. 多线程并行查询:ClickHouse支持多线程并行处理,分区并行化查询,充分利用多核CPU的性能。在一个查询中可以调度多个线程同时工作,使得复杂查询能高效完成。 ​ 5. 内存管理:ClickHouse对内存管理进行了优化,尽可能减少内存分配和释放操作。比如查询时,通常不会频繁分配和释放内存,而是通过重用内存块来降低内存碎片。 ​ 6. 数据分区和分片:ClickHouse支持对数据进行水平分区和分片,能够处理海量数据。它可以将不同分片的数据分布到不同节点上,并行处理分片数据,提高了系统的扩展性和查询性能。 ​ 7. 基于MergeTree的表引擎:MergeTree 是ClickHouse的核心存储引擎。它支持数据的自动排序和索引,查询时可以快速定位目标数据块,避免了全表扫描。 ​ 8. 向量化索引:ClickHouse会为列数据创建稀疏索引,通过向量化索引来过滤数据块,进一步加速查询。辅助索引的存在能够快速定位相关数据块,尤其适用于高基数的列。 ​ 9. 数据延迟写入和异步物化视图:ClickHouse的写入延迟较低,因为它将数据先写入内存中的“insert buffer”缓冲区,之后才会批量写入磁盘。异步物化视图会在后台进行处理,不影响查询性能。 以上技术特性使ClickHouse在处理大数据量的复杂分析查询时,性能极为优越,能够快速响应实时查询需求。 建表时order by的作用: 确定数据存储顺序:ORDER BY字段决定了表内数据的存储顺序,ClickHouse会按照该字段对数据块进行排序。 自动生成稀疏索引:ORDER BY字段会自动创建稀疏索引,帮助ClickHouse跳过不满足查询条件的数据块,从而加速查询。 提高查询效率:尤其对于范围查询、过滤、分组等,选择合适的ORDER BY字段能够减少扫描量并加快响应速度。 名词解释 稀疏索引:在ClickHouse中,数据组织为:分区(partition)->数据块(parts)->数据段(granules)。稀疏索引记录每个数据段的特征(比如最大值、最小值),查询的时候用来快速跳过某些不需要加载的数据段。 辅助索引:稀疏索引不适用于加速高基数列的查询。辅助索引有bloom filter、minmax等

十一月 1, 2024 · by NOSAE

golang sync包源码阅读

前言 sync包提供了常见的并发编程工具,比如最常见的Mutex、WaitGroup等。这些工具都非常简洁,几乎0学习成本。本篇将从源码角度简单看看这些工具的实现原理,以在未来有需求的时候,理解甚至是手动实现功能更强大的,更复杂的并发编程工具。 sync.Mutex sync.Mutex是golang中的互斥锁,但是注意它仅仅具有互斥访问的功能,没有其他功能,比如不支持可重入、不可自定义公平/非公平。 公平性 对于公平性,Mutex采取了综合两者的做法: normal mode(非公平模式,利于高效率运行):锁释放时,优先让同时新来尝试获取锁的线程获取到锁,而不是等待队列中的线程,运行成本低,只需数次CAS就能获取到锁。这是默认的模式 starvation mode(公平模式,避免高并发下线程饿死):锁释放时,优先让等待队列的线程获取到锁,而不是新来的线程。当等待队列队头线程等待超过1ms进入公平模式 如果当前为公平模式,那么当等待队列唯一的队头线程获取到锁,或者队头线程等待时间不足1ms,又会自动回到非公平模式。 可重入性 在开始源码之前,关于为什么golang的官方互斥锁不考虑支持可重入性我想简单讨论下。Russ Cox在讨论里核心观点在于:互斥锁的目的是保护程序的不变性(即invariant,关于什么是程序的不变性可以参考这篇)。因此当线程获取到互斥锁以及释放锁的那一刻,程序都应该是invariant的,在持有锁的期间,程序可以随便破坏invariant,只要保证释放锁的那一刻恢复了invariant即可。从这个观点来说,如果锁是可重入的,就会有这样的情况发生: func G() { mu.Lock() // 破坏invariant ... F() // 恢复invariant ... mu.Unlock() } func F() { mu.Lock() // 此时持有锁,程序应该是invariant的 // 继续执行下去可能会导致bug,因为F认为持有锁的那一刻程序是invariant的 // 但F不知道invariant已经被G破坏 ... mu.Unlock() } 也就是说,Russ Cox给互斥锁功能上的定义是保持程序的invariant,因此可重入锁的想法就是错的。但也有别的观点认为他对互斥锁的定义是错的,互斥锁本身就是为了避免多线程访问修改变量,invariant是开发者的责任,与你用不用互斥锁无关,互斥锁只是帮助你实现invariant的,并且出于编程上的方便,可重入锁可以make your life easier!况且很多语言其实都支持可重入锁。 另外关于invariant,本人也不认为是互斥锁的责任,比如在单线程的程序中,你需要维护(a==b)==true这个invariant,而且由于单线程你根本不需要锁,那么只要你会改变a或者改变b,就肯定会有某些时刻会出现invariant被破坏的情况,但这些情况一般是函数内部的瞬时发生的,而函数执行前后都是保持invariant的就没问题,可以看到与锁无关。因此锁只是个实现invariant的工具之一,它只需要关注底层并发的事情,不需要给他下“保持程序的invariant”这样的高层抽象定义。 后面我们会利用sync包现有的工具,尝试实现一个可重入锁。 源码 Mutex结构体: const ( mutexLocked = 1 << iota // 锁是否被线程持有 mutexWoken // 是否有被唤醒的线程正在等待获取锁 mutexStarving // 是否处于饥饿模式 ) type Mutex struct { state int32 // 低三位分别对应上述三个状态,高位记录等待队列的线程数 sema uint32 // 给底层同步原语使用的信号量 } Lock方法: func (m *Mutex) Lock() { // Fast path: 使用CAS快速加锁 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path: CAS加锁失败,说明锁被其他线程占有,当前应该被阻塞 m.lockSlow() } lockSlow方法: ...

十月 31, 2024 · by NOSAE

分布式事务综述

理论知识 事务的四个特性:ACID Atomic 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成 Consistency 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。完整性包括外键约束、应用定义等约束不会被破坏 Isolation 隔离性:防止多个事务并发执行时由于交叉执行而导致数据的不一致 Durability 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失 分布式系统三个特性:CAP Consistency 一致性:集群执行某个操作后,所有副本节点的状态都相同,那么这样的系统就被认为具有强一致性 Available 可用性:集群一部分节点故障后,还能对外提供服务 Partition tolerance 分区容忍性:狭义上是集群节点之间是否能正常通信,更广义的是对通信的时限要求,系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况 对于互联网的场景来说,一般选择AP:因为现在集群规模越来越大,主机众多、部署分散,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C。 分布式的BASE理论:柔性事务,对CAP的权衡 Basically Available 基本可用:系统在出现不可预知故障的时候,允许损失部分可用性,但这绝不等价于系统不可用 Soft state 软状态:允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性 Eventually consistent 最终一致性:经过一段时间的同步后,最终能够达到一个一致的状态 NewSQL的分布式事务: 以Spanner、TiDB为代表的NewSQL,在内部集群多节点间,实现了ACID的事务,即提供给用户的事务接口与普通本地事务无差别,但是在内部,一个事务是支持多个节点多条数据的写入,此时无法采用本地ACID的MVCC技术,而是会采用一套复杂的分布式MVCC来做到ACID。属于满足CP的系统,同时接近于满足A,称之为CP+HA。但是NewSQL和BASE的系统之间,性能上差异可能是巨大的。 跨库跨服务的分布式事务:这类分布式事务部分遵循 ACID 原子性:严格遵循 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽 隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽 持久性:严格遵循 现有的分布式事务方案都无法做到强一致,但是有强弱之分:XA事务 > TCC > 二阶段消息 > SAGA(一般情况下)。具体为: XA:XA虽然不是强一致,但是XA的一致性是多种分布式事务中,一致性最好的,因为他处于不一致的状态时间很短,只有一部分分支开始commit,但还没有全部commit的这个时间窗口,数据是不一致的。因为数据库的commit操作耗时,通常是10ms内,因此不一致的窗口期很短。 TCC:理论上,TCC可以用XA来实现,例如Try-Prepare,Confirm-Commit,Cancel-Rollback。但绝大多数时候,TCC会在业务层自己实现Try|Confirm|Cancel,因此Confirm操作耗时,通常高于XA中的Commit,不一致的窗口时间比XA长 MSG:二阶段消息型事务在第一个操作完成后,在所有操作完成之前,这个时间窗口是不一致的,持续时长一般比前两者更久。 SAGA:SAGA的不一致窗口时长与消息接近,但是如果发生回滚,而子事务中正向操作修改的数据又会被用户看到,这部分数据就是错误数据,容易给用户带来较差的体验,因此一致性是最差的。 2PC 两阶段提交流程如下: 准备阶段:RM去开启事务并执行操作,但是不提交事务,此时相关资源都被锁定,第三方不能访问这些资源。返回操作结果给TM 提交阶段:若所有RM在准备阶段都操作成功,TM将发出请求让所有RM提交事务,否则发出请求让所有RM回滚 存在问题: 同步阻塞:从准备阶段的锁定资源,直到事务提交/回滚的过程中,资源都处于被RM锁定的状态,第三方访问会被阻塞。 单点故障:2PC的推进重度依赖TM,TM若发生故障,RM会被阻塞。 数据不一致:若提交阶段部分RM无法与TM正常通信,导致一部分子事务提交了,而发生异常的RM没提交,全局事务发生不一致。 3PC 3PC提交将2PC的准备阶段再次拆分,加入检查阶段,如果检查失败的话马上abort,减少了2PC在失败情况下白白浪费资源,并且引入RM的超时机制(2PC只有TM超时机制),如果在提交阶段TM超时,直接提交事务释放资源。3PC流程如下: 检查阶段:TM 询问 RM,是否具备执行事务的条件,RM 进行自身事务必要条件的检查 预提交阶段:TM 通知 RM 进行事务的预提交 提交阶段:TM 根据预提交阶段 RM 的反馈结果通知 RM 是否进行事务提交或是进行事务回滚 RM超时自动提交是因为此时已经进入了提交阶段,说明RM知道检查阶段已经成功(但是RM不知道预提交阶段是否成功),认为第三阶段很大概率也可以成功。但很明显,如果第二步存在RM失败了,即预提交失败,因此在第三步中,TM发出回滚请求,但此时又有另外一个RM超时并自动提交了事务(超时时间内收不到回滚请求),就会出现不一致。 ...

十月 28, 2024 · by NOSAE

字节RPC框架kitex源码阅读(二)

Note 基于kitex@v0.11.3 开篇 在上篇字节RPC框架kitex源码阅读(一)中,简单过了一遍从创建服务、监听端口、建立连接&派发、退出清理的流程,对于代码生成的回调如何在kitex内部得到调用也有了初步的认知。 这篇是(一)的续篇,深入分析remote.Server如何基于与客户端建立的连接做交互,包括传输、解码、编码等。 remote.ServerTransHandler 我们知道server.Server主要构建调用链、调用用户定义的回调。与远程传输有关的remote.Server提供了简单的几个接口方法给server.Server使用,相当于server.Server只需要关心调用链要怎么消费封装好的数据,不用管传输如何建立、数据如何封装: ...

十月 23, 2024 · by NOSAE