Redis7_17 高阶篇 第八章 [万字解析]手写Redis的分布式锁(基础版)

🌟 大家好!👋 欢迎来到“孤尘”的Java世界!🌍
🛳️感谢认可!事不宜迟,一起尽情地畅享编程之旅吧!🌟

目录

1 面试题(文末有答案)

2 锁的种类

3 为什么要引入分布式锁?

3.1 主要原因

3.2 具体原因

4 分布式锁实现后应当具备哪些基本条件?

5 分布式系统中,各单机上使用单机锁测试案例(版本1-2)

5.1 项目一准备

5.1.1 建module

5.1.2 pom文件

5.1.3 yaml文件

5.1.4 主启动类

5.1.5 业务类

Service

Controller

5.3 测试

5.3.1 简单测试(版本1)

打开swagger地址

7777端口测试(module1测试)

8888端口测试(module2测试)

5.3.2 nginx测试负载均衡(版本2)

nginx配置负载均衡实现轮询

修改配置文件

关闭nginx(如果在运行中的话)

为nginx指定配置文件

重启nginx

开始测试

5.3.3 jemeter压力测试

5.4 该锁出现超卖的原因分析

5.5 如何改进

6 实现redis的分布式锁

6.1 版本三 引入分布式锁

6.1.1 改进内容

6.1.2 实现

实现要点

 6.1.3 不足之处

6.2 版本四 分布式锁防死锁的改进

6.2.1 介绍

改进内容

如何实现?

6.2.2 实现

实现要点

6.2.3 不足之处

6.3 版本五/六  分布式锁的防误解锁改进

6.3.1 介绍

改进内容

如何实现?

6.3.2 实现

实现要点

分步骤解锁(不满足原子性)

lua脚本解锁key(保证原子性) 

6.3.3 不足之处

7 总结

8 面试题(带答案)

Redis除了常用来做缓存,你还见过基于Redis的什么用法?

Redis做分布式锁的时候有需要注意的问题?

你们公司自己实现的分布式锁是否用的setnx命令实现?

这个是最合适的吗?你如何考虑分布式锁的可重入问题?

如果是Redis是单点部署的,会带来什么问题?

那你准备怎么解决单点问题呢?

Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?

那你简单的介绍一下Redlock吧?

你简历上写redisson,请你介绍一下你是怎么使用的。

Redis分布式锁如何续期?看门狗知道吗?


概念了解完成后,若需要直接获取redis分布式锁实现,可以直接跳至本文第六节(6.3)查看第六版本的分布式锁实现.

1 面试题(文末有答案)

  1. Redis除了常来做缓存,你还见过基于Redis的什么用法?
    1. (数据共享,分布式Session,分布式锁,全局ID,计算器、点赞,位统计,购物车,轻量级消息队列,抽奖,点赞、签到、打卡,差集交集并集,用户关注、可能认识的人,推荐模型,热点新闻、热搜排行榜)
  2. Redis做分布式锁的时候有需要注意的问题?
  3. 你们公司自己实现的分布式锁是否用的setnx命令实现?
  4. 这个是最合适的吗?你如何考虑分布式锁的可重入问题?
  5. 如果是Redis是单点部署的,会带来什么问题?
  6. 那你准备怎么解决单点问题呢?
  7. Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?
  8. 那你简单的介绍一下Redlock吧?你简历上写redisson,请你介绍一下你是怎么使用的。
  9. Redis分布式锁如何续期?看门狗知道吗?

2 锁的种类

  1. 单机版同一个JvM虚拟机内的锁,Synchronized或者Lock接口实现。
  2. 分布式多个不同VM虚拟机之间形成的锁,使用使用Redis、Zookeeper、etcd等工具来实现。

3 为什么要引入分布式锁?

3.1 主要原因

引入分布式锁主要是为了解决在分布式系统中多个节点对共享资源进行并发访问时所产生的冲突和数据不一致问题。

3.2 具体原因

  • 防止资源竞争:在分布式环境中,多个节点可能同时尝试访问和修改同一个资源(如数据库记录、文件等)。分布式锁可以确保在任意时刻只有一个节点能够对资源进行修改,避免竞争条件导致的数据不一致或冲突。

  • 保证数据一致性:分布式系统中的节点可能分布在不同的物理机器上,如果没有分布式锁机制,不同节点对同一资源的并发操作可能会导致数据不一致。分布式锁可以保证对资源的访问是原子的,从而确保数据的一致性。

  • 实现任务协调:在一些应用场景中,需要多个节点协调完成一项任务,比如分布式任务调度系统。通过分布式锁,可以确保某个任务在任意时刻只会被一个节点执行,避免重复执行或任务冲突。

  • 避免死锁和活锁:分布式锁机制可以通过设置锁超时时间和重试机制来避免死锁和活锁问题,从而提高系统的稳定性和可靠性。

  • 提升系统的可靠性:在分布式环境中,如果一个节点因故障导致锁无法释放,其他节点可以通过分布式锁服务提供的机制来检测锁的状态并采取相应的恢复措施,确保系统的高可用性和可靠性。

4 分布式锁实现后应当具备哪些基本条件?

  • 互斥性(Mutual Exclusion):在任意时刻,只有一个客户端可以获取到锁。确保同一时刻不会有多个客户端同时持有锁,以避免资源竞争和数据不一致。

  • 死锁避免(Deadlock Prevention):锁机制必须能够避免死锁的发生,即一个或多个客户端永远等待无法释放的锁。常见的做法是给锁设置一个过期时间,确保即使客户端因故障未能主动释放锁,锁也会在一段时间后自动释放。

  • 防误解锁(Ownership Verification):解锁时必须确保解锁的是当前所在资源拿到的锁,而不能误把其他资源的锁给解锁。(不能解了别人的锁)

  • 容错性(Fault Tolerance):在分布式环境中,锁的实现需要具备一定的容错能力。例如,如果持有锁的客户端崩溃或网络分区,其他客户端仍然能够安全地获取锁。分布式锁服务(如Zookeeper、Redis)通常会提供心跳检测和锁过期机制来实现这一点。

  • 锁重入(Reentrancy):某些应用场景下,需要支持同一客户端在持有锁的情况下可以多次获取锁而不会被阻塞。这对于递归调用或多层函数调用时尤为重要。

  • 高可用性(High Availability):分布式锁服务本身需要具有高可用性,避免单点故障。通常通过集群部署和多副本机制来提高锁服务的可用性和可靠性。

5 分布式系统中,各单机上使用单机锁测试案例(版本1-2)

我们构建一个分布式系统,在各单机上,使用单机锁,试一试,分布式系统中,使用单机锁到底能不能维护数据一致性。

5.1 项目一准备

5.1.1 建module

(直接新建module,这里省去了建立工程的步骤。)

新建两个module:

  1. redis distributed lock2
  2. redis distributed1 lock3

注意:两个module实现都一样,只是端口号不一样,一个是7777另一个是8888 让这两个module 作为分布式系统的卖货节点,所以写好了module1后,直接复制出module2即可,改写端口号即可。

5.1.2 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.atguigu.redislock</groupId>
    <artifactId>redis_distributed_lock2</artifactId>
    <version>1.0-SNAPSHOT</version>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <lombok.version>1.16.18</lombok.version>
    </properties>



    <dependencies>
        <!--SpringBoot通用依赖模块-->
        <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.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--通用基础配置boottest/lombok/hutool-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.8</version>
        </dependency>
    </dependencies>

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

</project>

5.1.3 yaml文件

server.port=7777
# module2 lock3就把端口改为8888即可


spring.application.name=redis_distributed_lock
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

5.1.4 主启动类

package com.atguigu.redislock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @auther zzyy
 * @create 2022-10-12 22:20
 */
@SpringBootApplication
public class RedisDistributedLockApp7777
{
    public static void main(String[] args)
    {
        SpringApplication.run(RedisDistributedLockApp7777.class,args);
    }
}

5.1.5 业务类

Service

使用了java的ReentrantLock在各个单节点上实现了单机锁。

package com.atguigu.redislock.service;

import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {
        String retMessage = "";
        lock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            lock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
Controller
//1.0 2.0版本 //1.0版本 只使用该方法实现单机加锁,搭配nginx,人工手动点击销库存,貌似没有问题
    //2.0版本 使用该方法,搭配nginx 和jmeter进行压测,发现出现了超卖情况,100个线程一秒钟卖出100个商品
    //执行了jmeter发现,还有30个。由此说明,此处的锁只能一个jvm下的线程,对于另一个端口的售卖线程,我们的锁是锁不住的。
    //从两个服务的输出日志也可以看出,都出现了库存剩余97的情况,说明,第98个产品被卖了两次,出现了问题。
    //应当使用redis分布式锁来避免该问题
package com.atguigu.redislock.controller;

import cn.hutool.core.util.IdUtil;
import com.atguigu.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicInteger;

@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController
{
    @Autowired
    private InventoryService inventoryService;

    @ApiOperation("扣减库存,一次卖一个")
    @GetMapping(value = "/inventory/sale")
    public String sale()
    {
        return inventoryService.sale();
    }
}
 

5.3 测试

5.3.1 简单测试(版本1)

打开swagger地址

http://localhost:7777/swagger-ui.html#/

另一个http://localhost:8888/swagger-ui.html#/

7777端口测试(module1测试)

测试正常

8888端口测试(module2测试)

与7777端口的module测试同理 

5.3.2 nginx测试负载均衡(版本2)

nginx配置负载均衡实现轮询
修改配置文件

找到nginx安装目录下conf下的nginx.conf,修改该文件

注意upsream中的ip地址是两个module服务的地址

#启动几个worker进程?
worker_processes  1;

events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;     
    sendfile        on;
    keepalive_timeout  65;
	
	
	upstream mynginx {
		server 192.168.101.96:7777 weight=1;
		server 192.168.101.96:8888 weight=1;
	}
	
    server {
        listen       80;
        server_name  mynginx;     
		
        location / {
			proxy_pass http://mynginx;
            index  index.html index.htm;
        }
		
		
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
		
    } 
}
关闭nginx(如果在运行中的话)

cd到nginx的sbin目录下,执行以下语句

./nginx -s stop

为nginx指定配置文件

cd到nginx的sbin目录下,执行以下语句

./nginx -c /usr/local/nginx/conf/nginx.conf

重启nginx

cd到nginx的sbin目录下,执行以下语句

./nginx -s reload

开始测试

测试地址

http://192.168.101.96/inventory/sale

nginx负载均衡 轮询实现成功!

5.3.3 jemeter压力测试

以上测试都是很普通的单点测试,面对高并发理论上在分布式系统上只使用单机的锁是无法实现数据一致性的。测试后发现,确实如此,出现了超卖情况。

5.4 该锁出现超卖的原因分析

根本原因是节点之间的锁不共享

  • 单机锁的作用范围仅限于单个节点,无法在多个节点之间共享锁状态。
  • 这意味着即使一个节点上的锁能够有效地控制并发访问,另一个节点上的锁并不知道当前资源的状态。而一旦俩节点同时处理了同一状态的资源,那就可能会发生超卖。

5.5 如何改进

使用分布式锁,让各个节点间的锁信息共享。

6 实现redis的分布式锁

6.1 版本三 引入分布式锁

6.1.1 改进内容

1,2版本的锁是单机的,无法实现分布式的数据一致性,故改进为分布式锁。

如何实现?

两节点都连接了redis,那么,如果节点每次获取锁都需要通过查询redia的某个状态才能拿到,不就是达成了锁信息共享的效果了吗?因此可以考虑使用setnx命令,如果set成功,那说明目前锁没人拿,申请者就可以大方的拿,如果拿不到就等待并继续获取,直到获取锁即可。

6.1.2 实现

实现要点
  • 在没有获取到锁,重新获取的实现逻辑中,最好使用while循环(自旋),而非递归,这样能有效降低栈溢出的风险。递归会不断的在方法中向内开辟新的方法层,不断递归非常容易导致栈溢出。
  • 注意要使用setnx命令也就是setIfAbsent方法
  • setnx设置的key要保证各个节点之间都一样,否则又是回退到了单机锁的状态,完全不是分布式锁了。
  • setnx设置的value可以考虑设置成uuid,方便查阅,保证唯一。
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
            //暂停20毫秒,类似CAS自旋
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
 

 6.1.3 不足之处

根据分布式锁满足的条件来看,我们满足了互斥性(任意时刻,只有一个客户端能获取到锁),但是否满足防死锁呢?如果在try过程中宕机,还没走到finally,那么这个key就会一直存在,其他服务将无法获取到锁。这便失去了分布式集群部署的意义,明明设置了售卖的集群服务,但挂了一个,还导致了其他服务也失效了,这是我们不想看到的,因此要考虑改进,实现防死锁。

6.2 版本四 分布式锁防死锁的改进

6.2.1 介绍

改进内容

3版本是不能防止死锁的,故考虑改进。

如何实现?

如果它出现不能自己释放锁的情况了,我们又想帮他释放,那就可以考虑用redis的过期自动失效的功能来实现。

6.2.2 实现

实现要点
  • 注意在setIfAbsent的方法中要新增过期时间,不要分步骤实现,要在一个语句中实现设置key value和过期时间的逻辑,否则不能保证原子性
    //4.0版本 加上设置过期时间的兜底设计,防止服务宕机时的死锁问题.
    //4.0问题:如果A线程拿到了锁,且在设置过期时间内,A没有完成业务,此时,锁会过期被释放,B就会拿到锁,A就会跑去把新的锁给删掉
    //也就是出现了 =====[删错锁的问题]===== A线程误删了B线程的锁 这样进行下去,还是会出现超卖现象.
    //原因 实际业务时间超过了设置的过期时间
    //结果 删除了错误的,不属于自己的锁
    //解决 只允许自己删除自己的锁
public String sale() {
        String retMessage = "";
        String key = "fyRedisLock";
        //用uuid和线程id 避免重复
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //setnx 代替 抢锁行为 =================除此之外,还要[加上过期时间],防止死锁=====================
        //设置key和设置过期时间要揉在一起,保证原子性,不要设置完key再去加锁.
        while (!redisTemplate.opsForValue().setIfAbsent(key, value,20,TimeUnit.SECONDS)) {
            //延迟20ms后,重新抢锁
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //[直接在while内重新抢锁,直到抢到锁]
        }
        //=====================走到这里说明拿到锁了,开始处理业务=============================
        try {
            //1.查询库存信息 这个key我们提前在redis创建好了 初始库存是100
            String result = redisTemplate.opsForValue().get("inventory001");
            //2.判断库存是否足够,不够的话设置为0,够的话顺便转成Integer
            Integer inventory = result == null ? 0 : Integer.parseInt(result);
            //3.操作库存
            if (inventory > 0) {
                //4.更新库存
                redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventory));
                //5.返回结果
                retMessage = "恭喜你,成功购买一个商品,当前库存为:" + inventory;
                System.out.println(retMessage + "\t服务端口号:" + port);
            } else {
                retMessage = "对不起,购买失败,没有库存了!";
                System.out.println(retMessage + "\t服务端口号:" + port);
            }
        } finally {
            //执行完业务,需要释放锁
            redisTemplate.delete(key);
        }
        return retMessage + "\t服务端口号:" + port;
    }

6.2.3 不足之处

虽然现在保底是能解锁,防止死锁了,但这会有一个潜在的问题,我们随意地设置过期时间是很鲁莽的,例如有一个线程A,获取锁后,执行业务的过程中,突然key过期了,把锁释放了,等于是解除了互斥性,此时线程B来获取锁,直接就能获取到,这是不应该的。等于是同一时间AB都有一把锁,不满足互斥性。

另外,若key已经过期,A执行完还会去解锁,由于我们的锁信息key是一样的,A去解锁时就会把其他线程的锁给解了,不满足防止误删锁的性质。

6.3 版本五/六  分布式锁的防误解锁改进

6.3.1 介绍

改进内容

4版本设置过期时间虽能防死锁,但也可能过早的释放锁(过期时间小于了业务执行时间),此时再去解锁,就会把别的线程的锁给解锁掉,故应当改进为只能解锁自己的锁。

如何实现?

finally中我们解锁的时候判断一下当前锁是谁的,不是自己的就不解锁。

6.3.2 实现

实现要点
  • 其余逻辑都不需要改动,只需要改动finally中的,注意在方法进入后,提前留存key的value值,方便解锁时的判断逻辑来使用。
  • 解锁时,判断和解锁不是原子性的,很可能我们判断的时候还没时效呢,哎,进入if语句了,一进来,哎,key刚好失效,哎,刚好key又被别人给抢到了,那我们一解锁,又解锁成了别人的锁,故要考虑在redis中使用lua脚本来保证原子性。
  • 分步骤解锁是不安全的,应当改进为lua脚本原子操作来执行。
  • lua脚本执行时,要在execute方法中 使用stringRedisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(key),value);    new DefaultRedisScript<>(script,Long.class)这个构造器,而不要使用new DefaultRedisScript<>(script)这个构造器,否则会报错
分步骤解锁(不满足原子性)
//用uuid和线程id 避免重复
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//   .... 这里是获取锁和trycatch代码

finally {
            //执行完业务,需要释放锁
            //===============判断一下是不是自己的锁,不是就不管了,是自己的才删除====================
            if (redisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
                redisTemplate.delete(key);
            }
lua脚本解锁key(保证原子性) 
public String sale() {
        String retMessage = "";
        String key = "fyRedisLock";
        //用uuid和线程id 避免重复
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //setnx 代替 抢锁行为 =================除此之外,还要[加上过期时间],防止死锁=====================
        //设置key和设置过期时间要揉在一起,保证原子性,不要设置完key再去加锁.
        while (!redisTemplate.opsForValue().setIfAbsent(key, value,20,TimeUnit.SECONDS)) {
            //延迟20ms后,重新抢锁
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //[直接在while内重新抢锁,直到抢到锁]
        }
        //=====================走到这里说明拿到锁了,开始处理业务=============================
        try {
            //1.查询库存信息 这个key我们提前在redis创建好了 初始库存是100
            String result = redisTemplate.opsForValue().get("inventory001");
            //2.判断库存是否足够,不够的话设置为0,够的话顺便转成Integer
            Integer inventory = result == null ? 0 : Integer.parseInt(result);
            //3.操作库存
            if (inventory > 0) {
                //4.更新库存
                redisTemplate.opsForValue().set("inventory001", String.valueOf(--inventory));
                //5.返回结果
                retMessage = "恭喜你,成功购买一个商品,当前库存为:" + inventory;
                System.out.println(retMessage + "\t服务端口号:" + port);
            } else {
                retMessage = "对不起,购买失败,没有库存了!";
                System.out.println(retMessage + "\t服务端口号:" + port);
            }
        } finally {
            //执行完业务,需要释放锁
            //=================释放时,判断一下是不是自己的锁,不是就不管了,是自己的才删除======================
            //==================为了保证释放时的原子性,此处使用lua脚本来处理删除锁的逻辑=======================
            //lua脚本内容
            String luaScript = "if redis.call('get',KEYS[1])== ARGV[1] " +
                    "then return redis.call('del' ,KEYS[1]) " +
                    "else return 0 end";
            //完善脚本中占位的参数,并执行脚本
            redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), Lists.newArrayList(key), value);
        }
        return retMessage + "\t服务端口号:" + port;
    }

6.3.3 不足之处

对于并发度不高的情况下,6.0版本已经能达到不错的效果了,可以投入使用

但仍然有不足之处,根据分布式锁的必要条件,我们可以得出以下结论:

问题:

  • 没有实现解耦,这次我们要用redis锁,下次我们要用zookeeper的分布式锁,怎么办?故获取锁时应当实现解耦。
  • 没有解决可重入问题,如果方法内调用了需要该锁的方法,就需要重新再次拿到锁,进而会出现方法内调用方法的地方一直拿不到锁的问题。对于同一线程下(这里其实应该指同一个方法内)的非第一次获取锁,应当予以通过,自动的获取到锁,只需要最外层的方法拿到锁即可,此时,最外层方法内的其他方法可以凭借最外层方法拿到的锁获取执行许可。
  • 实际上4版本还有一个不满足互斥性的地方我们没有解决,也就是虽然4版本中key过期后,线程解锁的时候会判断后在解锁,但仍然没有解决同一时间会有两个客户端拿到锁的情况。

解决:

  • 自写redis锁和zookeeper等各种锁,要获取时,利用工厂类,来拿到锁。实现解耦。
  • 此外,由解决方案的第一点,我们还可以减少service中的代码,把防误删锁的判断,过期时间的续期,获取锁,解开锁等方法都移动到自研锁的类中,在service中调用他们即可。
  • 考虑用hash代替String实现分布式锁。要解决可重入,我们既要记录重入了几次(记录的目的是为了保证获取几次解锁几次),还要方便的保证能够可重入。那我们不应该还是用String类型,这个类型没有办法记录重入次数,很容易我们可以想到用hash类型,其他都和string类型一样,但hash多出来一个地方给我们使用,那正好可以用来记录重入次数。
  • 要解决不满足互斥性的问题,可能需要推翻我们的自动过期这一实现,只要我还在业务执行期间,你就不能帮我解锁(也就是key失效),而是要帮我滚动过期时间,直到我的业务执行完成。(业务卡死怎么办?可以设置最大时间,到了最大时间还解不了锁,就抛异常或者服务降级等。。。总之一直卡着一直续期也是不太合理的)

7 总结

以上是分布式锁的基础用法,面对一般情况下的高并发,可以在生产中使用,如果要应对更高级别的并发,或者实现更完整的分布式锁,还需要解决第六版本遗留的两个问题。

8 面试题(带答案)

Redis除了常用来做缓存,你还见过基于Redis的什么用法?

  1. 数据共享

    • 数据类型:String、Hash
    • 实现:利用 Redis 的快速读写特性,将需要在多应用间共享的数据存储在 Redis 中。例如,配置参数、用户信息等,可以通过 String 或 Hash 存储。
  2. 分布式Session

    • 数据类型:String、Hash
    • 实现:将用户的 Session 信息存储在 Redis 中,实现多台服务器共享用户状态。使用唯一的 Session ID 作为键,Session 数据作为值。
  3. 分布式锁

    • 数据类型:String
    • 实现:使用 SETNX 命令和过期时间实现互斥锁,确保在分布式环境下对共享资源的安全访问。
  4. 全局ID生成

    • 数据类型:String
    • 实现:使用 Redis 的自增(INCR)命令生成全局唯一的 ID,适用于分布式系统中的唯一标识生成。
  5. 计数器、点赞

    • 数据类型:String、Hash
    • 实现:使用 Redis 的自增(INCR)和自减(DECR)命令进行计数操作,适用于计数、点赞等功能。
  6. 位统计

    • 数据类型:Bitmaps
    • 实现:使用 Bitmaps 进行位操作,适用于布尔值统计、签到等场景。
  7. 购物车

    • 数据类型:Hash、List、Set
    • 实现:利用 Hash 存储用户购物车信息,商品 ID 作为字段,数量作为值。
  8. 轻量级消息队列

    • 数据类型:List
    • 实现:使用 Redis 的 List 作为消息队列,使用 LPUSHRPOP 进行消息入队和出队操作。
  9. 抽奖

    • 数据类型:List、Set
    • 实现:将参与者存储在 List 或 Set 中,通过随机数生成器选取中奖者。
  10. 用户关注、可能认识的人

    • 数据类型:Set、Sorted Set
    • 实现:使用 Set 存储用户的关注列表,通过 Set 的交集、差集运算实现推荐关系。
  11. 热点新闻、热搜排行榜

    • 数据类型:Sorted Set
    • 实现:使用 Sorted Set 进行排序,按照新闻、搜索词的热度得分排序。

Redis做分布式锁的时候有需要注意的问题?

  1. 锁的原子性:确保获取锁和设置过期时间是一个原子操作,避免因客户端故障导致锁无法释放。
  2. 锁的过期时间:设置合理的过期时间,防止死锁。
  3. 所有权验证:确保只有持有锁的客户端才能释放锁。
  4. 锁的可重入性:解决同一客户端多次获取锁的问题。
  5. 避免单点故障:使用 Redis 集群或其他方式确保高可用性。

你们公司自己实现的分布式锁是否用的setnx命令实现?

是的,我们使用 SETNX 命令实现分布式锁。但也可以考虑使用更好的hincryby命令来实现。

这个是最合适的吗?你如何考虑分布式锁的可重入问题?

SETNX 命令加上过期时间的方式可以有效地实现分布式锁,但需要注意上述的问题。为了实现分布式锁的可重入性,可以在获取锁时使用一个唯一标识(如 UUID)并将其存储在锁中,只有该唯一标识的客户端可以再次获取和释放锁。

或者用hash的数据结构,利用其hash结构的map中的value值来记录重入次数,以此可以更方便的解决可重入问题。

如果是Redis是单点部署的,会带来什么问题?

  1. 单点故障:如果 Redis 节点宕机,所有锁信息将丢失,导致系统不可用。
  2. 扩展性差:单点 Redis 的性能和容量有限,无法满足高并发、大数据量的需求。

那你准备怎么解决单点问题呢?

  1. 主从复制:使用 Redis 的主从复制机制,确保数据的高可用性和持久性。
  2. 哨兵模式:使用 Redis Sentinel 实现高可用性,自动故障转移。
  3. 集群模式:使用 Redis Cluster 实现数据的水平扩展,提高系统的可用性和扩展性。

Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?

在 Redis 的主从模式下,CAP 定理中的一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)之间需要权衡:

  1. 一致性问题:主从复制是异步的,可能会导致读操作返回的结果不一致。
  2. 可用性问题:如果主节点故障,需要一定时间进行故障转移,期间系统可能不可用。
  3. 分区容忍性:Redis 集群可以通过分片和复制来提高分区容忍性,但需要权衡一致性和可用性。

那你简单的介绍一下Redlock吧?

Redlock 是 Redis 作者提出的一种基于 Redis 的分布式锁算法,用于解决分布式环境下锁的可靠性问题。它通过在多个独立的 Redis 实例上获取锁来实现高可靠性和容错性。基本步骤如下:

  1. 在 N 个 Redis 实例上尝试获取锁。
  2. 获取锁时,使用相同的唯一标识和过期时间。
  3. 如果在大多数实例(如 N/2+1)上成功获取锁,则认为锁获取成功。
  4. 如果获取失败,则释放已获取的锁。
  5. 锁的持有时间必须短于大多数实例的过期时间。

你简历上写redisson,请你介绍一下你是怎么使用的。

Redisson 是一个基于 Redis 的 Java 驻内存数据网格,用于分布式和并行处理应用。使用 Redisson 可以方便地实现分布式锁、同步器、缓存等功能。这是一个使用 Redisson 实现分布式锁的示例:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonLockExample {
    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        RedissonClient redisson = Redisson.create(config);

        RLock lock = redisson.getLock("myLock");
        lock.lock();

        try {
            // 业务逻辑
            System.out.println("Lock acquired, performing critical operation");
        } finally {
            lock.unlock();
            System.out.println("Lock released");
        }

        redisson.shutdown();
    }
}

Redis分布式锁如何续期?看门狗知道吗?

Redisson 提供了看门狗机制来自动续期锁。看门狗会在锁的过期时间到达之前自动续期,确保锁不会意外释放。这是一个使用看门狗机制的示例:

RLock lock = redisson.getLock("myLock");
lock.lock(10, TimeUnit.SECONDS); // 锁自动续期,看门狗机制会保持锁的持有
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

通过看门狗机制,可以避免锁因超时而被误释放,确保分布式锁的可靠性。

🎉 感谢大家的陪伴,旅程因你们而精彩!如果文章对你有帮助,我会感到很荣幸!👏

欢迎您访问主页:孤尘Java-优快云博客
👋 再见啦,朋友们!保持好奇,保持热情!🔥

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

孤尘Java

感谢认可!感谢您的打赏!

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

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

打赏作者

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

抵扣说明:

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

余额充值