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 中有个看门口的机制,当业务还没执行的时候,会不断地给锁续命,是锁不会失效。

相关推荐
NineData19 分钟前
NineData社区版 V4.6.0 正式发布!SQL 窗口新增4个数据源,新增支持OceanBase等多条数据复制和对比链路
数据库·sql·dba
IT果果日记21 分钟前
给DataX配置加密的方法
大数据·数据库·后端
小白学鸿蒙22 分钟前
鸿蒙数据库表中的数据如何导出为Excel存到系统下载目录
数据库·excel·harmonyos
WKP941834 分钟前
mysql的事务、锁以及MVCC
数据库·mysql
那我掉的头发算什么1 小时前
【数据库】增删改查 高阶(超级详细)保姆级教学
java·数据库·数据仓库·sql·mysql·性能优化·数据库架构
雨夜赶路人1 小时前
SQL -- GROUP BY 基本语法
数据库·sql
cr7xin2 小时前
缓存查询逻辑及问题解决
数据库·redis·后端·缓存·go
何中应2 小时前
Oracle数据库安装(Windows)
java·数据库·后端·oracle
没有bug.的程序员2 小时前
Spring Boot 整合第三方组件:Redis、MyBatis、Kafka 实战
java·spring boot·redis·后端·spring·bean·mybatis
遇见你的雩风2 小时前
【MySQL】--- 视图
数据库·mysql