14. Redisson 分布式锁

2023年 9月 28日 49.1k 0

Spring Cloud 微服务系列文章,点击上方合集↑

1. 开头

在单体应用中,我们可以用Java的synchronizedlock来使用锁,但在微服务的场景下,一个应用会部署多个实例,就需要保证多个实例的多个线程同时只能有一个线程来操作资源,那就需要分布式锁,

Redisson分布式锁的基本原理是通过Redis的setnx命令实现的。当一个进程需要获取锁时,通过调用Redis的setnx命令,在Redis中创建一个key表示锁的名称,如果成功地创建了这个
key,则表示获取锁成功,可以执行相应的业务逻辑。如果创建key失败,则表示锁已经被其他进程获取了,当前进程需要等待直到获取到锁为止。当执行完业务逻辑后,需要释放锁,即通过Redisdel命令删除锁的key,这样其他进程就可以获取到锁了。

举个通俗的栗子:你上厕所的时候推一下门看看里面有没有人(尝试获取锁),里面有人你需要在门口等着(等待锁),当他上完后开门了(释放锁),你进去的时候把门关上了(上锁),你执行完脱裤子、拉屎、提裤子等操作后把门打开了(释放锁)。

本文我们将通过抢购茅台的例子来进行演示。

抢购茅台分为三个步骤:

  • 判断库存是否充足
  • 创建新订单
  • 扣减库存。
  • 2. 安装运行Redis

    Redisson是基于Redis的,我们需要先下载运行Redis

    Redis是内存数据库,Redisson = Redis + son(儿子)。

    2.1 Windows

    • github下载地址:github.com/redis-windo…

    • github下载很慢,网盘下载(推荐):
      「Redis-7.0.8-Windows-x64.zip」来自UC网盘分享
      drive.uc.cn/s/4087d341f…

    启动Redis

    # 启动服务
    redis-server.exe redis.conf
    
    # 老版本的配置文件名称
    redis-server.exe redis.windows.conf
    
    • 需要指定redis.conf配置文件,可以在redis.conf文件中设置端口、密码等。

    进入Redis命令行界面

    # redis命令行界面
    redis-cli.exe
    
    # 设置键值对
    set mykey hello
    # 获取值
    get mykey
    

    2.2 Linux

    以CentOS为例,通过yum命令安装。

    # 安装
    sudo yum install redis
    
    # 启动
    sudo systemctl start redis
    
    # 进入redis命令行界面
    redis-cli
    # 设置键值对
    set mykey hello
    # 获取值
    get mykey
    

    3. 业务SQL脚本

    执行如下SQL脚本

    • 生成商品表product
    • 生成商品订单表product_order
    • 商品表插入一条记录,贵州茅台总数量100瓶
    CREATE TABLE `product` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `name` varchar(20) NOT NULL DEFAULT '' COMMENT '商品名称',
      `number` int(11) NOT NULL DEFAULT 0 COMMENT '商品数量',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    CREATE TABLE `product_order` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `product_id` bigint(20) DEFAULT NULL COMMENT '商品id',
      `number` int(11) DEFAULT NULL COMMENT '数量',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    INSERT INTO `product` VALUES (1, '贵州茅台', 100);
    

    4. SpringBoot 集成 Redisson

    4.1 pom.xml

    添加redisson-spring-boot-starter包依赖

    
        org.redisson
        redisson-spring-boot-starter
        3.16.8
    
    

    4.2 application.properties

    redis相关配置

    # redis 相关
    spring.redis.host=localhost
    spring.redis.port=6379
    

    4.3 buy()

    购买商品的方法

    • 判断库存数量是否充足
    • 创建订单
    • 扣减库存数量
    @Resource
    private RedissonClient redissonClient;
    
    @Override
    public Boolean buy(Long productId, Integer number) {
        RLock lock = redissonClient.getLock("lock_key_" + productId);
    
        try {
            boolean lockRes = lock.tryLock(5, TimeUnit.SECONDS);
            if (!lockRes) {
                throw new RuntimeException("获取锁失败~");
            }
    
            Product product = productService.getById(productId);
            log.info("库存数量:{}", product.getNumber());
            // ①判断库存是否充足
            if (product.getNumber() < number) {
                throw new RuntimeException("库存不足");
            }
    
            // ②创建订单
            ProductOrder order = new ProductOrder();
            order.setProductId(productId);
            order.setNumber(1);
            order.setCreateTime(LocalDateTime.now());
            save(order);
            log.info("创建订单:{}", order);
    
            // ③减库存
            product.setNumber(product.getNumber() - 1);
            productService.updateById(product);
            log.info("减库存:{}", product);
        } catch (InterruptedException e) {
            throw new RuntimeException("出现异常啦~");
        } finally {
            // 释放锁
            lock.unlock();
        }
    
        return true;
    }
    
    • "lock_key_" + productId这里对商品id加锁,同时只能有一个请求线程操作这个商品,其它请求线程必须等待。
    • 对商品id加锁表明每一种商品都是独立加锁的,就相当于你上厕所的时候是把那个坑位的门给关了,而不是把卫生间的门给关了。
    • lock.tryLock(5, TimeUnit.SECONDS),这里设置加锁的时间为5秒,如果当前请求线程5秒内还没有执行完操作就自动释放锁,让下一个线程来进行操作。

    4.4 加锁与不加锁分析

    不加锁会出现什么问题?

    假设库存还剩最后1瓶茅台,用户A和用户B同时发起购买1瓶茅台请求,用户A的请求线程判断库存充足,但还没有执行完创建订单和减库存操作(操作需要访问数据库,比较耗时)。此时用户B的请求线程判断库存数量为1,库存也充足,也进入了创建订单和减库存操作,最后创建了两个订单,库存减了两次。

    我们对商品加锁后,当用户A的请求线程执行判断库存、创建订单、减库存的购买操作过程中,用户B的请求线程需要等待用户A把这一系列操作做完释放了锁之后才能去执行。

    5. 测试

    接口地址:http://localhost:8100/product/buy

    用JMeter创建10个线程模拟多用户同时循环请求接口,当不加锁的情况下,如下是真实测试的结果:100瓶茅台产生了204个订单。

    当加锁后,100瓶茅台正常产生100个订单。

    JMeter的使用可以看前面Sentinel 流量控制和接口防护那一篇文章。

    6. 结语

    这里通过简单的抢购茅台的例子来演示锁,Redisson分布式锁可以用于单体或者微服务,它是借助redis中间件来加锁,如上内容我们可以创建多个微服务实例,然后调用接口结果也是一样的。

    记住:在限时抢购活动中,大量用户通过网络同时请求购买同一个商品,可能导致系统出现并发抢购的情况,从而会导致商品售罄或者超卖等问题。这种情况下一定要用锁,不管是单体应用还是微服务。

    Spring Cloud 微服务系列 完整的代码在仓库的sourcecode/spring-cloud-demo目录下。

    gitee(推荐):gitee.com/cunzaizhe/x…

    github:github.com/tigerleeli/…

    关注微信公众号:“小虎哥的技术博客”,让我们一起成为更优秀的程序员❤️!

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论