Redis分布式锁的正确实现方式(Java版)

56 篇文章 3 订阅

一、为什么需要分布式锁:

为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。

但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,即synchronized在分布式系统中失效了。 为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题

二、什么是分布式锁:

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

我们先来看下,一把靠谱的分布式锁应该有哪些特征:

在这里插入图片描述

三、主流实现分布式锁的技术:

  1. 基于关系型数据库实现的分布式锁:
    利用mysql的乐观锁实现分布式锁(版本号),但是性能完全基于mysql本身,在高并发会直接打垮mysql,用的时候一定要三思!!(由于性能太差,不推荐,也不做后续的对比)
  2. 基于单机redis实现分布式锁:
    redis2.6后 SET指令已经支持nx/px/expire,实现加锁非常简单,也是目前99%中小型公司分布式锁的选择
  3. 基于redis集群实现的分布式锁
    为了解决单机redis分布式锁的单点故障及可重入性问题,redis推出了红锁
  4. 基于zk集群实现的分布式锁
    利用zk有序的临时节点加上它的watch机制实现分布式锁,它的可靠性最高

对于以上分布式锁实现的对比:
1. 基于关系数据库的分布式锁,一定要在没有大流量的业务且没有其他资源下才会去选择它;
2. 实现复杂度:2,3,4都有对应的客户端,相应的功能已经都封装好,都非常简单;
3. 可靠性(高–低):zk集群 > 红锁 > 单机redis;
4. 性能(高–低):单机redis > 红锁 > zk集群;
5. 成本(高–低):红锁 > zk集群 > 单机redis;

基于以上的对比,也不难发现为什么99%中小型公司的分布式锁要选择 单机redis。

总结:
在这里插入图片描述

四、Redis分布式锁实现原理:

加锁过程:
redis的加锁就是给Key键设置一个值,且key存在时则不能设置成功,为避免死锁,并给定一个过期时间
且在redis 2.6 后SET指令已经支持nx/px/expire,它能保证设置key和给key加过期时间的原子性。

具体实现:
SET(key,value,NX,PX,time)
其中 key 就是要锁定的资源;value 可以是客户端唯一标识;NX代表只在键不存在时,
才对键进行设置操作;PX 表示对这个key要设置过期时间 ;time 表示key过期的具体时间。

解锁过程:
就是删除掉key来释放资源,但是不能乱删除,需要通过判断key中的value和当前客户端标识一致时才能删除,且整个过程要保证它的原子性

1.为什么解锁过程要保证原子性呢?
举例说明一下如果解锁过程没有保证原子性的问题,当v1客户端进来拿到锁了,当它准备解锁时在判断客户端标识时拿到value是v1即客户端标识一致,
此时刚好它加锁的key过期了,同时v2客户端加锁成功了,则v1会删除属于v2的key,这种情况下就属于误删,则解锁的过程必须要保证原子性

2.那么如何保证加锁过程保证原子性?
使用lua脚本,在lua脚本中的指令可以保证它的原子性执行。(lua脚本也不支持回滚,但可以手动回滚)

解锁的lua脚本如下:
-------------------------------------------------------
if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0 
end
-------------------------------------------------------

五、Redis分布式锁的演化升级:

redis分布式锁的实现原理图:
在这里插入图片描述

在这里插入图片描述

redis分布式锁的使用场景:

多个服务间+保证同一时刻+同一个用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

代码演示redis分布式锁:
在这里插入图片描述
创建一个空项目名字叫redis:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.在空项目创建两个子springboot模块:

boot_redis01
boot_redis02:boot_redis02是完全复制的模块一

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.改pom:

common-pool2的对象池依赖:这个依赖必须有,因为RedisCong配置类需要用到:

我们在服务器开发的过程中,往往会有一些对象,它的创建和初始化需要的时间比较长,比如数据库连接,网络IO,大数据对象等。在大量使用这些对象时,如果不采用一些技术优化,就会造成一些不可忽略的性能影响。一种办法就是使用对象池,每次创建的对象并不实际销毁,而是缓存在对象池中,下次使用的时候,不用再重新创建,直接从对象池的缓存中取即可。为了避免重新造轮子,我们可以使用优秀的开源对象池化组件apache-common-pool2,它对对象池化操作进行了很好的封装,我们只需要根据自己的业务需求重写或实现部分接口即可,使用它可以快速的创建一个方便,简单,强大对象连接池管理类。

使用common-pool2的对象池技术的一个完美例子就是redis的Java客户端JedisPool

综上所述,使用common-pool2可以快速的创建一个安全,强大,简单的对象池管理类。它的开源性使它的功能得到了众多项目的检测,是非常安全的。在我们的业务中,如果有需要使用对象池化的操作,可以使用common-pool2快速实现。

jedis依赖:
Jedis是一个jar包,是Redis官方推荐的用于java访问redis的客户端,主要是用来帮助连接使用数据库。Java项目使用Redis数据库,新版的默认是lettuce客户端,如果要使用jedis客户端,需要先排除lettuce,然后添加jedis客户端。
如果是Maven项目,在pom.xml文件中添加下面两种支持:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

引入redisson依赖,此依赖先暂时不添加,后边测试redisson的时候再添加
由于我们是springboot整合redisson,所以我们只需引入springboot-redisson-starter就可以了,
不过这里需要注意springboot与redisson的版本,因为官方推荐redisson版本与springboot版本配合使用。

将 Redisson 与 Spring Boot 库集成。取决于Spring Data Redis模块,支持 Spring Boot 1.3.x - 2.4.x

这句话是官方说的,不过现在的2.5.x也是支持的,只需要注意springboot最低版本不要低于1.3.x即可。

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

也可以添加这个依赖:

<!--redisson本次测试用的这个依赖-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

总的pom:
这里注意:common-pool2不要写自己规定的版本号。springboot会进行自动版本协调的。
这里使用最新的springboot2.7

<dependencies>
        <!--web+actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--spring-boot-starter-actuator
        自动配置模块用于支持 SpringBoot 应用的监控-->
        <!--spring-boot-starter-actuator
        可以用于检测系统的健康情况、当前的Beans、系统的缓存等-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!--新版的默认是lettuce客户端,如果要使用旧的jedis客户端,就排除lettuce-->
            <!--<exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>-->
        </dependency>
        <!--jedis客户端,jedis版本不能太高-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.7.3</version>
        </dependency>
        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <!--这里注意,千万不能自己写版本号,会出现冲突-->
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

3.写yml:

这里使用Lettuce 客户端的连接池:
在这里插入图片描述

server.port=1111

#redis
#Redis服务器地址
spring.redis.host=192.168.211.210

#Redis数据库索引(默认为0)
spring.redis.dataabase=0

#Redis服务器连接端口
spring.redis.port=6379

# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=500
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=1800000

4.主启动类:自动生成的

在这里插入图片描述

5.配置类conf.RedisConfig:
**添加redis配置类:**如果出现报错,也可以启动运行的话,不用管爆红。

package com.csdn.springboot_redis.conf;
 
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
import java.time.Duration;
 
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
 
 //这里也可以使用RedisConnectionFactory factory
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
//key序列化方式
        template.setKeySerializer(redisSerializer);
//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }
 
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

在这里插入图片描述

redis1.0版本:不使用锁

6.controller:

package com.fan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class GoodsController {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy")
    public String buy_Goods(){
        System.out.println("test");
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNum = result == null ? 0 : Integer.parseInt(result);
        if(goodsNum > 0){
            int realNum = goodsNum - 1;//卖出一个商品
            stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
            System.out.println("成功买到商品,库存还剩下:"
                    +realNum+"件,\t 服务提供的端口:"+ serverPort);
            return "成功买到商品,库存还剩下:"
                    +realNum+"件,\t 服务提供的端口:"+ serverPort;
        }else{
            System.out.println("商品卖完了!!!\t 服务提供的端口:"+ serverPort);
        }

        return "商品卖完了!!!\t 服务提供的端口:"+ serverPort;
    }

    @GetMapping("/redis")
    public String test01(){
        stringRedisTemplate.opsForValue().set("name","tom");
        String name = (String)stringRedisTemplate.opsForValue().get("name");
        return name;
    }


}

同样,创建第二个微服务,在父工程下包含两个微服务模块;

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

package com.fan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class GoodsController {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy")
    public String buy_Goods() {
        System.out.println("test");
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNum = result == null ? 0 : Integer.parseInt(result);
        if (goodsNum > 0) {
            int realNum = goodsNum - 1;//卖出一个商品
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNum));
            System.out.println("成功买到商品,库存还剩下:"
                    + realNum + "件,\t 服务提供的端口:" + serverPort);
            return "成功买到商品,库存还剩下:"
                    + realNum + "件,\t 服务提供的端口:" + serverPort;
        } else {
            System.out.println("商品卖完了!!!\t 服务提供的端口:" + serverPort);
        }

        return "商品卖完了!!!\t 服务提供的端口:" + serverPort;
    }

    @GetMapping("/redis")
    public String test01() {
        stringRedisTemplate.opsForValue().set("name", "tom");
        String name = (String) stringRedisTemplate.opsForValue().get("name");
        return name;
    }


}

后台启动redis;
同时启动两个微服务:view—>Tool Windows -->Servers 调动微服务同时运行的窗口。
在这里插入图片描述

然后点击加号运行选择springboot 类型。然后运行项目;

启动两个微服务,并测试:

在这里插入图片描述

在这里插入图片描述
单机版的2种锁的主要区别:sync和Lock,
sync容易造成线程的积压,而Lock可以进行tryLock的判断,并且可以约定等待时间,到时间后放弃抢锁。

在这里插入图片描述

redis2.0版本:使用单机版锁

其他配置 不变。这里修改controller即可:
这里2.0版本我们增加了锁来防止高并发,但是是单机锁synchronized或者是Lock。

代码:

package com.fan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class BuyController {
    @Value("${server.port}")
    private String serverPort;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy")
    public String buy(){
        synchronized (this){

            String s = stringRedisTemplate.opsForValue().get("goods:001");
            int count = s == null? 0 :Integer.parseInt(s);
            if(count > 0){
                int realCount = count - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount ));
                System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort);
                return "剩余商品为:"+realCount +",提供服务的端口号:"+serverPort;
            }else{
                System.out.println("商品售罄!!");
            }
            return "商品售罄!!";
        }
    }
}

在这里插入图片描述

在这里插入图片描述

这里为了演示抢锁和超卖的问题,我们设置了nginx来进行负载均衡:
在这里插入图片描述

修改nginx的配置文件:

cd /usr/local/nginx/conf
ll
vim nginx.conf
启动:cd /usr/local/nginx/sbin --> ./nginx
重启:cd /usr/local/nginx/sbin --> ./nginx -s reload

在这里插入图片描述

在这里插入图片描述

192.168.211.210 是nginx所在的linux机器的ip地址;

在这里插入图片描述

使用Jmeter来进行压测,测试高并发问题:

在这里插入图片描述

在这里插入图片描述
发现出现了超卖和重票的并发问题;所以说单机版的锁不能解决分布式的高并发问题的。

redis3.0版本:使用redis的setnx

stringRedisTemplate.opsForValue().setIfAbsent相当于setnx

在这里插入图片描述

package com.fan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.UUID;

@RestController
public class BuyController {
    @Value("${server.port}")
    private String serverPort;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy")
    public String buy(){

        String key = "redis_lock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //设置锁
        Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if(!flagLock){
            return "抢锁失败";
        }
        //抢锁成功后的业务操作:
        String s = stringRedisTemplate.opsForValue().get("goods:001");
        int count = s == null? 0 :Integer.parseInt(s);
        if(count > 0){
            int realCount = count - 1;
            stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount ));
            System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort);
            //操作完毕,释放锁,在return前释放
            stringRedisTemplate.delete(key);
            return "剩余商品为:"+realCount +",提供服务的端口号:"+serverPort;
        }else{
            System.out.println("商品售罄!!");
        }
        return "商品售罄!!";

    }
}

3.0版本的问题:
在这里插入图片描述
使用try-finally封装代码:
在这里插入图片描述

在这里插入图片描述

redis4.0版本:使用try-finally来释放锁:

4.0版本存在的问题:

在这里插入图片描述

package com.fan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.UUID;

@RestController
public class BuyController {
    @Value("${server.port}")
    private String serverPort;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/buy")
    public String buy(){

        String key = "redis_lock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //设置锁
        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            if(!flagLock){
                return "抢锁失败";
            }
            //抢锁成功后的业务操作:
            String s = stringRedisTemplate.opsForValue().get("goods:001");
            int count = s == null? 0 :Integer.parseInt(s);
            if(count > 0){
                int realCount = count - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount ));
                System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort);
                //操作完毕,释放锁,在return前释放
                return "剩余商品为:"+realCount +",提供服务的端口号:"+serverPort;
            }else{
                System.out.println("商品售罄!!");
            }
            return "商品售罄!!";
        } finally {
            stringRedisTemplate.delete(key);
        }

    }
}

redis5.0版本:对redis的锁有过期时间的设定:

注意,此处的是单独一行设置过期时间:
在这里插入图片描述

 @GetMapping("/buy")
    public String buy(){

        String key = "redis_lock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //设置锁
        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            //单独一行设置过期时间
            stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);
            if(!flagLock){
                return "抢锁失败";
            }
            //抢锁成功后的业务操作:
            String s = stringRedisTemplate.opsForValue().get("goods:001");
            int count = s == null? 0 :Integer.parseInt(s);
            if(count > 0){
                int realCount = count - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount ));
                System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort);
                //操作完毕,释放锁,在return前释放
                return "剩余商品为:"+realCount +",提供服务的端口号:"+serverPort;
            }else{
                System.out.println("商品售罄!!");
            }
            return "商品售罄!!";
        } finally {
            stringRedisTemplate.delete(key);
        }

    }

在这里插入图片描述

5.0存在的问题:设置key+过期时间分开了,不具备原子性;

redis6.0版本:对redis的锁有过期时间的设定具有原子性:

在这里插入图片描述
将过期时间和锁的设置放在一行:

@GetMapping("/buy")
    public String buy(){

        String key = "redis_lock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //设置锁
        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().
                    setIfAbsent(key, value,10L,TimeUnit.SECONDS);
            //单独一行设置过期时间
            //stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);
            if(!flagLock){
                return "抢锁失败";
            }
            //抢锁成功后的业务操作:
            String s = stringRedisTemplate.opsForValue().get("goods:001");
            int count = s == null? 0 :Integer.parseInt(s);
            if(count > 0){
                int realCount = count - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount ));
                System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort);
                //操作完毕,释放锁,在return前释放
                return "剩余商品为:"+realCount +",提供服务的端口号:"+serverPort;
            }else{
                System.out.println("商品售罄!!");
            }
            return "商品售罄!!";
        } finally {
            stringRedisTemplate.delete(key);
        }

    }

在这里插入图片描述
6.0版本存在的问题:锁的误删除

在这里插入图片描述

在这里插入图片描述

redis7.0版本:对redis的锁删除不进行误删

在这里插入图片描述

 @GetMapping("/buy")
    public String buy(){

        String key = "redis_lock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //设置锁
        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().
                    setIfAbsent(key, value,10L,TimeUnit.SECONDS);
            //单独一行设置过期时间
            //stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);
            if(!flagLock){
                return "抢锁失败";
            }
            //抢锁成功后的业务操作:
            String s = stringRedisTemplate.opsForValue().get("goods:001");
            int count = s == null? 0 :Integer.parseInt(s);
            if(count > 0){
                int realCount = count - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount ));
                System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort);
                //操作完毕,释放锁,在return前释放
                return "剩余商品为:"+realCount +",提供服务的端口号:"+serverPort;
            }else{
                System.out.println("商品售罄!!");
            }
            return "商品售罄!!";
        } finally {
            //防止锁的误删除,这里进行判断,后删除锁
            if(stringRedisTemplate.opsForValue().get(key).equals(value)){
                stringRedisTemplate.delete(key);
            }
        }

    }

redis8.0版本:lua脚本保证原子性:

在Redis的set命令介绍中,最后推荐Lua脚本进行锁的删除:

lua脚本:https://redis.io/commands/set/

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

工具类RedisUtil :
在这里插入图片描述

package com.fan.utils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Set;

public class RedisUtil {
	//实例

	//得到jedisPool
	private static JedisPool jedisPool;

	static {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxTotal(20);//最大连接数
		jedisPoolConfig.setMaxIdle(10);//空闲连接数

		//创建一个jedis,连接redis服务器
		jedisPool = new JedisPool(jedisPoolConfig,"192.168.211.210",6379,2*1000);
	}

	//得到jedis对象
	public static Jedis getJedis() throws Exception {
		if(null != jedisPool){
			return jedisPool.getResource();
		}
		throw new Exception("jedisPool is not ok");
	}

	//关闭jedis对象
	public void closeJedis(Jedis jedis){
		if (jedis != null){
			jedis.close();
		}
	}
	//获取某个键的值
	public String get(String key) throws Exception {
		Jedis jedis = getJedis();
		String value = jedis.get(key);
		closeJedis(jedis);
		return value;
	}
	//拿到所有键的集合
	public Set<String> keys(String key) throws Exception {
		Jedis jedis = getJedis();
		Set<String> keys = jedis.keys(key);
		closeJedis(jedis);
		return keys;

	}

}

脚本代码:
在这里插入图片描述

package com.fan.controller;

import com.fan.config.RedisUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class buyController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;
    @GetMapping("/buy")
    public String buy() throws Exception {

        String key = "redis_lock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //设置锁
        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().
                    setIfAbsent(key, value,10L, TimeUnit.SECONDS);
            if(!flagLock){
                return "抢锁失败";
            }
            //抢锁成功后的业务操作:
            String s = stringRedisTemplate.opsForValue().get("goods:001");
            int count = s == null? 0 :Integer.parseInt(s);
            if(count > 0){
                int realCount = count - 1;
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount));
                System.out.println("剩余商品为:"+realCount+",提供服务的端口号:"+serverPort);
                //操作完毕,释放锁,在return前释放
                return "剩余商品为:"+realCount+",提供服务的端口号:"+serverPort;
            }else{
                System.out.println("商品售罄!!");
            }
            return "商品售罄!!";
        } finally {

            Jedis jedis = null;
            try{
                //获取jedis实例
                jedis = RedisUtil.getJedis();

                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";

                Object eval = jedis.eval(script,
                        Collections.singletonList(key),
                        Collections.singletonList(value));
                if("1".equals(eval.toString())){
                    System.out.println("-----del redis lock ok....");
                }else{
                    System.out.println("-----del redis lock error ....");
                }
            }finally {
                if(null != jedis){
                    jedis.close();
                }
            }
        }

    }

}

测试:发现不出出现超卖和重复。

8.1版本:redis事务操作:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

redis事务来完成判断删除锁:
在这里插入图片描述

9.0版本:redlock+redisson

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//注册RedissonClient对象:此bean放到RedisConfig类中:

  //注册RedissonClient对象
    @Bean(destroyMethod="shutdown")
    RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.211.210:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

redisson代码:


import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class BuyController {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String serverPort;
    @Resource
    private RedissonClient redissonClient;//客户端

    @GetMapping("/buy")
    public String buy(){
        String key = "redis_lock";
        RLock lock = redissonClient.getLock(key);//获取锁
        lock.lock();//加锁
        try {
            //业务放到try中
            String res = stringRedisTemplate.opsForValue().get("goods:001");
            int count = res == null ? 0 :Integer.parseInt(res);
            if(count > 0){
                int realCount = count - 1;//减库存
                //将库存设置到数据库
                stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount));
                System.out.println("剩余库存:"+realCount+",服务的端口:"+serverPort);
                return "剩余库存:"+realCount+",服务的端口:"+serverPort;
            }else{
                System.out.println("库存没有了");
            }
            return "库存没有了";
        } finally {
            if(lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();//解锁
            }
        }
    }

}

  • 7
    点赞
  • 24
    收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页
评论 2

打赏作者

这个名字先用着

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值