Redis实战—黑马点评项目—分布式锁

一、简介

(1)基本原理和特性 

在集群模式下,多个jvm将会有多个锁监视器,那么加在某一个线程的锁,将不会被其他jvm下的线程所看到。

而分布式锁,将使多个jvm共享一个锁监视器,让一个锁被多个进程可见。

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

 (2)实现方式

常见的有三种

 二、基于redis的分布式锁

利用redis的setnx操作设置一个若干秒后自动释放的锁,初级版分布式锁

但是,这样的逻辑还有可能会发生锁的误删问题,如图

 当线程1由于某种原因阻塞时间过长,导致锁提前释放,此时线程2拿到了锁并开始执行任务,若这时恰好线程1结束阻塞开始执行任务,并释放锁,这时线程2将丢失锁。这时线程3拿到锁,就出现了线程2、3同时操作的状况。

因此,需要在释放锁之前进行线程判断,仅当这个锁是当前线程的锁时,当前线程才能对锁进行释放。

 在存入线程标识时,为了解决线程id冲突的问题,采用uuid作为线程的标识。

此外,线程的阻塞还有可能发生在【判断是否是自己的锁】和【释放锁之间】,也会导致类似上一种问题。如图

 原以为一切顺利时,在判断是自己锁之后,刚要准备释放,线程1就发生了阻塞。这样,阻塞的时间过长,超出了锁的到期自动释放时间,线程2就拿到了锁,开始执行任务。若此时,线程1结束阻塞,便会不分青红皂白立即释放锁,线程3就会乘虚而入,与线程2并行。

为了使【判断是否是自己的锁】和【释放锁之间】操作具有原子性,首先想到了redis事务,但redis事务其实是一种批处理,若将这两者放在一个事务中,可能会导致释放失败。

更好的解决办法是:使用Lua脚本(Lua 是一种轻量小巧的脚本语言)。

那么,如何Lua语言来调用redis呢?其实,Lua具有很强的扩展性,提供了redis专门的接口来调用redis。如

-- 执行set name jack
redis.call('set','name','jack')

更多信息可查看菜鸟教程:Lua 教程 | 菜鸟教程

 分析释放锁的流程主要是:获取当前锁中的线程id——>获取当前线程id——>对比一致——>释放锁。编写lua脚本

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET',KEYS[1]) == ARGV[1]) then
	-- 一致,则删除锁
	return redis.call('DEL', KEYS[1])
end
-- 不一致 则直接返回
return 0

Redis 脚本使用 Lua 解释器来执行脚本,执行脚本的常用命令为 EVAL

 那么在java代码中,eval对应的api是stringRedisTemplate中的execute。

【 定义接口和工具类】

public interface ILock {
    boolean tryLock(long timeoutSec);
    void unlock();
}
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate){
        this.name=name;
        this.stringRedisTemplate=stringRedisTemplate;
    }

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }//初始化脚本

    @Override
    public boolean tryLock(long timeoutSec) {

        String threadId =ID_PREFIX+ Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
        //若直接返回Boolean类型的success将会进行拆箱,将会有安全风险
    }

//使用lua脚本来让释放锁具备原子性
    @Override
    public void unlock() {
        //加载lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),//当前锁的名称,传入脚本中,拿到锁中的线程id
                ID_PREFIX+ Thread.currentThread().getId()//当前线程id
                );
    }

//原 不具备原子性的释放锁操作
    /*@Override
    public void unlock() {
        //获取当前线程id
        String threadId =ID_PREFIX+ Thread.currentThread().getId();
        //获取当前锁中的线程id
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //判断当前线程id是否与当前锁的中存放的线程id一致
        if(threadId.equals(id)){
            //释放
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
    }*/
}

修改原秒杀券订单代码,调用分布式锁,给创建订单业务加锁。

        //优惠券足够 创建订单
        Long userId = UserHolder.getUser().getId();
        //创建redis分布式锁 代替悲观锁
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        boolean isLock = lock.tryLock(1200);
        if(!isLock){
            //获取锁失败
            return Result.fail("不能重复下单");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }

三、Redisson的分布式锁

 如图,基于setnx命令实现的分布式锁还存在着这样四类问题。为了解决这些问题,我们选用现成的工具包Redisson作为替代方案。

(1)Redisson是个在Redis的基础上实现的Java驻内存数据网格 (In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现,包括可重入锁、公平锁、联锁、红锁等。

使用redisson分布式锁主要分3步:

1、导入依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

2、配置客户端

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.126.128:6379").setPassword("123456");
        return Redisson.create(config);
    }

3、使用分布式锁

装配客户端

@Resource
private RedissonClient redissonClient;

调用锁

RLock lock = redissonClient.getLock("locak:order:" + userId);
boolean isLock = lock.tryLock();

(2)redisson锁的可重入性原理

锁数据结构:哈希

 内置的lua脚本示意

获取锁

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; --线程唯一标识
local releaseTime = ARGV[2];-- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then
    -- 不存在,获取锁
    redis.call('hset', key, threadId, '1');
    -- 设置有效期
    redis.call('expire', key, releaseTime) ;
    return 1;-- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
    -- 不存在,获取锁,重入次数+1
    redis.call('hincrby', key, threadId, '1') ;
    --设置有效期
    redis.call('expire', key, releaseTime);
    return 1;-- 返回结果
end;
return 0;-- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; --线程唯一标识
local releaseTime = ARGV[2];-- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS',key, threadId) ==0) then
    return nil;-- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0
if(count > 0) then
-- 大于0说明不能释放锁,重置有效期然后返回
    redis.call('EXPIRE', key, releaseTime);
    return nil;
else
    --等于0说明可以释放锁,直接删除
    redis.call('DEL',key);
return nil;
end;

(2)redisson锁的可重试性

对于tryLock()方法,可以传三个参数:

long waitTime, long leaseTime, TimeUnit unit
//重试等待时间,锁自动释放时间,时间单位

当传入waitTime即等待时间后,则会在等待时间内进行重试获取锁:基于释放锁的发布和订阅,当任一进程释放锁时会进行发布,当前进程则订阅这一消息,获得释放消息后比对自己剩余等待时间,未超过则尝试获取。(原理详见底层源码)

(3)总结

获取锁和释放锁的代码层面的原理

 

 (4)Redisson锁对于redis主从一致性问题的解决

【redis主从一致性问题】当java应用在获取锁时(执行setnx命令到主节点),主节点写入锁后尚未来得及向从节点同步数据就发生宕机。此时,哨兵将选出新的主节点,当java应用再次访问redis时就会发现锁已经失效。其他应用可以随意获取锁。

既然这种主从模式会导致主从一致性问题,那么就可以采用多个主节点的模式,多人联锁以解决。

 联锁(multiLock)在获取锁时要同时在多个redis节点上setnx,仅当都成功时才能拿到锁。事实上,每一个锁其实都是一个redisson可重入锁。

四、分布式锁总结

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值