Redis实现分布式锁

Mr.LR2022年5月29日
大约 7 分钟

redis实现分布式锁

1、为什么要用分布式锁

要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。

  • 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
  • 进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
  • 分布式锁:当多个进程不在同一个系统中(比如分布式系统中控制共享资源访问),用分布式锁控制多个进程对资源的访问。

2、设计分布式锁需要的条件和刚需

  1. 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有。
  2. 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
  3. 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作。
  4. 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。
  5. 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

3、设计分布式锁

这里以一个案例的演变,加深对分布式锁设计思想的理解

入门案例

新建Module

boot_redis01和boot_redis02

pom

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.10.RELEASE</version>
        <relativePath/>
    </parent>
<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>

yml

# 端口号
server:
  port: 1111 #两个服务1111和2222
# ========================redis相关配置=====================
spring:
  redis:
    host: localhost
    port: 6379
#    password: #默认为空

主启动

@SpringBootApplication
public class BootRedis01Application {
    public static void main(String[] args) {
        SpringApplication.run(BootRedis01Application.class, args);
    }
}

业务类controller

@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;
}

测试

image-20220620173210366

设计案例演变

1.单机版

上面的入门案例,由于buy_Goods方法没有加锁,因此在单机模式下就会出现线程安全问题。

解决办法:加锁

@GetMapping("/buy_goods")
public String buy_Goods() {
    synchronized (this) {//**解决办法**
        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;
    }
}

2.分布式场景下

在单机环境下,可以使用synchronized或Lock来实现。

但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)

解决方案:通过redis实现分布式锁

@GetMapping("/buy_Goods2")
public String buy_Goods2() {
    String key = "key_lock";
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

    try {
        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 + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
    } finally {
        stringRedisTemplate.delete(key);//释放锁
    }
}

3.删除锁时加判断

加锁和解锁需要操作同一个线程,防止张冠李戴,删除掉别人的锁

@GetMapping("/buy_Goods3")
public String buy_Goods3() {
    String key = "key_lock";
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        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 + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
    } finally {
        if (stringRedisTemplate.opsForValue().get(key).equals(value)) {//判断value是不是当前线程加的锁
            stringRedisTemplate.delete(key);
        }
    }
}

4.加有效时间

分布式锁必须加有效时间,防止宕机无法释放锁。

注意:加锁和设置有效期需要保证原子性,Redis调用Lua脚本通过eval命令保证代码执行的原子性

@GetMapping("/buy_Goods4")
public String buy_Goods4() throws Exception {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("redisLockPay", value, 30L, TimeUnit.SECONDS);
        if (!flag) {
            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 + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
    } finally {
        Jedis jedis = RedisUtils.getJedis();

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

        try {
            Object result = jedis.eval(script, Collections.singletonList("redisLockPay"), Collections.singletonList(value));
            if ("1".equals(result.toString())) {
                System.out.println("------del REDIS_LOCK_KEY success");
            } else {
                System.out.println("------del REDIS_LOCK_KEY error");
            }
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }

    }
}

至此,基于单个Redis节点实现分布式锁完成。

5.最终版本

redis集群环境下,第四个版本也不行,需要RedLock之Redisson落地实现

RedisConfig

@Bean
public Redisson redisson() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
    return (Redisson) Redisson.create(config);
}

controller

@Autowired
private Redisson redisson;

@GetMapping("/buy_goods5")
public String buy_Goods5() {
    String key = "key_lock";
    RLock redissonLock = redisson.getLock(key);
    redissonLock.lock();
    try {
        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;
    } finally {
        if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {//redissonLock还持有锁,并且持有锁的是当前线程,才可以释放锁
            redissonLock.unlock();
        }
    }
}

参考

上次编辑于: 2023/2/28 00:12:58
贡献者: liurui,liurui-60837