Redis教程(3)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 你好看官,里面请!今天笔者讲的是Redis教程(3)。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途。

Redis教程(3)

 如果觉得写的还可以,点个赞支持一下笔者呗!你的点赞和关注会让我更快更新哦。笔者会持续更新关于Java和大数据有关的文章。目前集中精力在更新java框架的内容。


这个小节我们继续学习 Redis,在上个小节里我们学习了 Spring Boot 与 Redis 的整合以及简单的应用。在这一小节中我们将通过一个 Redis 在实际应用中的小案例来深入学习。这个案例就是在互联网业务场景中经常会用到的一个组件——分布式锁。

1. 什么是锁

1.1 锁的自我修养

作为一个演员要有演员的修养,同样,作为一把锁也要有锁的自我修养。那么下面我们来看一下,作为一把合格的锁,应该具备哪些性质:

  1. 互斥:锁具有独占性,一把锁在同一时刻最多只能有一个持有者;
  2. 安全:安全指的是解锁时的安全性,即只能解锁自己持有的锁;
  3. 不死锁:不能因为意外的发生,导致锁不能被正常释放。

1.2 传统锁的局限性

我们都知道 Java 为我们提供了锁相关的 API(synchronized、ReentrantLock 等)。但这些锁存在一定的局限性,在多线程(同一个 JVM)下可以从容应对,但是到了多进程(不同 JVM,甚至干脆不只有 JVM)的情况下,就有些力不从心了(感觉身体被掏空?)。

现在的业务场景早已经不是一个单体应用就能满足的时候了,随随便便就要整个集群搞个分布式啥的,再复杂一点的还会有异构平台的交互。既然传统的锁不能满足分布式应用的场景,于是聪明绝顶的程序员们就搞出来一个新的锁——分布式锁。

2. 分布式锁

要在分布式场景下实现锁的功能,那么这个锁就必须是独立于 JVM 之外的了。它必须是一个独立、客观、第三方的东西。那我们该用什么来实现分布式锁呢?往下看!

2.1 实现分布式锁的方式

目前市面上对于分布式锁的实现方式主要有以下 3 种方案:

  • 数据库(这种方式很少被使用了)
  • Redis
  • Zookeeper(Chubby,来自谷歌)

Chubby 是谷歌出品,是专门用来做分布式锁的,原理跟 Zookeeper 有些类似,暂且归为一类吧。据我了解目前国内使用的较少。

以上三种方案中,基于数据库的实现方案已经很少被应用在实际项目中了。原因很简单,性能是它最大的障碍。Redis 和 Zookeeper 这两种方案目前应用的最广泛。在这两者之间个人感觉(仅仅是个人感觉,没有任何证据)基于 Redis 的方案在国内应用更广,根据我胡乱的猜测(仅仅是猜测,依然没有证据)应该是因为用 Redis 实现起来比较简单。

2.2 实现原理

不管是哪种实现方案,其原理都差不多,只是所依赖的具体技术不同而已。三种方案都是基于对应技术的两个特性实现的分布式锁。

基于的两个特性:

  1. 操作的原子性;
  2. 资源的唯一性。

实现的 3 种方案:

  1. 数据库方式:乐观锁/悲观锁 + 唯一约束
  2. Redis 方式:setnx
  3. Zookeeper 方式:临时顺序节点

下面我们具体来看一下基于 Redis 的实现方案吧!

3. Redis 实现分布式锁

Redis 实现分布式锁,主要用到了它的一个命令—— SETNX。SETNX 是 SET if Not eXists 的缩写,即在指定的 key 不存在时,为 key 设置指定的值。并且当设置成功时返回 1 。 设置失败时返回 0 。因此,我们可以根据返回值来判断加锁是否成功。

3.1 第一版

根据上面我们分析 Redis 实现分布式锁的原理,我们先写出一个初始版本的 RedisLock:

@Component
public class RedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;
    public boolean lock(String key, String value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
    }
    public void unLock(String key) {
        redisTemplate.delete(key);
    }
}

RedisTemplate 中将 Redis 的 SETNX 命令封装成了 setIfAbsent() 方法,返回值也封装成了 Boolean。

下面在我们的业务代码中使用一下我们刚刚完成的 RedisLock:

@RestController
@RequestMapping("/redislock")
public class RedisLockController {
    private final long TIME_OUT = 50 * 1000;
    private final String REDIS_LOCK = "REDIS_LOCK";
    @Autowired
    private RedisLock redisLock;
    @GetMapping("/lock")
    public void lock() {
        // 加锁
        long currentTime = System.currentTimeMillis();
        boolean isLock = redisLock.lock(REDIS_LOCK, String.valueOf(currentTime + TIME_OUT));
        if (!isLock) {
            throw new RuntimeException("资源已被抢占,换个姿势再试试吧!");
        }
        // do something
        // 解锁
        redisLock.unLock(REDIS_LOCK);
    }
}

嗯,看起来还不错。但仔细想想这样写会不会有什么问题?

我相信聪明的你已经发现了问题所在,当 lock() 成功以后,在 do something 的过程中出现意外导致后面的 unLock() 没有被执行,那么就会导致其他请求无法再获得锁,从而造成了死锁。所以,它不是一把好锁。

3.2 进化版

我们想一下,如何解决上面死锁的问题?

首先,上面代码导致死锁的原因是,一旦程序执行出现意外,就无法删除对应的 key 了,这就会导致 key 会一直存在,最终的结果就是其他线程再也无法获得锁了。那么要解决这个问题就需要在不能删除 key 的情况下,也能够让这个 key 消失。嗯,于是我们可以想到了给 key 加上过期时间。

@Component
public class RedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;
    public boolean lockV2(String key, String value,Long timeOut) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, timeOut, TimeUnit.MILLISECONDS);
    }
}

Redis 从 2.6.12 版本开始,让 SETNX 操作的同时可以设置一个过期时间。不过不是直接通过 SETNX 命令实现的,而是通过 SET 命令加上 NX 参数来实现的。

现在这个方案完美了吗?显然还没有。假如有两个线程 A 和 B,在 A 执行完 do something 之后,恰好 key 到了过期时间,又恰好这时 B 获得了锁,那么接下来会发生什么?接下来 A 执行 unLock() 会将 B 获得的锁删掉!防不胜防呀!

3.3 再改进

上面我们遇到了一个问题,一个线程删除了不属于它的锁。那么要解决这个问题,就需要在删除之前先判断一下,当前的锁是不是被自己持有,如果是那么删除,如果不是,说明锁已经过期了(此时可能有别的线程持有了锁,也可能没有任何线程持有锁),则不需要再删除了。

@Component
public class RedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;
    public void unLockV2(String key, String value) {
        String oldValue = redisTemplate.opsForValue().get(key);
        if (Objects.nonNull(oldValue) && oldValue.equals(value)) {
            redisTemplate.delete(key);
        }
    }
}

OK,这次我们在删除之前对锁的持有者进行了判断,只有确定自己是锁的持有者才去释放锁。这次看起来没什么毛病了,但是很遗憾,判断持有者的逻辑和删除 key 的逻辑仍然不是一个原子的操作。虽然这两个操作之间的间隔非常短,但仍有可能在这两个操作之间被其他线程干扰。

虽然仍不完美,但相对于上面执行完业务代码后直接删除 key 的方式的可靠性已经提升了 N 个数量级了。因为一般的业务逻辑执行耗时都在几百毫秒上下,而判断所有者的逻辑与删除 key 的逻辑间隔在微秒级别,而时间越短,出错的概率就会越低。

4. Redis 分布式锁的其他实现方式

Lua 脚本

Redis 通过调用 Lua 脚本,可以实现更加强大与复杂的功能。而且在执行 Lua 脚本的时候操作具有原子性。这恰好可以用来实现分布式锁,上面例子中解锁时的逻辑漏洞可以通过调用 Lua 脚本来避免。

通过 RedisTemplate 的 execute() 方法来调用 Lua 脚本

Redission

Redission 是一个基于 Redis 的第三方组件,提供了很多强大的功能,也是 Redis 官方推荐的分布式锁解决方案。基于一种叫做 RedLock 的算法实现的分布式锁,比起我们自己实现分布式锁,Redission 更加的安全可靠,所以在生产环境中更加推荐使用 Redission 来作为分布式锁的解决方案。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
存储 缓存 NoSQL
熟练使用 Redis 的五大数据结构:Java 实战教程
熟练使用 Redis 的五大数据结构:Java 实战教程
108 0
|
7月前
|
NoSQL Go 数据库
2023最新版 Navicat 16.2+系列安装和试用教程详解:轻松掌握最新版本的数据库管理工具连接Redis
2023最新版 Navicat 16.2+系列安装和试用教程详解:轻松掌握最新版本的数据库管理工具连接Redis
305 0
|
3天前
|
NoSQL Linux 网络安全
Linux安装Redis(详细教程)
Linux安装Redis(详细教程)
25 2
|
3天前
|
NoSQL Linux Redis
【redis】安装和使用教程
【redis】安装和使用教程
21 1
|
3天前
|
NoSQL 前端开发 Redis
Windows 下安装和配置 Redis (图文教程)
Windows 下安装和配置 Redis (图文教程)
|
6月前
|
缓存 NoSQL Java
分布式系列教程(19) -基于Redis实现分布式锁
分布式系列教程(19) -基于Redis实现分布式锁
80 0
|
6月前
|
缓存 NoSQL 安全
分布式系列教程(05) -分布式Redis缓存 (持久化)
分布式系列教程(05) -分布式Redis缓存 (持久化)
113 0
|
6月前
|
缓存 NoSQL Java
分布式系列教程(03) -分布式Redis缓存(SpringBoot整合Redis)
分布式系列教程(03) -分布式Redis缓存(SpringBoot整合Redis)
106 0
|
7月前
|
NoSQL Linux 网络安全
linux安装redis超级详细教程
linux安装redis超级详细教程
|
5月前
|
存储 NoSQL Unix
Redis安装教程(保姆级教程)
Redis安装教程(保姆级教程)