2021-08-10-消息队列以及使用场景

2021-08-10-消息队列以及使用场景 [[Message Queue]] 消息队列以及使用场景 在计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的资料,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。 简单的说,就是一个中间件,可以存储生产者产生的数据,可以被消费者进行消费,而生产者和消费者之间处于解耦。对于不同的消息队列实现来说,例如 Kafka、rabbitMQ 等等,会有一些功能上或细节上的差距,但本质还是做上下文组件的数据传递。 好处 解耦:在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。 提速/缓冲:在任何重要的系统中,都会有需要不同的处理时间的元素。消息队列的加入可以视为一种缓存。 异步通信:消息队列结构上就决定了异步通信。 削峰:消息队列能够使关键组件顶住突发的访问压力。 缺点 引入复杂度:毕竟多了一个中间组件,原始的数据交互模式变得更复杂 暂时的不一致性:有了中间状态,上下游之间存在不一致性,对于上游来说,存入数据队列就等于下游已经读取,但实际上这时有个时间差的 什么情况下可以使用消息队列 生产者不需要从消费者处获得反馈 容许短暂的不一致性 可以带来系统性能上的提升 常用 MQ 的对比 RabbitMQ RabbitMQ 是使用 Erlang 编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了 Broker 构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。 Redis Redis 是一个基于 Key-Value 对的 NoSQL 数据库,开发维护很活跃。虽然它是一个 Key-Value 数据库存储系统,但它本身支持 MQ 功能,所以完全可以当做一个轻量级的队列服务来使用。对于 RabbitMQ 和 Redis 的入队和出队操作,各执行 100 万次,每 10 万次记录一次执行时间。测试数据分为 128Bytes、512Bytes、1K 和 10K 四个不同大小的数据。实验表明:入队时,当数据比较小时 Redis 的性能要高于 RabbitMQ,而如果数据大小超过了 10K,Redis 则慢的无法忍受;出队时,无论数据大小,Redis 都表现出非常好的性能,而 RabbitMQ 的出队性能则远低于 Redis。 ZeroMQ ZeroMQ 号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ 能够实现 RabbitMQ 不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这 MQ 能够应用成功的挑战。ZeroMQ 具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用 ZeroMQ 程序库,可以使用 NuGet 安装,然后你就可以愉快的在应用程序之间发送消息了。但是 ZeroMQ 仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter 的 Storm 0.9.0 以前的版本中默认使用 ZeroMQ 作为数据流的传输(Storm 从 0.9 版本开始同时支持 ZeroMQ 和 Netty 作为传输模块)。 ...

January 16, 2026

2021-08-09-雪花算法

2021-08-09-雪花算法 Algorithm [转载]雪花算法简介 转载信息 作者:Jeffrey 发表时间:2019-10-09 22:05 原始链接:# 分布式ID神器之雪花算法简介 雪花算法这一在分布式架构中很常见的玩意,但一般也不需要怎么去深入了解,一方面一般个人项目用不到分布式之类的大型架构,另一方面,就算要用到,市面上很多ID生成器也帮我们完成了这项工作。 分布式 ID 的特点 全局唯一性:不能出现有重复的 ID 标识,这是基本要求。 递增性:确保生成 ID 对于用户或业务是递增的。 高可用性:确保任何时候都能生成正确的 ID 高性能:在高并发的环境下依然表现良好 分布式ID的常见解决方案 UUID 生成一串唯一随机 36 位字符串(32个字符串+4个“-”)的算法。它可以保证唯一性,但是其业务可读性差,无法有序递增。 SnowFlake 今天的主角雪花算法,它是 Twitter 开源的由 64 位整数组成分布式 ID,性能较高,并且在单机上递增。 具体参考:twitter-archive/snowflake UidGenerator UidGenerator 是百度开源的分布式 ID 生成器,其基于雪花算法实现。 具体参考:baidu/uid-generator Leaf Leaf 是美团开源的分布式 ID 生成器,能保证全局唯一,趋势递增,但需要依赖关系数据库、Zookeeper 等中间件。 具体参考:Leaf——美团点评分布式ID生成系统 雪花算法的概要 SnowFlake 是 Twitter 公司采用的一种算法,目的是在分布式系统中产生全局唯一且趋势递增的 ID。 组成部分(64bit) 第一位:占用 1 bit,其值始终是 0,没有实际作用 时间戳:占用 41 bit,精确到毫秒,总共可以容纳约 69 年的时间 工作机器id:占用 10 bit,其中高位 5 bit 是数据中心 ID,低位 5 bit 是工作节点 ID,最多可以容纳 1024 个节点。 序列号:占用 12 bit,每个节点每毫秒 0 开始不断累加,最多可以累加到 4095,一共可以产生 4096 个 ID。 SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一 ID 呢? 同一毫秒的ID数量 = 1024 X 4096 = 4194304 ...

January 16, 2026

2021-08-08-KMP算法

2021-08-08-KMP算法 Algorithm KMP 算法 KMP算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。 它的核心就是 PMT,也就是 next 表。 暴力的匹配算法就是把子串和主串逐位进行匹配,如果不匹配,则子串右移一位。这种情况下必定会产生很多无效匹配,复杂度为 O(mn)。 KMP 说难不难,说简单也不简单,先要理解 PMT。 先解释一下字符串的“前缀”和“后缀”。 如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称B为A的前缀。例如,”Harry”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合。 同样可以定义后缀A=SB, 其中S是任意的非空字符串,那就称B为A的后缀,例如,”Potter”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的后缀。 对于一个字符串,它的 PMT 值就是前缀集合和后缀集合中相同最长的字符串的长度。 char a b a b a b c a index 0 1 2 3 4 5 6 7 value 0 0 1 2 3 4 0 1 对于字符串 ab,前缀集合 {“a”},后缀集合 {“b”},可见部分匹配表的值为0 对于字符串 aba,前缀集合{“a”,“ab”},后缀集合 {“ba”, “a”},可见部分匹配表为1 对于字符串 abab,前缀集合{“a”,“ab”,“aba”},后缀集合{“bab”,“ab”,“b”},最长的相同字符串为 “ab”,所以是2 对于字符串 ababa,前缀集合{“a”,“ab”,“aba”,“abab”},后缀集合{“baba”,“aba”,“ba”,“a”},最长的相同字符串为 “aba”,所以为 3 那么 PMT 表的这个特性有什么好处?好处就是字符匹配过程中,主串上的指针永不后退。 ...

January 16, 2026

2021-08-07-Golang GC 三色标记和混合写屏障

2021-08-07-Golang GC 三色标记和混合写屏障 Go Golang GC 三色标记和混合写屏障 大部分高级语言都有自己的垃圾回收机制,称之为 GC,Golang 的垃圾回收在多次迭代后才变成了现在这样的增加混合写屏障的三色标记法。 STW(stop the world):程序暂停,一般出现在GC中或者某些状态的保存过程,此时程序不停止的话可能会造成很多异常的情况,甚至于无法实现需求。 v1.3之前的标记清除法的实现和缺点 两个主要步骤:标记、清除。 STW,然后找出不可达到的对象,打上标记 清除这些对象 STW 结束,等待下一个 GC 周期 上述的逻辑简介但可行,对于 GC 来说也确实可以实现,但它有不少缺点。 STW(stop the world):让程序暂停,程序出现卡顿 (重要问题)。 标记需要扫描整个 heap 清除数据会产生 heap 碎片 v1.3 版本之前的逻辑就在上方,为了降低 STW 的影响,v1.3 版本的标记清除逻辑为: STW,然后找出不可达到的对象,打上标记 STW 结束,等待下一个 GC 周期 清除这些对象 但是这个方法还是治标不治本,STW的影响还是太大。 v1.5三色并发标记法 三色标记法 实际上就是通过三个阶段的标记来确定清除的对象都有哪些。 所有对象以及新创建的对象,都标记为白色 GC 开始时,从根节点开始遍历,可以遍历到的对象标记为灰色 遍历灰色节点,灰色节点引用的对象也变成灰色节点,然后该灰色节点变成黑色节点 重复第三步,直到灰色对象全部变成黑色对象 回收白色对象。这时候的白色对象就没有被任何对象引用 没有 STW 的三色并发标记法 标记清除法一个很大的问题就是 STW,那么三色并发标记法可以抛弃 STW 吗?答案是否定的。 如果没有 STW,会出现对象丢失的问题,需要符合以下两个条件: 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下) 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色) 当符合条件1之后,这个黑色对象不会继续遍历下去,所以这个白色对象就有风险;当符合条件2之后,这个对象就必定会丢失。但实际上这个白色对象是被引用的。 需要一些新的机制来破坏上面两个条件。 屏障机制 强-弱 三色不等式 强三色不等式:不存在黑色对象引用到白色对象的指针,破坏条件1 弱三色不等式:所有被黑色对象引用的白色对象都处于灰色保护状态,破坏条件2,被引用对象至少被一个灰色对象引用 插入屏障 具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色) ...

January 16, 2026

2021-08-04-Go 逃逸分析

2021-08-04-Go 逃逸分析 [[Go]] [转载]Go 逃逸分析 转载信息 作者:极客兔兔 更新时间:2022-03-10 原始链接:Go 逃逸分析 前言 逃逸?谁逃逸了?从哪儿逃到了哪儿? 数据(对象)从栈逃到了堆上。 Go 程序会在 2 个地方为变量分配内存,一个是全局的堆(heap)空间用来动态分配内存,另一个是每个 goroutine 的栈(stack)空间。与 Java、Python 等语言类似,Go 语言实现垃圾回收(Garbage Collector)机制,因此呢,Go 语言的内存管理是自动的,通常开发者并不需要关心内存分配在栈上,还是堆上。但是从性能的角度出发,在栈上分配内存和在堆上分配内存,性能差异是非常大的。 在函数中申请一个对象,如果分配在栈中,函数执行结束时自动回收,如果分配在堆中,则在函数结束后某个时间点进行垃圾回收。 在栈上分配和回收内存的开销很低,只需要 2 个 CPU 指令:PUSH 和 POP,一个是将数据 push 到栈空间以完成分配,pop 则是释放空间,也就是说在栈上分配内存,消耗的仅是将数据拷贝到内存的时间,而内存的 I/O 通常能够达到 30GB/s,因此在栈上分配内存效率是非常高的。 在堆上分配内存,一个很大的额外开销则是垃圾回收。Go 语言使用的是标记清除算法,并且在此基础上使用了三色标记法和写屏障技术,提高了效率。 逃逸分析 什么是逃逸分析? 在 C 语言中,可以使用 malloc 和 free 手动在堆上分配和回收内存。Go 语言中,堆内存是通过垃圾回收机制自动管理的,无需开发者指定。那么,Go 编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。 指针逃逸 指针逃逸应该是最容易理解的一种情况了,即在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。 // main_pointer.go package main import "fmt" type Demo struct { name string } func createDemo(name string) *Demo { d := new(Demo) // 局部变量 d 逃逸到堆 d.name = name return d } func main() { demo := createDemo("demo") fmt.Println(demo) } 这个例子中,函数 createDemo 的局部变量 d 发生了逃逸。d 作为返回值,在 main 函数中继续使用,因此 d 指向的内存不能够分配在栈上,随着函数结束而回收,只能分配在堆上。 ...

January 16, 2026

2021-08-03-GMP

2021-08-03-GMP [[Go]] GMP GMP 是 Golang 的调度器实现方案,其实不难,带着以下几个问题去思考? Q1:进程、线程是什么?它们之间的关系? Q2:协程是什么?它和进程、线程的关系是什么? Q3:Golang 旧版调度器是什么?有什么缺点? Q4:G、M、P 分别指代什么? Q5:为什么要有 P?解决了什么问题? Q6:G、M、P 的数量关系应该是如何的? Q7:G、M、P 到底是怎么调度的(关键)? 上面所有问题的答案都可以在 典藏版 Golang 调度器 GMP 原理与调度全分析 ,下面的内容将是简洁的回答。 Q1:进程、线程是什么?它们之间的关系? 进程是操作系统对一个正在运行的程序的一种抽象,进程是资源分配的最小单位。系统是给进程直接分配资源,它在运行过程中持有相关上下文(寄存器)。 进程存在的意义是:CPU速度太快,而单个任务(job)不一定实时的使用CPU,可能会等待其他IO,此时对于CPU就是一种浪费,所以形成了一个新的使用场景。多个任务同时运行,按照不同的CPU调度策略进行分配,在计算机科学中,单个的任务就叫做进程。 进程是一个缩写,代表:进行的程序。所以对于大部分程序来说,都是只启动一个进程的,也可以说一个进程就是一个实例。它的粒度是比较大的,程序内部的代码是有多个分支或者程序段的,这时就引出了线程概念。线程是依托于进程的,它们之间可以共享进程所持有的上下文信息,对于关键资源还会通过锁的方式进行线程间的限制。线程运行时的本质和进程一样,都是要占据 CPU 一段时间进行运算。 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。 做个简单的比喻:进程=火车,线程=车厢 线程在进程下行进(单纯的车厢无法运行) 一个进程可以包含多个线程(一辆火车可以有多个车厢) 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁” 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” Q2:协程是什么?它和进程、线程的关系是什么? 协程其实不算是操作系统的概念,不属于内核层,它更多的是编程层面的概念,属于用户态(即协程是由编程语言实现,由开发者自己创建并调用)。 协程与进程、线程相比并不是一个维度的概念,协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。 Q3:Golang 旧版调度器是什么?有什么缺点? GM 模型,G是协程,M是线程。M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。 缺点: 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争。 M 转移 G 会造成延迟和额外的系统负载。比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。 Q4:G、M、P 分别指代什么? GMP 在 Golang 里就是三种数据结构,它们的功能是: ...

January 16, 2026

2021-08-02-进程、线程和协程

2021-08-02-进程、线程和协程 Go 进程、线程和协程 这个问题太常提到了,说难不难,但往深了说又有很多可挖掘的细节。当前仅记录这三者的概念、区别和关系,更精准的差异需要参考相关书籍。 进程 进程是操作系统对一个正在运行的程序的一种抽象,进程是资源分配的最小单位。系统是给进程直接分配资源,它在运行过程中持有相关上下文(寄存器)。 进程存在的意义是:CPU速度太快,而单个任务(job)不一定实时的使用CPU,可能会等待其他IO,此时对于CPU就是一种浪费,所以形成了一个新的使用场景。多个任务同时运行,按照不同的CPU调度策略进行分配,在计算机科学中,单个的任务就叫做进程。 一般进程有三种状态: 运行态:正在调用CPU 就绪态:啥都好了,就差CPU空出来了 阻塞态:需要等待其他事件(例如文件IO,网络IO) 需要注意的是,现在常用的CPU调度模型为:时间片轮转法,它主要适用于多个进程按照时间片进行轮流调用,在用户看来,这台电脑就像是同时运行多个进程,实际上单个 CPU 只能运行单个进程,只是切换的速度太快了,用户无感知。 线程 进程是一个缩写,代表:进行的程序。所以对于大部分程序来说,都是只启动一个进程的,也可以说一个进程就是一个实例。它的粒度是比较大的,程序内部的代码是有多个分支或者程序段的,这时就引出了线程概念。线程是依托于进程的,它们之间可以共享进程所持有的上下文信息,对于关键资源还会通过锁的方式进行线程间的限制。线程运行时的本质和进程一样,都是要占据 CPU 一段时间进行运算。 进程和线程的关系 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。 做个简单的比喻:进程=火车,线程=车厢 线程在进程下行进(单纯的车厢无法运行) 一个进程可以包含多个线程(一辆火车可以有多个车厢) 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁” 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” 协程 协程其实不算是操作系统的概念,不属于内核层,它更多的是编程层面的概念,属于用户态(即协程是由编程语言实现,由开发者自己创建并调用)。 协程与进程、线程相比并不是一个维度的概念,协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。 线程的栈有 8 MB,而协程栈的大小通常只有 KB,而 Go 语言的协程更夸张,只有 2-4KB,非常的轻巧。 优点: 内存占用小:协程更加轻量,创建成本更小,降低了内存消耗,协程一般只占据极小的内存(2~5KB),而线程是 1MB 左右。虽然线程和协程都是独有栈,但是线程栈是固定的,比如在Java中,基本是 2M,假如一个栈只有一个打印方法,还要为此开辟一个 2M 的栈,就太浪费了。而 Go 的的协程具备动态收缩功能,初始化为 2KB,最大可达 1GB 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。 减少了同步锁:协程最终还是运行在线程上,本质上还是单线程运行,没有临界区域的话自然不需要锁的机制。多协程自然没有竞争关系。但是,如果存在临界区域,依然需要使用锁,协程可以减少以往必须使用锁的场景 稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。 异步逻辑清晰:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。 缺点: 无法利用多核资源:协程运行在线程上,单线程应用无法很好的利用多核,只能以多进程方式启动。 协程不能有阻塞操作:线程是抢占式,线程在遇见IO操作时候,线程从运行态→阻塞态,释放cpu使用权。这是由操作系统调度。协程是非抢占式,如果遇见IO操作时候,协程是主动释放执行权限的,如果无法主动释放,程序将阻塞,无法往下执行,随之而来是整个线程被阻塞。 CPU密集型不是长处:假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。 参考 线程和进程的区别是什么? Go 面试官:什么是协程,协程和线程的区别和联系? 并发编程-协程 进程与线程的一个简单解释

January 16, 2026

2021-08-01-MySQL和PostgreSQL异同点

2021-08-01-MySQL和PostgreSQL异同点 database MySQL [[PostgreSQL]] MySQL 和 PostgreSQL 异同点 MySQL 是一个非常常见的 RDBMS,同时在国内的市场占有率也非常的高。PostgreSQL 近些年来才在国内有了些起色。先不论这种现象是如何形成的,只谈两者间的差异。 由于数据库版本的更新,以下部分内容可能不太准确。仅从一些大的方面进行比较。 功能 MySQL PostgreSQL OLTP yes yes OLAP no yes 性能 弱 强 数据类型 少 多 时序数据 no yes 图数据 no yes 集群 yes,主从 yes,强一致 其他数据接入 弱 强 SQL特性支持 36 94 GPU加速 no yes 发版速度 慢 快 数据库命名 好点 差点 以上比较仅参考一些回答梳理而出,相比于 MySQL 我更乐意学习 PostgreSQL。 从技术层面来看,PostgreSQL 比 MySQL 更好用,性能更好,同时做了很多其他事,增加了学习成本;从工程实践来看,MySQL 足够流行,易于学习,只做自己份内的事。 参考 PostgreSQL 与 MySQL 相比,优势何在? MySQL已经可以干大部分事情了,还有必要使用商业数据库或者PostgreSQL吗? PostgreSQL具有更多的企业级数据库的特性,为什么国内开源数据库用MySQL的更加广泛?

January 16, 2026

2021-07-31-redis分布式锁

2021-07-31-redis分布式锁 database Redis Redis 分布式锁 锁指的是对某个资源的限制,分布式锁是相对于单机锁来的,一般来说程序内的加锁都是针对于单个进程的,但是在集群的条件下,业务实例会有多个,进程内的锁就无法适用,就出现了分布式锁概念。 分布式锁指的是:适用于分布式场景下的锁,而不是分布式实现的锁。本质上就是一个独立的工作点,可以被多个实例访问。分布式锁需要做到以下几点: 互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁 高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署 可重入(防止锁超时):如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁 独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了; Redis 是一种分布式锁方案,但不一定是最好的方案,它有以下几个好处: 核心单进程单线程,获得锁的过程是确定的,一个实例获得,另一个就无法获得 可重入性的实现,直接通过对key设定存活周期 高可用性,可以通过Redis集群的主从复制(但这里是一个坑点,主从复制不是强一致性,可能会丢锁) 独占性,通过对锁key进行随机赋值,只有加锁方才可以解锁(其实不讲规矩的话都可以删除该锁) 但它也有以下的缺点: 高可用性和一致性难以保证,Redis 集群不是强一致性的,容易丢失 key 可重入性和独占性的实现,需要业务代码的强力支撑,如果乱来的话,Redis 无法保证这两点 Redis 锁的使用 SETNX key value SETNX是『 SET if Not eXists』(如果不存在,则 SET)的简写,设置成功就返回1,否则返回0。 > SETNX lock1 1 (interger) 1 但是这样使用,锁就没法自动释放,假如加锁方服务挂掉了,那么就死锁了。 可以通过先设置 key,然后赋予存活周期,但这是两步操作,不符合原子性,容易出问题,所以该方法一般不用。 SETEX key seconds value > setex lock1 10 1 OK 这里确实可以解决上述方法的原子性问题,同时分布式锁也可重入,但它有个问题,就是没法保证独占性,任何实例都可以这么操作这个锁。我在某篇文章中看到了以下方案,但不太理解,暂且记录: setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。 通过业务上的逻辑实现独占性,但无法防止不规矩的代码生效。 ...

January 16, 2026

2021-07-28-数据库事务的四大原则以及常见问题

2021-07-28-数据库事务的四大原则以及常见问题 database [转载]数据库事务的四大原则以及隔离级别 转载信息 作者:Shi Peng 发布时间:2020-10-27 17:54:04 原始链接:数据库事务的ACID四大原则 学习笔记 四大原则 A 代表Atomicity,即原子性。表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交。事务中的任何一个数据库操作失败,已经执行的任何操作都必须被撤销,让数据库返回初始状态。 C 表示Consistency,即一致性。事务操作成功后,数据库所处的状态和他的业务规则是一致的,即数据不会被破坏。如A账户转账100元到B账户,不管操作成功与否,A和B账户的存款总额是不变的。数据库事务在实现了另外三个原则的情况下,一致性就已经实现了。业务的一致性需要开发者决定。 I 表示Isolation,即隔离性。在并发数据操作时,不同的事务拥有各自的数据空间,他们的操作不会对对方产生干扰。准确地说,并非要求做到完全无干扰。数据库规定了多种事务隔离界别,不同的隔离级别对应不用的干扰程度,隔离级别越高,数据一致性越好,但并发性越弱。 D 表示Durability,即持久性。一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中。即使在事务提交后,数据库马上崩溃,在数据库重启时,也必须保证能够通过某种机制恢复数据,例如事务日志恢复。 隔离不好容易造成的问题 脏读 脏读指一个事务读到了另一个事务的中间结果,还用转账举例: 当A给B转账的事务没有执行完,另一个事务就读取了它的中间结果,就有可能造成脏读。因为万一之前的事务回滚,那么新读取到的结果就是错的,和A账号回滚之后的余额就不一致了。 不可重复读 不可重复读的意思是说,在一个事务中,我们读取了某个数据两次。刚好在这两次中间,有另一个事务修改了这条数据,那么同样会引起数据错误,因为这两次读取到的结果不一致。 比如我们对A账户的一个事务还没有结束,这时它的结果就被另一个事务修改了,那么程序就会发生错乱,因为读到了它没有预料到的修改。 解决方法就是针对当前修改的数据进行隔离,同一时刻只允许一个事务对该条数据进行修改,以保证数据的一致性。 幻读 幻读就是一个事务读取两次,读到的数据条数不一致。这点和不可重复读非常类似,不过不同的是不可重复读是确定的某一条数据,而幻读是指整个数据库或者整个表而言。 要解决也很简单,因为幻读是其他事务修改其他数据产生的,所以要排除掉这种情况,只针对我们修改的数据进行加锁和隔离是不够的,我们需要将整个数据库,或者是分区进行隔离,同一时刻,只允许一个事务对一个分片或数据库表进行修改。 更新丢失 更新丢失的定义很直观,当我们修改一条数据的时候,另一个事务也在修改这条数据,导致后者的修改覆盖了前者修改的内容。 解决的办法是做好隔离操作,在一个事务写入完成之前,禁止其他事务写入。即更新丢失是在并发场景下出现的错误。 四种隔离级别 隔离级别 脏读 更新丢失 不可重复读 幻读 并发模型 更新冲突检测 未提交读 Read uncommited yes yes yes yes 悲观 no 已提交读 Read commited no yes yes yes 悲观 no 可重复读 Repeatable Read no no no yes 悲观 no 可串行读 Serializable no no no no 悲观 no 从上到下,对应四种隔离级别,越往下隔离级别越高,能够解决的隔离性问题也就越多,同样的,用到的锁也就越多,系统的性能也就越差。 ...

January 16, 2026