两笔交易并发写入,数据能否唯一?我先解释一下标题的意思。我们假设有这样一个用户注册场景。用户并发请求注册新用户。你有一个数据库表,就是下面的用户表。产品经理要求用户之间的电话号码不能重复,为了保证这一点。我们想到先查数据库,再做判断。如果存在,我们就退出,否则就插入一条数据。伪代码如下。选择phone_no=2的用户;//查询sqlif(userexists){return}else{insertuser;//insertsql}复制代码但是这是两条sql语句,先执行querysql,再决定是否执行insertsql。每次用户注册都会执行这样一段逻辑。那么,如果此时有多个用户在进行操作,这个逻辑就会并发执行。如果都是并发执行,执行完第一条SQL语句后,会发现用户不存在。此时进行插入,这样就有了两个相同的数据。于是,有人认为这两条SQL语句的逻辑应该是一个整体,不应该被拆散,于是就想到了事务,通过事务,将两条SQL语句作为一个整体,要么一起执行,要么滚动后退。这就是数据库ACID中的A(Atomicity),原子性的完美体现。伪代码看起来像这样。begin;selectuserwherephone_no=2;//查询sqlif(userexists){return}else{insertuser;//插入sql}commit;copycode那么问题来了,这个逻辑,并发执行,能保证数据的唯一性吗?当然不是。一个事务中的多条SQL语句确实是原子的,它们要么一起成功,要么一起失败。这是事实,但与此场景无关。事务是并发执行的。第一个事务执行查询用户,不会阻塞另一个事务查询用户,所以有可能发现用户不存在。这时候两个事务逻辑都判断用户不存在,然后插入到数据库中。事务中的两条sql语句都执行成功,所以插入了两条相同的数据。如何保证数据的唯一性?那我们就来说说在上面的场景下如何保证插入的数据是唯一的。方法有很多,但是今天我们只讨论mysql内部的做法,不考虑其他外部中间件(比如redis分布式锁)。唯一索引通过下面的命令,可以给数据库用户表的phone_no字段添加一个唯一索引。ALTERTABLEuserADDunique(phone_no);复制代码当我们进行写操作时,比如下面这句话,INSERTINTOuser(user_name,phone_no)VALUES('Xiaohong',2);copy代码第一次插入会成功,第二次插入会报错。Duplicateentry'2'forkey'phone_no'重复代码表示字段phone_no是唯一的,两次添加phone_no=2将导致重复。于是回到我们文章开头的场景,重复插入的问题就完美解决了。那么问题来了。为什么唯一索引可以保证数据的唯一性呢?让我们看看一个写操作,会发生什么。首先,mysql作为数据库,内部主要分为两层,一层是server层,一层是存储引擎层(通常是innodb)。服务器层主要负责数据库连接、权限校验、SQL语句校验和优化。查询和更新数据的真正操作是在请求到达存储引擎层时。大家都知道数据库是一个持久化存储,数据最终是存储在磁盘上的。数据库读写是直接读写磁盘数据吗?不行,如果直接读写磁盘,太慢了,为了提高速度。它在磁盘前面加了一层内存,叫做缓冲池。里面有很多细节,但是最重要的是一个双向链表,里面有数据页。每个数据页默认大小为16kb,数据页包含磁盘数据。所以有了这层缓冲池内存,mysql的读写操作就可以先操作这部分内存。如果你要读写的数据页不在缓冲池中,你可以去磁盘中获取。因为读写内存的速度要比读写磁盘快很多。所以引擎的读写速度要快得多。但这还不够。在很多写操作中,我的诉求是将xx更新为xx,或者插入xx。数据库知道这一点就足够了。我不需要知道数据页是什么样的。有点抽象?让我举一个例子。比如我想把id=1的数据的phone_no字段更新为100,数据库知道这个就够了。至于这个数据原来的phone_no是等于20还是30,根本无所谓,反正最终会变成我想要的phone_no=100。换句话说,如果有这么一块内存,记录下我要把数据改成什么,然后慢慢异步更新数据到磁盘。那我连最开始的这段数据都不需要从磁盘读到缓冲池里了。按照这个思路,changebuffer来了。因此,在用普通索引写入数据时,只需要将它要写入的内容写入到changebuffer中,它就会立即结束并返回。之后innodb引擎取changebuffer,将磁盘数据异步读入内存,修改changebuffer中的数据到数据页,再写回磁盘。速度将以秒为单位增加。但是这个changebuffer如果放在uniqueindex里面就没用了。毕竟要保证真的只有一条数据,所以你得去查数据库,看是否真的有这条数据。所以,对于insert场景,普通索引是把需求丢到changebuffer中返回,而unique索引需要实际从磁盘读取数据到内存,看是否有重复,如果有重复再插入数据没有重复项。这个唯一索引在性能方面损失很大。那么回到唯一索引为什么能保证数据的唯一性这个问题。总之,唯一索引会绕过changebuffer,保证磁盘数据读入内存后再判断数据是否存在。只有当数据不存在时,才能插入数据。否则会报错。这样可以确保数据是唯一的。总结添加唯一索引可以保证数据在并发写入的时候是唯一的,最省事。数据库通过引入一层缓冲池内存来提高读写速度,普通索引可以使用changebuffer来提高数据插入的性能。唯一索引会绕过changebuffer,确保数据从磁盘读入内存后,再判断数据是否存在。只有当数据不存在时,才能插入数据。否则会报错,保证数据的唯一性。让我留个问题给你。如前所述,在InnoDB中,更改缓冲区用于加速普通索引。是否存在更改缓冲区不仅不能加速普通索引,而且还会产生负面影响的场景?最后,大家不要笑。文章开头提到的通过开启事务来保证数据唯一性的错误其实很容易犯,这样的事情我遇到过不止一次。做这个手术的人会发誓,肯定地表达自己的理解。在我解释了几次都没有结果后,我选择低头假装思考,然后说:“你说得对,我回去考虑一下”,然后默默的给数据加了一个唯一索引表……相信对方一定是看懂了。那一刻,我觉得我写的不是代码,而是人的经验。如果文章对你有帮助,欢迎……算了。废话不多说,一起畅游在知识的海洋里
