Redis 高并发分布式锁实战

目录

环境准备

[一 . Redis 安装](#一 . Redis 安装)

[二:Spring boot 项目准备](#二:Spring boot 项目准备)

[三:nginx 安装](#三:nginx 安装)

[四:Jmeter 下载和配置](#四:Jmeter 下载和配置)

案例实战

[优化一:加 synchronized 锁](#优化一:加 synchronized 锁)

[优化二:使用 redis 的 setnx 实现分布式锁](#优化二:使用 redis 的 setnx 实现分布式锁)

[优化三:使用 Lua 脚本 原子删除锁](#优化三:使用 Lua 脚本 原子删除锁)

[优化四:使用 Redission 实现分布式锁](#优化四:使用 Redission 实现分布式锁)


环境准备

一 . Redis 安装

  1. Redis 下载: https://github.com/tporadowski/redis/releases

2. 解压

3.进入到目录下的cmd,执行如下命令启动 redis

复制代码
redis-server.exe redis.windows.conf

二:Spring boot 项目准备

1. 引入 Maven 依赖

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>

    <groupId>com.xinxin</groupId>
    <artifactId>cyh</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.16.5</version>
        </dependency>
    </dependencies>

</project>

2. 修改 application.properties 配置文件

复制代码
spring.application.name=cyh
spring.redis.host=localhost
spring.redis.port=6379

3. 提供 web 服务 Controller

复制代码
@RestController
public class RedissionController {
    @Autowired
    private RedissonClient redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/add_stock")
    public String addStock() {
        stringRedisTemplate.opsForValue().set("good_stock", "60");
        return "ok";
    }


    @RequestMapping("/sub_stock")
    public String deductStock() {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        return "ok";
    }

}

4. 分别启动端口为8085和8086 端口的 Spring boot服务

三:nginx 安装

使用 nginx 来进行负载均衡,轮询调用 端口为8085和8086的服务,进行扣减库存。

项目架构图如下:

  1. nginx 下载地址:https://nginx.org/en/download.html

2. 解压

  1. 在conf 文件中修改 nginx.conf 配置文件

    #user nobody;
    worker_processes 1;

    #error_log logs/error.log;
    #error_log logs/error.log notice;
    #error_log logs/error.log info;

    #pid logs/nginx.pid;

    events {
    worker_connections 1024;
    }

    http {
    include mime.types;
    default_type application/octet-stream;

    复制代码
     #access_log  logs/access.log  main;
    
     sendfile        on;
     #tcp_nopush     on;
    
     #keepalive_timeout  0;
     keepalive_timeout  65;
    
     #gzip  on;
     upstream redislock{
         server 127.0.0.1:8085 weight=1;
         server 127.0.0.1:8086 weight=1;
     }
    
     server {
         listen       80;
         server_name  localhost;
    
         #charset koi8-r;
    
         #access_log  logs/host.access.log  main;
    
         location / {
             root   html;
             index  index.html index.htm;
             proxy_pass http://redislock;
         }
         error_page   500 502 503 504  /50x.html;
         location = /50x.html {
             root   html;
         }
     } 

    }

主要配置了 8085 和 8086 服务的负载均衡

  1. 启动nginx 服务,点击以下命令

nginx.exe

在任务管理中,看到nginx就代表启动成功了

四:Jmeter 下载和配置

  1. jmeter 下载地址:Apache JMeter - Download Apache JMeter

2.解压

  1. 进入 bin 目录,运行 jmeter.bat 文件(jmeter运行环境需要配置JDK环境)
  1. 配置 jmeter

  2. 新增线程组

设置线程数和循环次数

  1. 新增 http 请求

配置nginx的域名端口 和 Spring boot项目的 请求路径

3.新增查看结果树和聚合报告

至此 压测分布式环境搭建完成。

案例实战

初始化 Redis 库存,在浏览器执行下面链接

http://localhost:8085/add_stock

启动 jmeter 进行压测

执行结果如下

从 8085 和 8086 服务的执行日志来看,8085服务中不仅出现重复扣减的问题 ,而且与8086服务中也存在重复扣减库存问题。

优化一:加 synchronized 锁

针对上面的问题,我们通常会加 synchronized 锁,来解决并发问题,修改代码如下

复制代码
 @RequestMapping("/sub_stock1")
    public String deductStock1() {
        synchronized (this) {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }
        return "ok";
    }

重启 8085 和 8086 服务,启动 jmeter 再次压测,结果如下

从 8085 和 8086 服务的执行日志来看,同一个服务中不会出现并发问题,但不同服务,比如8085 和8086服务就会出现重复扣减库存的问题。

加 synchronized 锁缺点:在分布式的场景下,还是会出现分布式问题

优化二:使用 redis 的 setnx 实现分布式锁

复制代码
 @RequestMapping("/sub_stock2")
    public String deductStock2() {
        String lockKey = "lock_good_stock";
        String lockValue = UUID.randomUUID().toString();
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);

        if (!isLock) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            stringRedisTemplate.delete(lockKey);
        }
        return "ok";
    }
复制代码
注意:
设置值和设置过期时间不能分开写,不然也会出现服务器宕机或者启动,导致锁无法释放的问题
Boolean isLock =stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

上面代码还存在什么问题呢?

其实,上面的代码在高并发场景下,还是会出现问题,问题是锁过期,当锁过期时间到了之后,则会出现两个问题.

  • 下一个线程B会获取到锁,执行扣减库存,导致并发问题
复制代码
上一个线程A执行完时,又把锁释放了,导致下一个线程C又可以获取到锁。

针对锁失效导致的问题,对于第一个问题可以把锁的过期时间调长一点,针对第二个问题,可以先判断是不是自己加的锁,只有自己加的锁才删除。修改代码,如下:

复制代码
  @RequestMapping("/sub_stock3")
    public String deductStock3() {
        String lockKey = "lock_good_stock";
        String lockValue = UUID.randomUUID().toString();
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 5, TimeUnit.MINUTES);

        if (!isLock) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            if (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "ok";
    }

上面代码看似没问题,但删除锁的代码还是存在问题。判断和删除是两行代码,存在原子性问题。等系统并发变高,系统执行速度变慢,锁可能还是会失效,而判断和删除不是原子性,所以线程A 还是会将线程B 的锁给删除了。在下个优化解决。

优化三:使用 Lua 脚本 原子删除锁

复制代码
 @RequestMapping("/sub_stock4")
    public String deductStock4() {
        String lockKey = "lock_good_stock";
        String lockValue = UUID.randomUUID().toString();
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 5, TimeUnit.MINUTES);

        if (!isLock) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            final String script =
                    "local lockValue = redis.call('get', KEYS[1])" +
                            "if lockValue == ARGV[1] then " +
                            "   redis.call('del', KEYS[1])" +
                            "end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script);
            stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue);
        }
        return "ok";
    }

至此一把完善的分布式锁就搞定了。这对于很多中小型公司来说已经够用,但在用户量比较多,并发比较高的公司来锁,可能还是会存在一定问题。比如锁失效的问题,此时可以使用业务比较流行的框架 Redission 来解决。

优化四:使用 Redission 实现分布式锁

复制代码
@RequestMapping("/sub_stock5")
    public String deductStock5() {
        String lockKey = "lock_good_stock";
        RLock lock = redisson.getLock(lockKey);
        lock.lock(60, TimeUnit.SECONDS);
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("good_stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("good_stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            lock.unlock();
        }
        return "ok";
    }

redission 解决了我们上面所有锁提高的问题,包括分布式,原子性和锁失效问题。redission 中有个看门口的机制,当业务还没执行的时候,会不断地给锁续命,是锁不会失效。

相关推荐
禺垣1 小时前
区块链技术概述
大数据·人工智能·分布式·物联网·去中心化·区块链
Channing Lewis1 小时前
sql server如何创建表导入excel的数据
数据库·oracle·excel
秃头摸鱼侠1 小时前
MySQL安装与配置
数据库·mysql·adb
UGOTNOSHOT1 小时前
每日八股文6.3
数据库·sql
行云流水行云流水2 小时前
数据库、数据仓库、数据中台、数据湖相关概念
数据库·数据仓库
John Song2 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
IvanCodes2 小时前
七、Sqoop Job:简化与自动化数据迁移任务及免密执行
大数据·数据库·hadoop·sqoop
tonexuan2 小时前
MySQL 8.0 绿色版安装和配置过程
数据库·mysql
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql
我最厉害。,。3 小时前
Windows权限提升篇&数据库篇&MYSQL&MSSQL&ORACLE&自动化项目
数据库·mysql·sqlserver