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

相关推荐
云和数据.ChenGuang1 小时前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys1 小时前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi1 小时前
SQL注入的那些面试题总结
数据库·sql
建投数据2 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
Hacker_LaoYi3 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀3 小时前
Redis梳理
数据库·redis·缓存
独行soc3 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
你的微笑,乱了夏天4 小时前
linux centos 7 安装 mongodb7
数据库·mongodb
工业甲酰苯胺4 小时前
分布式系统架构:服务容错
数据库·架构
Data跳动4 小时前
Spark内存都消耗在哪里了?
大数据·分布式·spark