栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Redis实现分布式锁的姿势

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Redis实现分布式锁的姿势

Redis实现分布式锁核心原理以及利弊总结 一、关键点
  1. 原子性
  2. 过期时间
  3. 锁续期
  4. 正确释放锁

下面我会介绍它们的意义以及作用

原子性

言简意赅,用转账服务举例,扣款和增加余额两个操作,要么都成功,要么都失败

redis使用setnx(key,value)来进行加锁,其中,key为资源ID,value可以存放操作的用户id

可能会引起的问题:

  1. 如果锁没来得及释放就宕机了,会出现死锁

解决方案:给key设置个过期时间:expire(key,time,unit);

也就是两条命令

redis.set(key,value);
redis.expire(key,time,unit);

那么,如果加锁成功后还没设置过期时间就宕机了,岂不是还是死锁了?

所以,原子性是redis分布式锁的关键点之一

过期时间

不带过期时间可能发生死锁,上文已经提到

锁续期(著名的看门狗机制)

假设锁设置过期时间为3s,但是业务执行需要4s,那么业务还没执行完锁就释放了,肯定不行,于是引入锁续期的概念

解决方案:开辟另外一个线程,专门用于锁续期,上锁的时候起个线程进行死循环续期,核心流程是判断锁的时间过了三分之一就给他重新续期为上锁时间,比如设置的锁是3s,过了1s还没执行完则重新设置为3s

正确释放锁

也是锁续期就能解决该问题,具体伪代码

private volatile boolean isRunning;

// 抢锁成功
if (RESULT_OK.equals(client.setNxPx(key, value, ttl))) {
    // 续期
    renewalTask = new RenewTask(new IRenewalHandler() {
        @Override
        public void callBack() throws LockException {
            // 刷新值
            client.expire(key, ttl <= 0 ? 10 : ttl);
        }
    }, ttl);
    renewalTask.start();
}

// 续期线程的逻辑
@Override
public void run() {
    while (isRunning) {
        try {
            // 1、续租,刷新值
            call.callBack();
            LOGGER.info("续租成功!");
            // 2、三分之一过期时间续租
            TimeUnit.SECONDS.sleep(this.ttl * 1000 / 3);
        } catch (InterruptedException e) {
            close();
        } catch (LockException e) {
            close();
        }
    }
}

public void close() {
    isRunning = false;
}

问题 2:客户端 A 上了锁,客户端 B 搞破坏,上来就 unlock 解锁,这时候直接执行redis.del(key)将客户端 A 的锁给释放了。这就相当于释放了别人的锁,这种情况怎么解决呢?

首先我们应该想一个问题:客户端 B 释放锁的时候为什么会释放掉客户端 A 的锁呢?因为锁的 key 肯定是一样的,毕竟锁的是同资源嘛。那么就需要从 value 入手了,解锁前先判断下这个 key 的 value 是不是自己加的,value 不能是线程 ID,因为分布式环境线程 ID 会重复,所以可以换成类似 userID 等业务主键;或者也可以换成随机数,因为加锁、解锁都在一个方法里,解锁的时候是可以得到这个随机数的。其伪代码如下:

// 判断订单 ID 的锁是自己上的方可释放
if((userId).equals(stringRedisTemplate.opsForValue().get(orderId))) {
    stringRedisTemplate.delete(orderId);
}

这样行吗?肯定不行的,因为判断里的 Redis 获取操作和 del 操作是非原子的,如果你设置了超时时间,而你的业务在超时时间内没有执行完,那么这个锁就会被释放,其他线程拿到锁,以上恰好发生在 get 之后、del 之前,会删除其他的锁,那么是不是就脏读了呢?但是如果有续期的话就不存在此问题。但还是尽量用 lua 脚本,lua 脚本如下:

// 如果get的值等于传进来的值,就给它del
if redis.call("get",KEYS[1])==ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end 

所以,什么叫“正确释放锁”?一言以蔽之:利用锁续期机制防止出现业务没执行完成锁就到期了、提前被释放的情况,且释放锁的时候还要判断是不是自己加的锁,避免误释放了别人的锁。

二、Redis实现分布式的几种部署方式 1、单机

直接部署一台Redis,然后单机加锁

  • 优点:简单,部署、使用都很方便
  • 缺点:宕机直接GG,单点故障
2、哨兵

使用哨兵机制,故障时自动主从切换

存在问题:锁写到Master时还没同步到Slave时Master挂了,随后选举了新的Master,但是新master无锁,不安全

3、集群

集群只是做了slot分片,实际还是单机

4、红锁(Redlock)

是Redis实现分布式锁方式中相对安全的手段

核心思路:搞几个独立的Master,比如5个,然后挨个加锁,只要超过一半以上(5/2+1)就代表加锁成功,释放时也逐台释放,好处在于一台master挂了还有其他的,不耽误,但并不是100%安全

三、Redis 实现分布式锁的优缺点

凡事都有两面性,Redis 实现分布式锁亦然。

其优点包括:

  1. 大多数互联网企业都在用 Redis,所以无需额外引入其他中间件,节省了成本开销;
  2. Redis 的性能还是很高的,所以对业务系统的性能影响相对较小;
  3. Redisson 客户端类库将锁的所有用法都封装了(可重入锁、读写锁、公平锁等),用户调用就完事了,所以支持得比较友好。

另外,其缺点有如下:

  1. Redis 虽然提供了很多种部署架构,单机、哨兵、集群以及著名的红锁(RedLock),但是没有哪一种是 100% 安全的。
  2. 红锁(RedLock)虽然是相对最安全的一种方式,但是它极其麻烦,而且对性能的开销也是其他锁的 N 倍,因为需要逐个加锁,需要多次与 Redis 通信。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1007327.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号