一、先搞懂:什么是乐观锁?

很多人可能没听过这个名字,但换个场景就明白了:比如你在网上抢限量款奶茶的最后一杯,你点开商品页确认有库存,正准备下单,突然看到页面提示“该商品已被抢光”,这就是真实世界里的抢资源冲突。而数据库里的乐观锁,就是专门解决这种“多人同时改一个数据”的工具。 简单说,乐观锁的核心逻辑是:我默认大家不会随便改同一个数据,所以我在操作之前不锁任何东西,只在要改数据的时候,确认一下“我之前看到的这个数据有没有被别人改过”——就像你下单前,需要确认页面上的库存数字和你刚看到的一样,要是不一样,说明已经被别人抢了,你得重新看库存再操作。

1.1 乐观锁的关键:版本号

怎么确认数据没被改过?靠的是一个“版本号”字段。就像你手上的奶茶商品页有个小标记,每次有人改了库存,这个标记就会自动加1。你读数据的时候记下这个标记,改数据的时候,必须带这个标记一起改,要是数据库里的标记和你记的不一样,说明被别人抢了,这就是冲突了。

二、为什么会出现写冲突?

写冲突说白了就是“两个人同时改了同一个数据,最后谁的修改没生效”,最常见的原因就是——选错了数据库的隔离级别。 举个生活化的例子:你和同事同时拿了一份相同的周报草稿,你们俩都改了“本周完成项”,然后同时存盘,最后只会有一个人的修改生效,另一个人的就丢了,这就是写冲突。而如果数据库的隔离级别选得不对,乐观锁就起不到作用,相当于你记的版本号没用,不管别人改没改,你都能改成功,自然会丢数据。

2.1 举个真实的小例子

比如电商平台的一件商品,库存是10,两个用户同时来买,都读了库存是10,然后都减1,最后如果没处理好,库存会变成9,而不是正确的8,这就是典型的写冲突。要是这时候隔离级别选成“只能看到已经提交的修改”,就容易出问题,因为两个事务都读了初始的库存,根本不知道对方在改。

三、Snowflake里怎么实现乐观锁?

Snowflake是云数据仓库,本身就支持版本号的操作,不需要额外的插件,我们用Snowflake SQL就能实现,整个逻辑很简单,只要记住“读的时候记版本,改的时候带版本查”就行。 下面是完整的示例,技术栈只用Snowflake SQL,没有其他东西,所有注释都写清楚:

-- 技术栈:Snowflake SQL
-- 1. 先建一个商品表,加上version字段当版本号
-- product_id是商品ID,stock是库存,version是乐观锁的版本标记
CREATE TABLE product (
    product_id INT,
    stock INT,
    version INT DEFAULT 1 -- 初始版本号是1,每次更新加1
);

-- 2. 插入测试数据:商品1的库存是10
INSERT INTO product (product_id, stock) VALUES (1, 10);

-- 3. 模拟用户A的下单流程(事务开始)
BEGIN TRANSACTION;
-- 用户A读商品1的库存和版本,这时候拿到的是stock=10,version=1
SELECT stock, version FROM product WHERE product_id = 1;

-- 模拟用户B几乎同时做同样的操作,用户B的事务先提交了
BEGIN TRANSACTION;
SELECT stock, version FROM product WHERE product_id =1; -- 也拿到stock=10,version=1
-- 用户B减库存,版本加1,提交
UPDATE product SET stock = stock -1, version = version +1 WHERE product_id=1;
COMMIT; -- 现在商品1的stock是9,version变成2了

-- 用户A继续操作,要减1,这时候要带之前读的version=1来更新
UPDATE product 
SET stock = stock -1, version = version +1 
WHERE product_id =1 AND version =1; -- 这里的条件里必须加上version=之前读的1

-- 这个UPDATE的结果很重要:如果影响的行数是0,说明冲突了!
-- 因为现在数据库里的version是2,不是1,所以这个更新没生效,需要重试或者提示用户
COMMIT;

这个示例里,用户A的更新因为条件里的version不对,所以影响行数是0,开发者就能知道冲突了,不用盲目的提交,这就是乐观锁的作用。

3.1 乐观锁的核心细节

刚才的示例里,最容易错的地方就是:

  1. 必须在update的where条件里加上版本号,不能少;
  2. 必须在事务开始的时候就把版本号读出来,不能中途读;
  3. 冲突了不能直接忽略,必须让用户重试,或者给出提示,不然数据还是不对。

四、隔离级别选错了会咋样?

刚才说的写冲突,大部分情况都是隔离级别选得不对。Snowflake里的隔离级别有几种,我们不用记太复杂的名字,只要记住: 如果选“读提交”(就是只能看到别人已经提交的修改),那两个事务读的都是初始数据,根本不知道对方在改,就容易出冲突; 如果选“可重复读”(整个事务里看到的都是同一个时间的数据),那事务里读的版本号不会变,update的时候就能校验到有没有被改过,这样乐观锁就生效了。 简单说:乐观锁必须配合“可重复读”或者“快照隔离”的级别,不然选成“读提交”,乐观锁的校验就没用,相当于白搭。

4.1 选错隔离级别的坑

比如刚才的用户A和用户B的例子,要是隔离级别是“读提交”,用户A在update的时候,虽然version变了,但可能因为隔离级别的问题,数据库允许更新,最后库存会变成8?不对,实际是如果隔离级别是读提交,两个事务都读了初始的库存,都减1,最后可能变成9,这就是丢了一个更新。而用可重复读的话,用户A读的版本是1,用户B改了之后,版本变成2,用户A的update条件里version是1,所以更新失败,不会出现这个问题。

五、乐观锁的应用场景和优缺点

不是所有场景都适合用乐观锁,得先搞清楚它的适用范围。

5.1 适合的应用场景

最常见的就是低冲突的修改场景:

  1. 电商的常规商品库存扣减(比如日常商品,每分钟只有几个用户抢);
  2. 多人同时修改的文档(比如在线笔记,每次修改都加版本号,避免覆盖);
  3. 优惠券的领取(避免同一优惠券被多人同时领取); 这些场景里,冲突的概率很低,就算有冲突,重试一两次就好,不会影响体验。

5.2 优点

  1. 不用加锁,性能高:悲观锁是直接锁数据,会影响其他用户操作,而乐观锁不用锁,读的时候完全不影响别人;
  2. 开发简单:Snowflake原生支持,只要加个version字段,写update的时候带条件就行;
  3. 数据一致性有保障:只要处理好重试,就能避免写冲突,不会出现数据错误。

5.3 缺点

  1. 冲突太多会影响性能:要是每秒有几百上千人抢同一个商品,冲突概率很高,每次都要重试,会浪费资源;
  2. 不适合高冲突场景:比如秒杀,每秒几十万请求,乐观锁的重试会让系统压力变大,这时候用悲观锁更合适;
  3. 需要处理重试逻辑:不能直接把冲突的情况返回给用户,得提示“操作太火爆,请重试”,开发的时候要加这个逻辑,不然体验不好。

六、注意事项

用乐观锁的时候,有几个必须注意的点,不然容易踩坑:

  1. 版本号必须每次更新都加1,不能漏,也不能乱改;
  2. 必须在事务里先读版本号,再改数据,不能分开操作,不然中间会被别人改;
  3. 隔离级别一定要选“可重复读”或者快照,不能用“读提交”,不然乐观锁的校验失效;
  4. 冲突了一定要重试,最多重试3-5次,不能无限重试,不然会死循环;
  5. 不要用乐观锁做高冲突的场景,比如秒杀,换悲观锁或者队列处理更合适。

七、总结

其实乐观锁的核心就是“提前预防,事后校验”,不用一开始就锁数据,只在最后改的时候确认数据没被改过,适合低冲突的场景。而写冲突的最主要原因就是隔离级别选错,必须配合正确的隔离级别,才能让乐观锁生效。对于开发者来说,不用记太多专业术语,只要记住“版本号”和“隔离级别”这两个关键点,就能用乐观锁解决大部分写冲突的问题,尤其是在Snowflake里,实现起来很简单,只要按照示例来,就能避免很多数据异常的问题。