返回顶部
首页 > 资讯 > 数据库 >Redis专题(3):锁的基本概念到Redis分布式锁实现
  • 843
分享到

Redis专题(3):锁的基本概念到Redis分布式锁实现

2024-04-02 19:04:59 843人浏览 泡泡鱼
摘要

拓展阅读:Redis闲谈(1):构建知识图谱 Redis专题(2):Redis数据结构底层探秘 近来,分布式的问题被广泛提及,比如分布式事务、分布式框架、ZooKeeper、SpringCloud等等。本文

拓展阅读:Redis闲谈(1):构建知识图谱

Redis专题(2):Redis数据结构底层探秘

近来,分布式的问题被广泛提及,比如分布式事务、分布式框架ZooKeeperSpringCloud等等。本文先回顾的概念,再介绍分布式锁,以及如何用Redis来实现分布式锁。

一、锁的基本了解

首先,回顾一下我们工作学习中的锁的概念。

为什么要先讲锁再讲分布式锁呢?

我们都清楚,锁的作用是要解决多线程对共享资源的访问而产生的线程安全问题,而在平时生活中用到锁的情况其实并不多,可能有些朋友对锁的概念和一些基本的使用不是很清楚,所以我们先看锁,再深入介绍分布式锁。

Redis专题(3):锁的基本概念到Redis分布式锁实现

通过一个卖票的小案例来看,比如大家去抢dota2 ti9门票,如果不加锁的话会出现什么问题?此时代码如下:

package Thread;

import java.util.concurrent.TimeUnit;

public class Ticket {

    
    Integer ticketNum = 8;

    public void reduce(int num){
        //判断库存是否够用
        if((ticketNum - num) >= 0){
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            ticketNum -= num;
            System.out.println(Thread.currentThread().getName() + "成功卖出"
            + num + "张,剩余" + ticketNum + "张票");
        }else {
            System.err.println(Thread.currentThread().getName() + "没有卖出"
                    + num + "张,剩余" + ticketNum + "张票");
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Ticket ticket = new Ticket();
        //开启10个线程进行抢票,按理说应该有两个人抢不到票
        for(int i=0;i<10;i++){
            new Thread(() -> ticket.reduce(1),"用户" + (i + 1)).start();
        }
        Thread.sleep(1000L);
    }

}

代码分析:这里有8张ti9门票,设置了10个线程(也就是模拟10个人)去并发抢票,如果抢成功了显示成功,抢失败的话显示失败。按理说应该有8个人抢成功了,2个人抢失败,下面来看运行结果:

Redis专题(3):锁的基本概念到Redis分布式锁实现

我们发现运行结果和预期的情况不一致,居然10个人都买到了票,也就是说出现了线程安全的问题,那么是什么原因导致的呢?

原因就是多个线程之间产生了时间差

如图所示,只剩一张票了,但是两个线程都读到的票余量是1,也就是说线程B还没有等到线程A改库存就已经抢票成功了。

Redis专题(3):锁的基本概念到Redis分布式锁实现

怎么解决呢?想必大家都知道,加个synchronized关键字就可以了,在一个线程进行reduce方法的时候,其他线程则阻塞在等待队列中,这样就不会发生多个线程对共享变量的竞争问题。

举个例子

比如我们去健身房健身,如果好多人同时用一台机器,同时在一台跑步机上跑步,就会发生很大的问题,大家会打得不可开交。如果我们加一把锁在健身房门口,只有拿到锁的钥匙的人才可以进去锻炼,其他人在门外等候,这样就可以避免大家对健身器材的竞争。代码如下:

public  synchronized void reduce(int num){
        //判断库存是否够用
        if((ticketNum - num) >= 0){
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            ticketNum -= num;
            System.out.println(Thread.currentThread().getName() + "成功卖出"
            + num + "张,剩余" + ticketNum + "张票");
        }else {
            System.err.println(Thread.currentThread().getName() + "没有卖出"
                    + num + "张,剩余" + ticketNum + "张票");
        }
    }

运行结果:

Redis专题(3):锁的基本概念到Redis分布式锁实现

果不其然,结果有两个人没有成功抢到票,看来我们的目地达成了。

二、锁的性能优化

2.1 缩短锁的持有时间

事实上,按照我们对日常生活的理解,不可能整个健身房只有一个人在运动。所以我们只需要对某一台机器加锁就可以了,比如一个人在跑步,另一个人可以去做其他的运动。

对于票务系统来说,我们只需要对库存的修改操作的代码加锁就可以了,别的代码还是可以并行进行,这样会大大减少锁的持有时间,代码修改如下:

public void reduceByLock(int num){
        boolean flag = false;

        synchronized (ticketNum){
            if((ticketNum - num) >= 0){
                ticketNum -= num;
                flag = true;
            }
        }
        if(flag){
            System.out.println(Thread.currentThread().getName() + "成功卖出"
                        + num + "张,剩余" + ticketNum + "张票");
        }
        else {
            System.err.println(Thread.currentThread().getName() + "没有卖出"
                        + num + "张,剩余" + ticketNum + "张票");
        }
        if(ticketNum == 0){
            System.out.println("耗时" + (System.currentTimeMillis() - startTime) + "毫秒");
        }
    }

这样做的目的是充分利用cpu的资源,提高代码的执行效率

这里我们对两种方式的时间做个打印:

public synchronized void reduce(int num){
        //判断库存是否够用
        if((ticketNum - num) >= 0){
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            ticketNum -= num;
            if(ticketNum == 0){
                System.out.println("耗时" + (System.currentTimeMillis() - startTime) + "毫秒");
            }
            System.out.println(Thread.currentThread().getName() + "成功卖出"
            + num + "张,剩余" + ticketNum + "张票");
        }else {
            System.err.println(Thread.currentThread().getName() + "没有卖出"
                    + num + "张,剩余" + ticketNum + "张票");
        }
    }

Redis专题(3):锁的基本概念到Redis分布式锁实现

Redis专题(3):锁的基本概念到Redis分布式锁实现

果然,只对部分代码加锁会大大提供代码的执行效率。

所以,在解决了线程安全的问题后,我们还要考虑到加锁之后的代码执行效率问题

2.2 减少锁的粒度

举个例子,有两场电影,分别是最近刚上映的魔童哪吒和蜘蛛侠,我们模拟一个支付购买的过程,让方法等待,加了一个CountDownLatch的await方法,运行结果如下:

package Thread;

import java.util.concurrent.CountDownLatch;

public class Movie {
    private final CountDownLatch latch =  new CountDownLatch(1);
    //魔童哪吒
    private Integer babyTickets = 20;

    //蜘蛛侠
    private Integer spiderTickets = 100;

    public synchronized void showBabyTickets() throws InterruptedException{
        System.out.println("魔童哪吒的剩余票数为:" + babyTickets);
        //购买
        latch.await();
    }

    public synchronized void showSpiderTickets() throws InterruptedException{
        System.out.println("蜘蛛侠的剩余票数为:" + spiderTickets);
        //购买
    }

    public static void main(String[] args) {
        Movie movie = new Movie();
        new Thread(() -> {
            try {
                movie.showBabyTickets();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        },"用户A").start();

        new Thread(() -> {
            try {
                movie.showSpiderTickets();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        },"用户B").start();
    }

}

执行结果:

魔童哪吒的剩余票数为:20

我们发现买哪吒票的时候阻塞会影响蜘蛛侠票的购买,而实际上,这两场电影之间是相互独立的,所以我们需要减少锁的粒度,将movie整个对象的锁变为两个全局变量的锁,修改代码如下:

public void showBabyTickets() throws InterruptedException{
        synchronized (babyTickets) {
            System.out.println("魔童哪吒的剩余票数为:" + babyTickets);
            //购买
            latch.await();
        }
    }

    public void showSpiderTickets() throws InterruptedException{
        synchronized (spiderTickets) {
            System.out.println("蜘蛛侠的剩余票数为:" + spiderTickets);
            //购买
        }
    }

执行结果:

魔童哪吒的剩余票数为:20
蜘蛛侠的剩余票数为:100

现在两场电影的购票不会互相影响了,这就是第二个优化锁的方式:减少锁的粒度。顺便提一句,java并发包里的ConcurrentHashMap就是把一把大锁变成了16把小锁,通过分段锁的方式达到高效的并发安全。

2.3 锁分离

锁分离就是常说的读写分离,我们把锁分成读锁和写锁,读的锁不需要阻塞,而写的锁要考虑并发问题。

三、锁的种类

  • 公平锁: ReentrantLock
  • 非公平锁: Synchronized、ReentrantLock、cas
  • 悲观锁: Synchronized
  • 乐观锁:cas
  • 独享锁:Synchronized、ReentrantLock
  • 共享锁:Semaphore

这里就不一一讲述每一种锁的概念了,大家可以自己学习,锁还可以按照偏向锁、轻量级锁、重量级锁来分类。

四、Redis分布式锁

了解了锁的基本概念和锁的优化后,重点介绍分布式锁的概念。

Redis专题(3):锁的基本概念到Redis分布式锁实现

上图所示是我们搭建的分布式环境,有三个购票项目,对应一个库存,每一个系统会有多个线程,和上文一样,对库存的修改操作加上锁,能不能保证这6个线程的线程安全呢?

当然是不能的,因为每一个购票系统都有各自的JVM进程,互相独立,所以加synchronized只能保证一个系统的线程安全,并不能保证分布式的线程安全。

所以需要对于三个系统都是公共的一个中间件来解决这个问题。

这里我们选择Redis来作为分布式锁,多个系统在Redis中set同一个key,只有key不存在的时候,才能设置成功,并且该key会对应其中一个系统的唯一标识,当该系统访问资源结束后,将key删除,则达到了释放锁的目的。

4.1 分布式锁需要注意哪些点

1)互斥性

在任意时刻只有一个客户端可以获取锁。

这个很容易理解,所有的系统中只能有一个系统持有锁。

2)防死锁

假如一个客户端在持有锁的时候崩溃了,没有释放锁,那么别的客户端无法获得锁,则会造成死锁,所以要保证客户端一定会释放锁。

Redis中我们可以设置锁的过期时间来保证不会发生死锁。

3)持锁人解锁

解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端A的线程加的锁必须是客户端A的线程来解锁,客户端不能解开别的客户端的锁。

4)可重入

当一个客户端获取对象锁之后,这个客户端可以再次获取这个对象上的锁。

4.2 Redis分布式锁流程

Redis专题(3):锁的基本概念到Redis分布式锁实现

Redis分布式锁的具体流程:

1)首先利用Redis缓存的性质在Redis中设置一个key-value形式的键值对,key就是锁的名称,然后客户端的多个线程去竞争锁,竞争成功的话将value设为客户端的唯一标识。

2)竞争到锁的客户端要做两件事:

  • 设置锁的有效时间 目的是防死锁 (非常关键)

需要根据业务需要,不断的压力测试来决定有效期的长短。

  • 分配客户端的唯一标识,目的是保证持锁人解锁(非常重要)

所以这里的value就设置成唯一标识(比如uuid)。

3)访问共享资源

4)释放锁,释放锁有两种方式,第一种是有效期结束后自动释放锁,第二种是先根据唯一标识判断自己是否有释放锁的权限,如果标识正确则释放锁

4.3 加锁和解锁

4.3.1 加锁

1)setnx命令加锁

set if not exists 我们会用到Redis的命令setnx,setnx的含义就是只有锁不存在的情况下才会设置成功。

2)设置锁的有效时间,防止死锁 expire

加锁需要两步操作,思考一下会有什么问题吗?

假如我们加锁完之后客户端突然挂了呢?那么这个锁就会成为一个没有有效期的锁,接着就可能发生死锁。虽然这种情况发生的概率很小,但是一旦出现问题会很严重,所以我们也要把这两步合为一步。

幸运的是,Redis3.0已经把这两个指令合在一起成为一个新的指令。

来看jedis的官方文档中的源码

    public String set(String key, String value, String nxxx, String expx, long time) {
        this.checkIsInMultiOrPipeline();
        this.client.set(key, value, nxxx, expx, time);
        return this.client.getStatusCodeReply();
    }

这就是我们想要的!

4.3.2 解锁
  • 检查是否自己持有锁(判断唯一标识);
  • 删除锁。

解锁也是两步,同样也要保证解锁的原子性,把两步合为一步。

这就无法借助于Redis了,只能依靠Lua脚本来实现。

if Redis.call("get",key==argv[1])then
    return Redis.call("del",key)
else return 0 end

这就是一段判断是否自己持有锁并释放锁的lua脚本。

为什么Lua脚本是原子性呢?因为Lua脚本是jedis用eval()函数执行的,如果执行则会全部执行完成。

五、Redis分布式锁代码实现

public class RedisDistributedLock implements Lock {

    //上下文,保存当前锁的持有人id
    private ThreadLocal<String> lockContext = new ThreadLocal<String>();

    //默认锁的超时时间
    private long time = 100;

    //可重入性
    private Thread ownerThread;

    public RedisDistributedLock() {
    }

    public void lock() {
        while (!tryLock()){
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public boolean tryLock() {
        return tryLock(time,TimeUnit.MILLISECONDS);
    }

    public boolean tryLock(long time, TimeUnit unit){
        String id = UUID.randomUUID().toString(); //每一个锁的持有人都分配一个唯一的id
        Thread t = Thread.currentThread();
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //只有锁不存在的时候加锁并设置锁的有效时间
        if("OK".equals(jedis.set("lock",id, "NX", "PX", unit.toMillis(time)))){
            //持有锁的人的id  
            lockContext.set(id); ①
            //记录当前的线程
            setOwnerThread(t); ②
            return true;
        }else if(ownerThread == t){
            //因为锁是可重入的,所以需要判断当前线程已经持有锁的情况
            return true;
        }else {
            return false;
        }
    }

    private void setOwnerThread(Thread t){
        this.ownerThread = t;
    }

    public void unlock() {
        String script = null;
        try{
            Jedis jedis = new Jedis("127.0.0.1",6379);
            script = inputStream2String(getClass().getResourceAsStream("/Redis.Lua"));
            if(lockContext.get()==null){
                //没有人持有锁
                return;
            }
            //删除锁  ③
            jedis.eval(script, Arrays.asList("lock"), Arrays.asList(lockContext.get()));
            lockContext.remove();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    
    public String inputStream2String(InputStream is) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i = -1;
        while ((i = is.read()) != -1) {
            baos.write(i);
        }
        return baos.toString();
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public Condition newCondition() {
        return null;
    }
}
  • 用一个上下文全局变量来记录持有锁的人的uuid,解锁的时候需要将该uuid作为参数传入Lua脚本中,来判断是否可以解锁。
  • 要记录当前线程,来实现分布式锁的重入性,如果是当前线程持有锁的话,也属于加锁成功。
  • 用eval函数来执行Lua脚本,保证解锁时的原子性。

六、分布式锁的对比

6.1 基于数据库的分布式锁

1)实现方式

获取锁的时候插入一条数据,解锁时删除数据。

2)缺点

  • 数据库如果挂掉会导致业务系统不可用。
  • 无法设置过期时间,会造成死锁。

6.2 基于zookeeper的分布式锁

1)实现方式

加锁时在指定节点的目录下创建一个新节点,释放锁的时候删除这个临时节点。因为有心跳检测的存在,所以不会发生死锁,更加安全

2)缺点

性能一般,没有Redis高效。

所以:

  • 从性能角度:
    Redis > zookeeper > 数据库
  • 从可靠性(安全)性角度:
    zookeeper > Redis > 数据库

七、总结

本文从锁的基本概念出发,提出多线程访问共享资源会出现的线程安全问题,然后通过加锁的方式去解决线程安全的问题,这个方法会性能会下降,需要通过:缩短锁的持有时间、减小锁的粒度、锁分离三种方式去优化锁。

之后介绍了分布式锁的4个特点:

  • 互斥性
  • 防死锁
  • 加锁人解锁
  • 可重入性

然后用Redis实现了分布式锁,加锁的时候用到了Redis的命令去加锁,解锁的时候则借助了Lua脚本来保证原子性。

最后对比了三种分布式锁的优缺点和使用场景。

希望大家对分布式锁有新的理解,也希望大家在考虑解决问题的同时要多想想性能的问题。

作者:杨亨

您可能感兴趣的文档:

--结束END--

本文标题: Redis专题(3):锁的基本概念到Redis分布式锁实现

本文链接: https://lsjlt.com/news/44638.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Redis专题(3):锁的基本概念到Redis分布式锁实现
    拓展阅读:Redis闲谈(1):构建知识图谱 Redis专题(2):Redis数据结构底层探秘 近来,分布式的问题被广泛提及,比如分布式事务、分布式框架、ZooKeeper、SpringCloud等等。本文...
    99+
    2024-04-02
  • 基于Redis实现分布式锁
    我们知道分布式锁的特性是排他、避免死锁、高可用。分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是...
    99+
    2017-09-11
    基于Redis实现分布式锁
  • Redis——》实现分布式锁
    推荐链接:     总结——》【Java】     总结——》【Mysql】     总结——》【Redis】     总结——》【Kafka】     总结——》【Spring】     总结—...
    99+
    2023-09-03
    redis 分布式 过期 lua
  • Redis实现分布式锁
    单体锁存在的问题 在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。 (下述实例是一个简单的下单问题:从redis中获取库存,检查库存是否够,>0才允许下单) 我们的解决办法通常是加锁。如下加单体锁...
    99+
    2023-08-16
    分布式 java jvm
  • Java基于Redis实现分布式锁
    分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。一、为什么Redis可以方便地实现分布式锁Redis为单进程单线程模式,采用...
    99+
    2015-09-14
    java教程 Java
  • redis分布式锁的实现
    一、使用分布式锁要满足的几个条件:1、系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)2、共享资源(各个系统访问同一个资源,资源的载体可...
    99+
    2024-04-02
  • Redis分布式锁之红锁的实现
    目录一、问题二、办法三、原理四、实战一、问题 分布式锁,当我们请求一个分布式锁的时候,成功了,但是这时候slave还没有复制我们的锁,masterDown了,我们的应用继续请求锁的时候,会从继任了master的原slav...
    99+
    2022-08-09
    Redis红锁
  • 详解基于redis实现分布式锁
    目录前言原理剖析实现编写注解拦截器拦截上述提及工具问题分析前言 为了保证一个在高并发存场景下只能被同一个线程操作,java并发处理提供ReentrantLock或Synchroniz...
    99+
    2024-04-02
  • Redis Template实现分布式锁
    今天就跟大家聊聊有关Redis Template实现分布式锁,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。可靠性首先,为了确保分布式锁可用,我们至少...
    99+
    2024-04-02
  • PHP+Redis实现分布式锁
    目录 一、分布式锁概述 二、redis实现锁的命令 1、redis实现锁的命令 3、释放锁的步骤 三、PHP+redis分布式锁示例 四、redis集群分布式锁 一、分布式锁概述         在分布式环境下,各个线程通过对公共资...
    99+
    2023-09-15
    分布式锁
  • Redis实现分布式锁(SETNX)
    目录 1、什么是分布式锁 2、分布式锁应具备的条件         3、为什么使用分布式锁 4、SETNX介绍 5、分布式锁实现 6、效果演示 7、Redisson分布式锁详解 8、Lua脚本实现可重入分布式锁 1、什么是分布式锁  ...
    99+
    2023-10-21
    redis 分布式 java spring boot 后端
  • python实现redis分布式锁
    #!/usr/bin/env python # coding=utf-8 import time import redis class RedisLock(object): def __init__(self, key): ...
    99+
    2023-01-31
    分布式 python redis
  • 基于Redis分布式锁的实现代码
    概述 目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(...
    99+
    2024-04-02
  • C#实现Redis的分布式锁
    目录Redis实现分布式锁(悲观锁/乐观锁)Redis连接池使用Redis的SetNX命令实现加锁,调用方式Redis实现分布式锁(悲观锁/乐观锁) 对锁的概念和应用场景在此就不阐...
    99+
    2024-04-02
  • Redis分布式锁Redlock的实现
    目录普通实现Redlock实现Redlock源码用法唯一ID获取锁释放锁普通实现 说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value p...
    99+
    2024-04-02
  • Redis分布式锁的实现方式
    目录一、分布式锁是什么1、获取锁2、释放锁二、代码实例上面代码存在锁误删问题:三、基于SETNX实现的分布式锁存在下面几个问题1、不可重入2、不可重试3、超时释放4、主从一致性四、Redisson实现分布式锁1、pom2...
    99+
    2023-04-03
    Java Redis分布式锁实现方式 实现Redis分布式锁 Redis分布式锁实现
  • 分布式锁的原理及Redis怎么实现分布式锁
    这篇文章主要介绍“分布式锁的原理及Redis怎么实现分布式锁”,在日常操作中,相信很多人在分布式锁的原理及Redis怎么实现分布式锁问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解...
    99+
    2023-02-02
    redis
  • Redis分布式锁怎么实现
    这篇文章给大家分享的是有关Redis分布式锁怎么实现的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。分布式锁一般有三种实现方式:1、数据库乐观锁;2、基于Redis的分布式锁;3、...
    99+
    2024-04-02
  • Redis分布式锁如何实现
    这篇文章将为大家详细讲解有关Redis分布式锁如何实现,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。什么是分布式锁?要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、...
    99+
    2024-04-02
  • Redis如何实现分布式锁
    目录一、前言二、正文今天我们来聊一聊分布式锁的那些事。 相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作