REDIS14_分布式锁的概述、加锁使用sexnu、解锁使用lua脚本保证原子性、引发的问题思考

①. 分布式锁的概述

  • ①. 锁的种类
  1. 单机版同一个JVM虚拟机内,synchronized或者Lock接口
  2. 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了
  • ②. 一个靠谱分布式锁需要具备的条件和刚需 (掌握)
  1. 独占性:任何时刻只能有且仅有一个线程持有
  2. 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
  3. 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
  4. 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。
  5. 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

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

②. 分布式锁的案例搭建

  • ①. 建Module boot_redis01、boot_redis02

  • ②. 改POM

<?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.3.10.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.xiaozhi.redis</groupId>
    <artifactId>boot_redis01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>
        <!--web+actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- springboot-aop 技术-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        <!--一般通用基础配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • ③. 写yml
# 端口号
server.port=1111
# ========================redis相关配置=====================
# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=192.168.56.10
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
  • ④. 配置类config
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}
  • ⑤. 业务类
@RestController
public class GoodController{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @GetMapping("/buy_goods")
    public String buy_Goods(){
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0){
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
        }else{
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
    }
}

③. 使用sexnx+lua脚本解决

  • ①. 没有加锁,并发下数字不对,出现超卖现象,可以加上lock和synchronized来解决,不适合分布式的情况

  • ②. 使用分布式锁setIfAbsent来解决

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        String key = "RedisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
			 // setIfAbsent是java中的方法
			 // setnx是redis命令中的方法
        Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if(!flagLock) {
            return "抢夺锁失败";
        }

        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0)
        {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            stringRedisTemplate.delete(key);
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
        }else{
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
        }

        return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
    }
  • ③. 在②的情况下,如果出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
    在这里插入图片描述
  • ④. 部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
   Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);//1
   stringRedisTemplate.expire(key,10L,TimeUnit.SECONDS);//2
  • ⑤. 设置key+过期时间分开了,必须要合并成一行具备原子性(加锁必须确保原子性)
    在④中,如果某一个时刻,刚刚执行完//1,这个时候发生宕机,那么就造成了死锁
    stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);
  • ⑥. 张冠李戴,删除了别人的锁
    如果线程A拿到分布式锁,设置的过期时间小于业务代码执行的时间,当A线程分布式锁刚刚过期,这个时候B线程获取到了分布式锁,A线程执行完业务逻辑,进行删除,就可能删除的是B的分布式锁
finally {
  if (stringRedisTemplate.opsForValue().get(key).equals(value)) {
      stringRedisTemplate.delete(key);
  }
}
  • ⑦. 基于⑥的操作也有问题,finally块的判断+del删除操作不是原子性的
    在这里插入图片描述
  • ⑧. 用Lua脚本,删除保证原子性
   //1.占分布式锁,去redis占坑  setIfAbsent==sexnx
   //Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
   //redisTemplate.expire("lock",30,TimeUnit.SECONDS);
   String uuid = UUID.randomUUID().toString();
   Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
   if(lock){
       Map<String, List<Catelog2Vo>> dataFromDb=null;
       try{
           //加锁成功,执行业务
           dataFromDb = getDataFromDb();
       }finally {
           //原子删锁
           String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                   "then " +
                   "return redis.call('del', KEYS[1]) " +
                   "else " +
                   "   return 0 " +
                   "end";
           //删除成功返回1,删除不成功返回0
           redisTemplate.execute(
                   new DefaultRedisScript<Long>(script,Long.class),
                   Arrays.asList("lock"),uuid);
       }
       return dataFromDb;
   }else{
       //加锁失败....重试
       try { TimeUnit.SECONDS.sleep(1);  } catch (InterruptedException e) {e.printStackTrace();}
       return getCatalogJsonFromDbWithRedisLock();
   }

④. 问题总结 - 推出分布式锁

  • ①. synchronized单机版OK,上分布式

  • ②. nginx分布式微服务,单机锁不行

  • ③. 取消单机锁,上redis分布式锁setnx

  • ④. 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁

  • ⑤. 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定

  • ⑥.为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行

  • ⑦. 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3

  • ⑧. redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现

⑤. Redis单机CP、集群AP、EurekaAP、Zookeeper集群CP

  • ①. Redis单机是CP,集群是AP
    在这里插入图片描述

  • ②. Redis集群:AP
    (redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据)

  • ③. Zookeeper集群的CP
    在这里插入图片描述在这里插入图片描述

  • ④. Eureka是AP
    在这里插入图片描述

⑥. 单机的Redis案例加锁、解锁

  • ①. 加锁:加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间

  • ②. 解锁:将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁
    (为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功)

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0 
end
  • ③. 超时:锁key要注意过期时间,不能长期占用

  • ④. 单机模式中,一般都是用set/setnx+lua脚本搞定,想想它的缺点是什么?
    如果redis发生了宕机,所有的请求压力都指向了数据库

public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
    String uuid = UUID.randomUUID().toString();
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
    if (lock) {
        Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
        String lockValue = ops.get("lock");
        // get和delete原子操作
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
            "    return redis.call(\"del\",KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        stringRedisTemplate.execute(
            new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型
            Arrays.asList("lock"), // 参数
            lockValue); // 参数值,锁的值
        return categoriesDb;
    }else {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 睡眠0.1s后,重新调用 //自旋
        return getCatalogJsonDbWithRedisLock();
    }
}
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

所得皆惊喜

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值