在分布式系统中,实现对共享资源的安全访问是一个关键问题。Redis作为一种高性能的内存数据库,提供了多种方式来实现分布式锁,以解决多个节点之间对共享资源的并发访问问题。

本文将介绍五种Redis分布式锁的解决方案及其原理、应用场景以及Java代码的实现步骤。

1、SETNX

SETNX(SET if Not eXists)命令是Redis提供的一种原子操作,用于设置一个键值对,当键不存在时才设置成功。利用该命令可以实现基本的分布式锁。

SETNX lock_name true

1.1、原理

  • 当节点需要获取锁时,它尝试通过 SETNX 命令设置一个特定的键作为锁。
  • 如果设置成功,表示获取到了锁,可以执行访问共享资源的操作。
  • 用完共享资源使用 DEL 命令释放锁。

1.2、代码实现

Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "my_distributed_lock";
String identifier = UUID.randomUUID().toString(); boolean lockAcquired = jedis.setnx(lockKey, identifier) == 1;
if (lockAcquired) {
// 成功获取锁
// 执行访问共享资源的操作
// ...
jedis.del(lockKey); // 释放锁
}

1.3、缺点

死锁:可能因网络等原因 DEL 命令执行失败而造成锁无法释放。

2、SET(NX EX)

避免死锁解决方案是为锁设置一个TTL(Time To Live)。

SET 命令支持选项:

NX:表示只在键不存在时才设置。

EX:表示设置键的过期时间(秒)。

SET lock_name arbitrary_lock_value NX EX 10

在上面的命令中,NX 与 SETNX 中的含义相同,而 EX 10 表示 TTL 为 10 秒。

2.1、原理

  • 当节点需要获取锁时,它通过SET命令的(NX EX)选项尝试设置一个特定的键作为锁,并设置锁的过期时间。

  • 如果设置成功,表示获取到了锁,可以执行访问共享资源的操作。

2.2、代码实现

import redis.clients.jedis.Jedis;

public class DistributedLockExample {

    public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); String lockKey = "my_distributed_lock";
String identifier = "unique_id"; // Replace with a unique identifier for your node
int lockTimeout = 5000; // Lock expiration time in milliseconds // Try to acquire the lock
String result = jedis.set(lockKey, identifier, "NX", "EX", lockTimeout / 1000); if (result != null && result.equals("OK")) {
try {
// Successfully acquired the lock
System.out.println("Lock acquired, performing the task.");
} finally {
// Release the lock
jedis.del(lockKey);
}
} else {
// Failed to acquire the lock
System.out.println("Failed to acquire the lock.");
}
jedis.close();
}
}

2.3、缺点

线程A获得锁,在执行业务还未完成时TTL过期,线程A锁被释放,线程B获得锁。线程A后续为无锁执行业务,在线程A完成业务后,执行DE删除锁,因为KEY相同则会导致线程B的锁被删除,线程B后续操作则为无锁执行业务

如下图所示:

3、SET(NX EX) + 唯一ID

在设置key时,客户端应将唯一的 ID 添加到 kv 对。在删除key之前,检查这个 ID 以确定它是否仍然持有锁。如果 ID 不匹配,则表示该锁被其他客户端持有,当前客户端不应删除该key。

SET lock_name client_id NX EX 10

3.1、原理

  • 当节点需要获取锁时,它通过 SET 命令的 (NX EX) 选项设置一个特定的键作为锁,并设置锁的过期时间。
  • 在设置锁的同时,还设置一个唯一的标识(如uuid)为锁的值。
  • 当节点释放锁时,只有当锁的值与自己的标识相匹配时才释放锁。

3.2、代码实现

import redis.clients.jedis.Jedis;

public class DistributedLockExample {

    public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); String lockKey = "my_distributed_lock";
String identifier = UUID.randomUUID().toString();
int lockTimeout = 30000; // Lock expiration time in milliseconds // Try to acquire the lock
String result = jedis.set(lockKey, identifier, "NX", "EX", lockTimeout / 1000); if (result != null && result.equals("OK")) {
try {
// Successfully acquired the lock
System.out.println("Lock acquired, performing the task.");
} finally {
// Release the lock (if acquired by the same identifier)
String currentValue = jedis.get(lockKey);
if (currentValue != null && currentValue.equals(identifier)) {
jedis.del(lockKey);
}
}
} else {
// Failed to acquire the lock
System.out.println("Failed to acquire the lock.");
} jedis.close();
}
}

3.3、缺点

if (currentValue != null && currentValue.equals(identifier)) {
jedis.del(lockKey);
}

IF 条件和 DEL 是两个独立的操作。极端情况下,IF 条件判定可以释放锁,在执行删除锁操作前刚好TTL过期,其他线程获取锁执行,前者线程删除锁删除的依然是别的线程的锁。

4、SET(NX EX) + 唯一ID + Lua脚本

为了确保释放锁的原子性,可以使用Lua脚本来执行判断+删除操作(lua脚本的执行的原子的)。

lua脚本:

// 如果来自 Redis GET 操作的值等于传入的值,则删除键
if redis.call("get", "lock_name") == ARGV[1]
then
return redis.call("del", "lock_name")
else
return 0
end

4.1、代码实现

String lockKey = "my_distributed_lock";
String identifier = UUID.randomUUID().toString(); String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifier)); if (result == 1) {
// 成功释放锁
} else {
// 锁的标识不匹配,可能已被其他节点获取
}

5、Redisson

以上的方案还存在一个问题:不优雅。

若执行业务完成时间不固定,可能小于过期时间也可能大于过期时间,在代码中只能设置可能完成的预估最长时间。

5.1、开源解决方案: Redisson

一旦客户端持有锁,它就会启动一个WatchDog守护线程来定期检查锁是否存在。如果存在,守护线程将重置 TTL 以防止锁自动释放。这种策略被称为租赁策略。防止先于业务完成就释放锁。

5.2、代码实现

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("my_distributed_lock"); try {
boolean lockAcquired = lock.tryLock(acquireTimeout, lockTimeout, TimeUnit.MILLISECONDS);
if (lockAcquired) {
// 成功获取锁
// 执行访问共享资源的操作
// ...
} else {
// 获取锁失败,处理重试逻辑
}
} finally {
lock.unlock(); // 释放锁
}

6、总结

Redis分布式锁提供了多种解决方案,可以根据具体的应用场景和需求选择合适的方案。无论选择哪种方案,都需要考虑锁的安全性、原子性、续约和释放等问题,以确保在分布式环境中共享资源的安全访问。

Redis从入门到放弃(10):分布式锁的更多相关文章

  1. Redis 从入门到放弃

    Redis 从入门到放弃 http://www.iocoder.cn/Fight/Redis-went-from-getting-started-to-quitting/

  2. redis从入门到放弃 -> 简介&概念

    一.redis简介 Redis是一款开源的.高性能的键-值存储.它常被称作是一款数据结构服务器. 当值支持的主要数据类型为:字符串(strings)类型,括哈希(hashes).列表(lists).集 ...

  3. 一站式学习Redis 从入门到高可用分布式实践

    1:redis 是用c语言来实现的,速度快 持久化 单线程 复杂的数据类型有bitmap和hyperloglog和geo地理信息2:高可用.分布式 v2.8开始支持Redis-Sentinel(哨兵) ...

  4. redis 的惊群处理和分布式锁的应用例子

    在并发量比较高的情况下redis有很多应用场景,提升查询效率,缓解底层DBio ,下面列举两个平时开发中应用过的两个例子,欢迎各位一起讨论改进. 1 . redis 惊群处理 1.1 方案的由来 Re ...

  5. 关于redis在cluster模式化下的 分布式锁的探索

    背景 redis作为一个内存数据库,在分布式的服务的大环境下,占的比重越来越大啦,下面我们和大家一起探讨一下如何使用redis实现一个分布式锁  说明       一个分布式锁至少要满足下面几个条件 ...

  6. 【分布式锁】Redis实现可重入的分布式锁

    一.前言 之前写的一篇文章<细说分布式锁>介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现.自定义分布式锁注解以及需要注意的问题都没描述.本文就是详细说明如何利用 ...

  7. redis从入门到放弃 -> 部署方案

    单点部署方案 环境准备: [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) [root@ ...

  8. Spark入门实战系列--10.分布式内存文件系统Tachyon介绍及安装部署

    [注]该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取 .Tachyon介绍 1.1 Tachyon简介 随着实时计算的需求日益增多,分布式内存计算 ...

  9. redis中使用java脚本实现分布式锁

    转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/115.html?1455860390 edis被大量用在分布式的环境中,自 ...

  10. python从入门到放弃之进程锁lock

    # ### lock (互斥锁)"""# 应用在多进程当中# 互斥锁lock : 互斥锁是进程间的get_ticket互相排斥进程之间,谁先抢占到资源,谁就先上锁,等到解 ...

随机推荐

  1. 自动化运维工具-Ansible PlayBook

    自动化运维工具-Ansible PlayBook PlayBook基本概念 PlayBook的组成 PlayBook即"剧本","兵书"之意,PlayBook是 ...

  2. 2020-11-04:java里,总体说一下集合框架。

    福哥答案2020-11-04: 福哥口诀法:收马李色坤(Collection.Map.List.Set.Queue).李矢数链写(List:Vector矢量.ArrayList数组.LinkedLis ...

  3. 2021-01-19:mysql中,一张表里有3亿数据,未分表,其中一个字段是企业类型,企业类型是一般企业和个体户,个体户的数据量差不多占50%,根据条件把个体户的行都删掉。请问如何操作?

    2021-01-19:mysql中,一张表里有3亿数据,未分表,其中一个字段是企业类型,企业类型是一般企业和个体户,个体户的数据量差不多占50%,根据条件把个体户的行都删掉.请问如何操作?福哥答案20 ...

  4. 2022-01-03:比如arr = {3,1,2,4}, 下标对应是:0 1 2 3, 你最开始选择一个下标进行操作,一旦最开始确定了是哪个下标,以后都只能在这个下标上进行操作。 比如你选定1下标,

    2022-01-03:比如arr = {3,1,2,4}, 下标对应是:0 1 2 3, 你最开始选择一个下标进行操作,一旦最开始确定了是哪个下标,以后都只能在这个下标上进行操作. 比如你选定1下标, ...

  5. Hugging News #0526: Hugging Cast 发布第一期、邀请来认领自己的论文啦!

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...

  6. Python日期带时区转换工具类总结

    目录 1.背景 2. 遇到的坑 3. 一些小案例 3.1 当前日期.日期时间.UTC日期时间 3.2 昨天.昨天UTC日期.昨天现在这个时间点的时间戳 3.3 日期转时间戳 3.4 时间戳转日期 3. ...

  7. VLAN——提高网络性能、安全性和灵活性的利器

    前言 VLAN是Virtual Local Area Network的缩写,它是一种通过网络交换机虚拟划分局域网的技术.VLAN可以将一个物理局域网划分成多个逻辑上的虚拟局域网,各个虚拟局域网之间相互 ...

  8. 流量劫持 —— GZIP 页面零开销注入 JS

    前言 HTTP 代理给页面注入 JS 是很常见的需求.由于上游服务器返回的页面可能是压缩状态的,因此需解压才能注入,同时为了节省流量,返回下游时还得再压缩.为了注入一小段代码,却将整个页面的流量解压再 ...

  9. CANoe_系统变量的创建过程

    在Canoe中创建系统变量,可以用于定义和管理与CAN网络通信相关的参数和配置.遵循以下步骤: 1.打开Canoe 启动Canoe软件. 2.打开项目 在Canoe的菜单栏中,选择"File ...

  10. Java基础之基础语法与面向对象

    前言 小知识 Java由Sun公司于1995年推出,2009年Sun公司被Oracle公司收购,取得Java的版权 Java之父:James Gosling(詹姆斯·高斯林) 专业术语 JDK:jav ...