【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 本文通过电商场景中的库存超卖问题,深入探讨了JVM锁、MySQL悲观锁和乐观锁的实现及其局限性。首先介绍了单次访问下库存扣减逻辑的正常运行,但在高并发场景下出现了超卖问题。接着分析了JVM锁在多例模式、事务模式和集群模式下的失效情况,并提出了使用数据库锁机制(如悲观锁和乐观锁)来解决并发问题。悲观锁通过`update`语句或`select for update`实现,能有效防止超卖,但存在锁范围过大、性能差等问题。乐观锁则通过版本号或时间戳实现,适合读多写少的场景,但也面临高并发写操作性能低和ABA问题。最终,文章强调没有完美的方案,只有根据具体业务场景选择合适的锁机制。

引言

在电商业务中,库存超卖问题就如同一颗定时炸弹,随时可能在高并发的环境下引爆。对于后端工程师来说,就需要为这颗炸弹加上防止爆炸的保险,从而避免因为超卖导致的资损问题。本系列文章就将从这个场景入手,一步步地为各位读者引入分布式锁的各种实现,从而让大家可以掌握分布式锁在常见场景的使用。

需求背景

背景非常简单,就是在电商项目中,用户购买商品和数量后后,系统会对商品的库存进行相应数量的扣减。因此,我们模拟这个场景就需要商品表库存表两张表,但业务并不是这里的重点,需要简化一下,一张简单的商品库存表足以,如下:

CREATE TABLE `tb_goods_stock`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `goods_id` bigint(20) NOT NULL COMMENT '商品id',
  `stock` int NOT NULL COMMENT '库存数',
  PRIMARY KEY (`id`)
) COMMENT = '商品库存表';
AI 代码解读

接着,我们创建一个SpringBoot的项目,在接口中实现简单的扣减库存的逻辑,示例如下:

public String reductStock(Long goodsId,Integer count){
   
    //1.查询商品库存的库存数量
    Integer stock = stockDao.selectStockByGoodsId(goodsId);
    //2.判断商品的库存数量是否足够
    if (stock < count) return "库存不足";
    //3.如果足够,扣减库存数量
    stockDao.updateStockByGoodsId(goodsId,stock-count);
    //4.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

创建成功后,先往数据库里插入一条商品id为1、库存为1的数据,便于我们测试接口的逻辑。分别执行两次调用,分别得到库存不足库存扣减成功的提示,验证逻辑没有问题,如下:
1.png

2.png

发现问题

上面的例子如果是通过单次访问,那么它的执行结果也是符合我们预期的。但在高并发场景下,多个线程同时访问同一个数据就可能出现超卖问题。因此,我们用JMeter来模拟大量并发数据来进行线上抢购场景复现,如下:
3.png
添加一个线程组,设定50个线程和100次循环次数,如下:
4.png
这时再将数据库里的商品id为1的数据的库存修改为5000,如下:
5.png
接着执行HTTP请求,如下:
6.png
通过聚合报告可以看出5000次请求都执行成功,这个时候按照正常逻辑,库存应该扣完了,回到数据库查询,如下:
7.png
通过查询发现还有4000多个库存,带换到线上场景,这个时候后续还有用户继续请求购买,最终实际卖出的肯定会远远超过库存,这就是经典的超卖问题

JVM锁初显神通

并发问题去找锁这个几乎是大家的共识,那么这里的超卖问题也不例外。因此,最直接的办法就是直接在涉及扣减库存的逻辑或操作上进行加锁处理。首先,最先想到的就是JVM锁,只需要一个synchronized关键字就可以实现,代码修改如下:

public synchronized String reductStock(Long goodsId,Integer count){
   
    //1.查询商品库存的库存数量
    Integer stock = stockDao.selectStockByGoodsId(goodsId);
    //2.判断商品的库存数量是否足够
    if (stock < count) return "库存不足";
    //3.如果足够,扣减库存数量
    stockDao.updateStockByGoodsId(goodsId,stock-count);
    //4.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

我们这时候去把数据库的库存还原下,然后重新用JMeter进行请求(Ps:原参数不变),执行后我们先看数据库结果,如下:
8.png
可以看到这次的库存就被扣减完了,但我们查看聚合报告会发现对比前面的请求,有一项指标下降了很多-吞吐量,从三千多到现在的一千多,所以加锁肯定对性能是会产生影响的,如下:
9.png
当然除了synchronized关键字,还有更为灵活的方式,毕竟它是作用在方法上的,而我们使用reentrantLock则可以实现对代码块进行加锁,如下:

ReentrantLock reentrantLock = new ReentrantLock();

public String reductStock(Long goodsId,Integer count){
   
    //1.加锁
    reentrantLock.lock();
    try {
   
        //2.查询商品库存的库存数量
        Integer stock = stockDao.selectStockByGoodsId(goodsId);
        //3.判断商品的库存数量是否足够
        if (stock < count) return "库存不足";
        //4.如果足够,扣减库存数量
        stockDao.updateStockByGoodsId(goodsId,stock-count);
    } finally {
   
        //5.解锁
        reentrantLock.unlock();
    }
    //6.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

JVM锁是万能的吗?

经过了上面的简单改造就让我们的扣减库存不失效了,那么是否这样就可以真正地解决线上的超卖问题呢?当然不是的,JVM锁并不是万能的,它在部分场景下是会失效的,如下:

1.多例模式

首先,我们都知道Spring默认是单例的,即每个对象都会被注册成为一个bean交给IOC容器进行管理。但是它是可以设置成多例的,只需要一个简单的注解,如下:

@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Service
public class StockService {

    @Autowired
    private StockDao stockDao;

    public synchronized String reductStock(Long goodsId,Integer count){
        //1.查询商品库存的库存数量
        Integer stock = stockDao.selectStockByGoodsId(goodsId);
        //2.判断商品的库存数量是否足够
        if (stock < count) return "库存不足";
        //3.如果足够,扣减库存数量
        stockDao.updateStockByGoodsId(goodsId,stock-count);
        //4.返回扣减成功
        return "库存扣减成功!";
    }
}
AI 代码解读

这个时候我们再次进行调用测试,结果如下:
10.png

可以看到超卖问题又重出江湖了。那么这是为什么呢?其实很好理解,多例模式下这个类对应的bean也可以有多个,也就是我们每次执行到这个方法都是一个新的bean,自然就根本没有锁住。

2.事务模式

事务模式就是在方法上加上事务注解(Ps:这里测试记得把上面的多例注解注释掉),代码如下:

@Transactional
public synchronized String reductStock(Long goodsId,Integer count){
   
    //1.查询商品库存的库存数量
    Integer stock = stockDao.selectStockByGoodsId(goodsId);
    //2.判断商品的库存数量是否足够
    if (stock < count) return "库存不足";
    //3.如果足够,扣减库存数量
    stockDao.updateStockByGoodsId(goodsId,stock-count);
    //4.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

再次进行调用测试,结果如下:
11.png

可以看到依然会有剩余库存,那么为什么加上事务就破坏了JVM锁呢?其实也很好理解:我们看代码,在扣减库存的方法上我们加了事务,方法内部加了锁,可以理解成事务包着锁。那么当请求A执行到扣减库存的方法后,会先进入事务,然后加锁->执行业务逻辑->解锁。这里需要注意的是,一旦解锁之后,请求B就会马上抢夺锁,所以这个时候就出现了旧请求还没提交事务,新请求就拿到锁开始执行了。在读已提交这个默认的隔离级别下,就可能出现新旧请求扣减了同一份库存,自然超卖问题就又出现了。那么是否有解决办法呢?答案是肯定的。这里我们分析了失效的原因,那么其实只要把锁加到事务外,确保事务提交了才释放锁就行。比如按照我们现有的例子,把synchronized关键字加到controller层就行了,这里很简单就不演示了,感兴趣的读者可以自行测试。

3.集群模式

集群模式则是最常见的情况,毕竟应该不会有生产级别的服务只部署一个实例,几乎都是部署多实例的。那么这个时候JVM锁自然就失效了,如下:
12.png
在这个例子中,外部的请求进入到nginx,通过负载均衡策略转发到库存服务,JVM锁只在所在的JVM内部失效,所以这里加的JVM锁其实是3个服务各加了一把锁,那各自锁各自的等于没锁,超卖问题自然就又出现了。

解决JVM锁失效后的并发问题

上文中提到了3种JVM锁失效的场景,那么就需要想出新的策略来应对并发问题,那么让我们把目光投向MysQL,它天然就带有表锁、行锁、间隙锁等,那么我们可以利用这些性质来实现我们业务上的加解锁。这种利用数据库锁机制并且假设数据会冲突在操作前加锁的思想,我们称为悲观锁。它的实现方式主要有以下两种:

悲观锁 - 单条update语句实现

首先,让我们回到扣减库存的业务逻辑,如下:

public String reductStock(Long goodsId,Integer count){
   
    //1.查询商品库存的库存数量
    Integer stock = stockDao.selectStockByGoodsId(goodsId);
    //2.判断商品的库存数量是否足够
    if (stock < count) return "库存不足";
    //3.如果足够,扣减库存数量
    stockDao.updateStockByGoodsId(goodsId,stock-count);
    //4.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

先查询现在的库存数量,然后判断库存是否足够,如果足够再扣减。那么这三步操作我们其实可以合成一步SQL来执行,这是原本的扣减库存的SQL语句,如下:

@Update("update tb_goods_stock set stock= #{count} where goods_id= #{goodsId}")
Integer updateStockByGoodsId(@Param("goodsId") Long goodsId, @Param("count") Integer count);
AI 代码解读

让我们进行一个迭代,直接在SQL进行扣减和判断操作,如下:

@Update("update tb_goods_stock set stock= stock - #{count} where goods_id= #{goodsId} and stock >= #{count}")
Integer updateStockByGoodsId(@Param("goodsId") Long goodsId, @Param("count") Integer count);
AI 代码解读

然后回到service那里同步修改,如下:

public String reductStock(Long goodsId,Integer count){
   
    //1.扣减库存数量
    Integer result = stockDao.updateStockByGoodsId(goodsId, count);
    //2.如果数量大于0,则扣减成功
    if (result > 0){
   
        return "库存扣减成功!";
    }
    //3.返回扣减失败
    return "库存扣减失败!";
}
AI 代码解读

接着我们用JMeter再次进行测试,最终库存按照预期归零了,如下:
13.png

那么这种悲观锁-单条update语句的方式是否就很完美了呢?当然不是,它其实也存在一些问题:

1.易造成锁范围过大

范围过大怎么理解呢,我们在MySQL客户端里进行测试,首先插入id = 1和2的两条商品库存数据,如下:
14.png

然后我们写下update语句,如下:

BEGIN;

UPDATE tb_goods_stock SET stock = stock - 1 WHERE id = 1;

SELECT * FROM tb_goods_stock;

COMMIT;
AI 代码解读

然后逐行执行,但执行到查询后先不提交,这个时候执行这条sql查询数据库的加锁情况,如下:

select * from performance_schema.data_locks;
AI 代码解读

然后得到如下结果:
15.png

于是我们可以分析出来,当前的这条update语句会把每条tb_goods_stock表上每条数据都锁起来,虽然锁类型都是行锁,但实际上每行都锁其实已经是表锁了。在我们这个例子中,就是用户购买id = 1的商品,但所有商品库存都被锁住了,一个用户买东西,所有用户都得排队等,这个性能只能说相当感人了。那么这个问题有解决办法吗?当然是有的,我们观察下index_name字段,发现它的值都是主键id,因为我们的商品id并没有建立索引,所以这里锁的时候就会根据主键将全表锁住了。既然知道问题出在哪里了,那么解决办法也很简单,给商品id加个索引就行,加好索引之后我们重新开启事务执行update语句,再来查锁信息,如下:
16.png

这个时候看到id=2的lock_mode发生了变化,多了一个GAP,它表示间隙锁(Ps:它的意思是你在1和2之间插入一条大于1小于2的数据是插入不进去的)。

2.无法在程序中获取扣减库存之前的值

这个就很好理解了,原本在代码中拆了三段逻辑执行,在扣减前会先获取,自然就有记录。现在全部一条SQL执行了,在应用层面是没有旧库存了。

3.很多场景下无法满足业务诉求

我们这里的案例业务逻辑十分简单,一条SQL就搞定了,那么在实际场景中,还可能涉及到拆单、合单等之类的操作,那么这个时候是需要我们在代码中处理业务逻辑的,显然单靠一条update语句就无法满足需求了。

悲观锁 - for update语句实现

那么为了解决上述的后两个问题,我们可以使用悲观锁的另一种方式。只需要在查询语句后加个for update,如下:

@Select("select stock from tb_goods_stock where goods_id= #{goodsId} for update")
Integer selectStockByGoodsIdForUpdate(@Param("goodsId") Long goodsId);
AI 代码解读

它的作用是在查询的时候加锁,和前面的update语句一样会加行锁,当然,如果你没有建索引,它会建表锁。注意的是:这里的锁是依靠mysql的锁机制实现的,所以当你的事务没提交的时候,当前的连接就会一直持有锁,所以需要我们在方法上加上事务注解,保证逻辑执行完成后自动提交事务,如下:

@Transactional(rollbackFor = Exception.class)
public String reductStock(Long goodsId,Integer count){
   
    //1.查询商品库存数量
    Integer stock = stockDao.selectStockByGoodsIdForUpdate(goodsId);
    //2.判断库存数量是否足够
    if (stock < count){
   
        return "库存不足!";
    }
    //3.如果库存足够,扣减库存
    stockDao.updateStockByGoodsId(goodsId, count);
    //3.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

它的优势也很明显,解决了上述的两个问题:无法在程序中获取扣减库存之前的值和很多场景下无法满足业务诉求。那么,它的问题是什么呢?

1.易造成锁范围过大

这个很好理解,毕竟它本身的实现和单条update语句一样,所以自然也会存在相同的这个问题。

2.性能较差

长时间锁定以及频繁的加锁和解锁操作都会成为性能的瓶颈点。

3.死锁问题

其实这个问题,单条update语句也可能出现,主要和加锁顺序有关。比如现在两个客户端A和B同时请求,客户端A里我们先给商品id=1的加锁,客户端B则先给商品id=2的加锁,接着A再给商品=2的加锁,B则给商品id=1的加锁,这个时候就形成了死锁。

4.select for update和普通select语句读取内容不一致

在默认的隔离级别(即读已提交)下,假如客户端A开启了事务,并做了扣减库存,这个时候还未提交事务,客户端B这个时候使用select语句读取到的值就是扣减前的,但是如果客户端B使用的是select for update来读取,读到的就是扣减后的值,因为它是当前读,即数据的真实值而不受事务影响。那么如果在业务中,有的地方使用select for update,有的地方使用select,而且需要对读取到的值做业务处理,这样处处不一致就可能导致数据问题。

乐观锁-版本号

有悲观锁自然也有乐观锁,和悲观锁相反,它是假设每次去拿数据别人都不会修改,所以不会上锁,只在更新的时候判断一下别人有没有更新这个数据。虽然叫乐观锁,但它其实更像是一种设计思想,先来介绍一下它的一种实现-版本号

1.给指定表增加一个字段version

ALTER TABLE `tb_goods_stock` 
ADD COLUMN `version` int NULL DEFAULT 0 COMMENT '版本号' AFTER `stock`;
AI 代码解读

17.png

2.读取数据的时候将version字段一起读出

@Select("select id,stock,version from tb_goods_stock where goods_id= #{goodsId}")
List<GoodsStockEntity> selectStockAndVersionByGoodsId(@Param("goodsId") Long goodsId);
AI 代码解读

3.数据每更新一次,version字段加1

@Select("update tb_goods_stock set stock= #{count}, version=#{version} + 1 where goods_id= #{goodsId} and version = #{version}")
Integer updateStockAndVersionByGoodsIdAndVersion(@Param("goodsId") Long goodsId, @Param("count") Integer count,@Param("version") Integer version);
AI 代码解读

4.提交更新的时候,判断库中的version字段和前面读出来的进行比较

//1.查询商品库存数量 + version
List<GoodsStockEntity> goodsStockEntities = stockDao.selectStockAndVersionByGoodsId(goodsId);
//2.判空
if (goodsStockEntities.isEmpty()) {
   
    return "商品不存在!";
}
//3.存在则取出
GoodsStockEntity goodsStockEntity = goodsStockEntities.get(0);
//4.判断库存数量是否足够
if (goodsStockEntity.getStock() < count) {
   
    return "库存不足!";
}
//5.如果库存足够,扣减库存
result = stockDao.updateStockAndVersionByGoodsIdAndVersion(goodsId,
        goodsStockEntity.getStock() - count, goodsStockEntity.getVersion());
AI 代码解读

5.相同更新,不相同重试

public String reductStock(Long goodsId,Integer count) {
   
    //1.声明修改标志变量
    Integer result = 0;
    while (result == 0) {
   
        //1.查询商品库存数量 + version
        List<GoodsStockEntity> goodsStockEntities = stockDao.selectStockAndVersionByGoodsId(goodsId);
        //2.判空
        if (goodsStockEntities.isEmpty()) {
   
            return "商品不存在!";
        }
        //3.存在则取出
        GoodsStockEntity goodsStockEntity = goodsStockEntities.get(0);
        //4.判断库存数量是否足够
        if (goodsStockEntity.getStock() < count) {
   
            return "库存不足!";
        }
        //5.如果库存足够,扣减库存
        result = stockDao.updateStockAndVersionByGoodsIdAndVersion(goodsId,
                goodsStockEntity.getStock() - count, goodsStockEntity.getVersion());
    }
    //6.返回扣减成功
    return "库存扣减成功!";
}
AI 代码解读

修改完成后,我们再次进行测试,可以看到库存扣减为0,版本号也加到了5000,如下:
18.png
19.png

乐观锁-时间戳

通过版本号机制,我们成功解决了扣减库存的问题,接下来看下乐观锁的另一种实现-时间戳。它的实现方式和版本号类似,这里我们就不演示了,给大家说明下步骤:

  • 1.给表增加timestamp字段;
  • 2.读取数据的时候,将timestamp字段一起读出;
  • 3.数据每更新一次,timestamp取当前时间戳;
  • 4.提交更新时,判断库中的timestamp字段值和之前取出来的timestamp进行比较;
  • 5.相同更新,不相同重试。

乐观锁问题

看了乐观锁的实现,在前文中我们分析了悲观锁存在的问题,那么同样地,接下来我们聊聊乐观锁存在的问题。

1.高并发写操作性能低

因为我们存在重试机制,那么在高并发场景下,存在多个请求不断重试,每个请求的读也都需要和数据库进行IO,所以它更适合读多写少的场景。

2.存在ABA问题

这是一个乐观锁的常见问题,虽然在我们上面的例子中并不会发生,因为它只涉及到减库存,可能有的同学会困惑为什么呢?我来举个例子:假设目前业务还存在取消订单,需要对版本号做减一。那么假设此时有三个线程同时进入,线程A减库存,版本号加一;线程B加库存,版本号减一;线程C也是减库存,但是线程B把版本号减一,把A加的又减回去了,就导致C拿到的和读到的一样了,于是C也减库存了。这个时候A和C扣了一次,但实际消费了两次,超卖问题就又出现了。

小结

本篇文章通过超卖问题引入了JVM锁、MySQL悲观锁和乐观锁,并对每种锁的实现和局限都做了讲解,其实是想在开篇就告诉各位读者没有完美的方案,只有更好的方案。在我们后续的学习中你也会看到不断地肯定与否定,主要的目的是希望各位读者在学习后可以根据自己的业务场景选择合适的方案!

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
打赏
0
13
12
0
158
分享
相关文章
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
536 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
1月前
|
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
108 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
1月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
91 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
5天前
|
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
26 0
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。
145 6
【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性
|
1月前
|
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
74 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
【📕分布式锁通关指南 05】通过redisson实现分布式锁
本文介绍了如何使用Redisson框架在SpringBoot中实现分布式锁,简化了之前通过Redis手动实现分布式锁的复杂性和不完美之处。Redisson作为Redis的高性能客户端,封装了多种锁的实现,使得开发者只需关注业务逻辑。文中详细展示了引入依赖、配置Redisson客户端、实现扣减库存功能的代码示例,并通过JMeter压测验证了其正确性。后续篇章将深入解析Redisson锁实现的源码。
59 0
【📕分布式锁通关指南 05】通过redisson实现分布式锁
|
2月前
|
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
91 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
112 0
分布式爬虫框架Scrapy-Redis实战指南
|
2月前
|
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
225 83

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等