Redis 分布式锁

Redis 分布式锁

前提条件:

初始库存:50
请添加图片描述

SpringBoot工程搭建:

  1. pom.xml依赖
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!-- springboot整合redis-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>
  1. application.yaml配置
server:
  port: 8081

spring:
  redis:
    host: 192.168.216.129
    port: 6379
1、单机场景下,请问会出现超卖场景?
@RestController
public class StockController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/distributed-lock/substract-stock")
    public String subtractStock() {
        String stockKey = "stock";
        int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(stockKey)));
        if (stock > 0) {
            stock = stock - 1;
            stringRedisTemplate.opsForValue().set(stockKey, stock + "");
            System.out.println("减库存成功,当前库存:" + stock);
        } else {
            System.out.println("库存不足");
        }
        return "succees";
    }
}

理论分析:

请添加图片描述

压测结果(利用jmeter工具,模拟100个线程同时访问该接口,并测10组下的结果):

请添加图片描述

jmeter配置如下:

  • Thread Group配置

请添加图片描述

  • Http Request配置

请添加图片描述

  • Aggregate Report配置,保存报告生成位置

请添加图片描述

解决方案:

使用synchronized同步代码块,即可。

请添加图片描述

压测结果(无超卖):

请添加图片描述

2、分布式场景下,请问会出现超卖场景?

负债均衡架构:

请添加图片描述

	@RequestMapping("/distributed-lock/substract-stock")
    public String subtractStock() {
        String stockKey = "stock";
        synchronized (this) {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(stockKey)));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, stock + "");
                System.out.println("减库存成功,当前库存:" + stock);
            } else {
                System.out.println("库存不足");
            }
        }
        return "succees";
    }

理论分析:

会出现超卖场景,因为synchronized锁住的代码块,仅生效于单个服务器上,多个服务器之间,锁是不生效的。代表两个服务器之间可能获取到redis同一条数据,再在同一条数据上做减库存操作,出现超卖。

IDEA修改端口号及修改启动方式支持平行,快速开启两个tomcat服务:

请添加图片描述

jmeter修改访问地址为nginx地址:

请添加图片描述

压测结果(超卖,发现两台服务器出现相同的库存,代表同一时刻获取到了一样的库存,再做减库存操作):

请添加图片描述
请添加图片描述

3、分布式场景下超卖现象如何解决呢?

方案架构图:

请添加图片描述

3.1 解决方案一:利用SETNX

方案详解:

SETNX:当给锁指定的key设值时,若key不存在,则设置成功,返回1。相反,若key存在,则设置失败,返回0。

利用该思路,我们设计一个共同标识,加锁相当于执行SETNX,加锁成功,返回1,执行业务,相反,设值失败,代表别的线程正在执行,该线程等待。解锁相当于删除共同标识,让别的线程可以重新得到锁。

代码修改第一版:

@RestController
public class StockController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/distributed-lock/substract-stock")
    public String subtractStock() {
        // 分布式锁key定义
        String redisLock = "distributed-lock";
        // 设值一个唯一标识,保障改线程仅能释放自己加的锁,不能释放别的线程的锁
        UUID clientId = UUID.randomUUID();
        String stockKey = "stock";
        try {
            // 相当于执行 setnx distributed-lock lock
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, clientId.toString());
            // 设值失败,相当于没有得到锁,通过自旋方式尝试获得锁
            if (!result) {
                // 如果没有抢到锁,返回一个错误码,让前台做友好提示
                return "error";
            }
            // 减库存
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(stockKey)));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, stock + "");
                System.out.println("减库存成功,当前库存:" + stock);
            } else {
                System.out.println("库存不足");
            }
            return "succees";
        } finally {
            // 仅加锁的线程,能够释放该锁
            if (clientId.toString().equals(stringRedisTemplate.opsForValue().get(redisLock))) {
                // 放在finally中执行,保障异常场景下,锁能够得到释放
                stringRedisTemplate.delete(redisLock);
            }
        }
    }
}

压测结果(无超卖):

请添加图片描述

请添加图片描述

想想上述代码可能还有那些问题?

问题:

因为setnx设置的key是永久的,代表如果线程A获得锁,迟迟不释放,比如运行10多分钟都不释放,那么别的线程都得不到锁,整个服务就无法正常对外提供。

如何解决:

问题点在key是永久的,那么给key设置一个过期时间,比如30s,当线程A如果30s内仍然不释放锁,那么key过期,别的线程依旧能够正常获取到锁,正常往下执行。

解决代码如下:

@RestController
public class StockController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/distributed-lock/substract-stock")
    public String subtractStock() {
        // 分布式锁key定义
        String redisLock = "distributed-lock";
        // 设值一个唯一标识,保障改线程仅能释放自己加的锁,不能释放别的线程的锁
        UUID clientId = UUID.randomUUID();
        String stockKey = "stock";
        try {
            // 相当于执行 setnx distributed-lock lock
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, clientId.toString());
            // 设值过期时间为30s
            stringRedisTemplate.expire(redisLock, 30, TimeUnit.MILLISECONDS);
            // 设值失败,相当于没有得到锁,通过自旋方式尝试获得锁
            if (!result) {
                // 如果没有抢到锁,返回一个错误码,让前台做友好提示
                return "error";
            }
            // 减库存
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(stockKey)));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, stock + "");
                System.out.println("减库存成功,当前库存:" + stock);
            } else {
                System.out.println("库存不足");
            }
            return "succees";
        } finally {
            // 仅加锁的线程,能够释放该锁
            if (clientId.toString().equals(stringRedisTemplate.opsForValue().get(redisLock))) {
                // 放在finally中执行,保障异常场景下,锁能够得到释放
                stringRedisTemplate.delete(redisLock);
            }
        }
    }
}

想想上述代码是否还有问题?

问题:

// 相当于执行 setnx distributed-lock lock
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, clientId.toString());
// 设值过期时间为30s
stringRedisTemplate.expire(redisLock, 30, TimeUnit.MILLISECONDS);

由于上述两段代码不是原子执行的,所以有可能第一个方法执行完,系统宕机,那么也会出现锁得不到释放。

解决办法:

将两个方法,提成原子执行即可。

@RestController
public class StockController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/distributed-lock/substract-stock")
    public String subtractStock() {
        // 分布式锁key定义
        String redisLock = "distributed-lock";
        // 设值一个唯一标识,保障改线程仅能释放自己加的锁,不能释放别的线程的锁
        UUID clientId = UUID.randomUUID();
        String stockKey = "stock";
        try {
            // 相当于执行 setnx distributed-lock lock
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, clientId.toString(), 30, TimeUnit.MILLISECONDS);
            // 设值失败,相当于没有得到锁,通过自旋方式尝试获得锁
            if (!result) {
                // 如果没有抢到锁,返回一个错误码,让前台做友好提示
                return "error";
            }
            // 减库存
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(stockKey)));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, stock + "");
                System.out.println("减库存成功,当前库存:" + stock);
            } else {
                System.out.println("库存不足");
            }
            return "succees";
        } finally {
            // 仅加锁的线程,能够释放该锁
            if (clientId.toString().equals(stringRedisTemplate.opsForValue().get(redisLock))) {
                // 放在finally中执行,保障异常场景下,锁能够得到释放
                stringRedisTemplate.delete(redisLock);
            }
        }
    }
}

想想上述代码是否还有问题?

问题:

假如线程A获得锁,30s没有把业务完成,比如还未获取库存,锁过期了,那么线程B获取锁,继续减库存,由于线程A业务还没完成,所以库存还未减,那么此时相当于两个线程同时去执行减库存了,也会出现超卖,不过概率很小。

解决:

Redisson

3.2 解决方案二:Redisson
  1. 添加依赖
<!-- springboot整合redisson -->
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-spring-boot-starter</artifactId>
	<version>3.16.0</version>
</dependency>
  1. 注入Redisson对象,定义Redisson Bean
@Configuration
public class ReddsionConfig {

    /**
     * 配置Redisson,来获取到redisLock
     *
     * @return Redisson
     */
    @Bean
    public Redisson redisson() {
        // Redisson为单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.216.129:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}
  1. 业务代码使用Redisson加锁、解锁实现分布式锁
@RestController
public class StockController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private Redisson redisson;

    @RequestMapping("/distributed-lock/substract-stock")
    public String subtractStock() {
        // 分布式锁key定义
        String redisLock = "distributed-lock";
        // 获取分布式锁,注意,此处默认过期时间是30s,由于没有显示定义过期时间,所以会默认给key续命
        RLock redissonLock = redisson.getLock(redisLock);
        try {
            // 加锁
            redissonLock.lock();
            String stockKey = "stock";
            // 减库存
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(stockKey)));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, stock + "");
                System.out.println("减库存成功,当前库存:" + stock);
            } else {
                System.out.println("库存不足");
            }
            return "succees";
        } finally {
            // 解锁
            redissonLock.unlock();
        }
    }
}
  1. 压测结果(无超卖)

请添加图片描述

请添加图片描述

注意点:

  1. RLock redissonLock = redisson.getLock(redisLock);
    

不显示给定key默认过期时间,Ression底层会给定30s过期时间,并且每隔30*1/3=10s,发现业务仍为执行完成,会继续续命到30s。

  1. redissonLock.lock(30, TimeUnit.MILLISECONDS);
    

显示给定超时时间,实际就是key过期时间,如果到了key过期时间,业务仍为执行完成,可能被别的线程解锁,导致报错,报错信息:java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id

Redisson底层原理图:

请添加图片描述

想想上述方案是否还有问题?

如果Redis搭哨兵集群方式,如果某个线程使用Ression加锁成功,突然Redis主节点宕机了,该锁还未来得及释放,那么Redis从节点此时选举为主节点,其Redis中的数据,相比主节点有缺失,可能之前锁信息丢失,导致别的线程又能获取到锁,造成分布式锁失效。

解决办法:

可以使用Zookeeper集群方式代替Redis方式,不过性能大大降低,所以需要在性能和高可用方面做取舍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值