Redission源码详解

Redisson 源码解析系列一:分布式锁RedissonLock可重入机制+锁续约解析 分布式锁RedissonLock `可重入机制`+`锁续约` 解析 阅读详情

环境搭建

redis搭建

在本地搭建个redis,启动redis服务端和客户端。客户端是为了观察redis中锁的信息。

依赖

新建个SpringBoot项目,依赖Redission jar包

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-spring-boot-starter</artifactId>
	<version>3.21.1</version>
</dependency>

工具类

@Service
public class RedisUtil {
    @Autowired
    private RedissonClient redissonClient;

    public  boolean tryLock(Long key){
        RLock rLock= redissonClient.getLock(String.valueOf(key));
        try {
            boolean lockResult=rLock.tryLock(10,30, TimeUnit.SECONDS);
            return lockResult;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return false;
        }
    }
}

测试Controller

package com.ludk.weixin.controller;

import com.ludk.weixin.common.tool.RedisUtil;
import com.ludk.weixin.entity.Product;
import com.ludk.weixin.service.IProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;

@Controller
@Slf4j
@RequestMapping("/product")
public class ProductController {
    @Autowired
    RedisUtil redisUtil;




    @RequestMapping("/myProduct")
    @ResponseBody
    Object myProduct(@RequestParam("userId") Long userId){
        long before=System.currentTimeMillis();
        log.error("获取锁之前:"+before);
        redisUtil.tryLock(userId);
        long after=System.currentTimeMillis();
        System.out.println("获取锁之后:"+after);
        System.out.println((after-before)/1000);
        return productService.getMyProduct(userId);

}

源码思路分析

首先需要知道Redission实现的分布式锁有哪些特点?

  • 支持锁的超时,避免死锁。通过对Redis key设置超时时间实现。
  • 支持可重入锁,同一个线程可以多次获取锁。Redis中通过存储hash类型,其中一个字段记录次数实现。
  • 当线程A获取到锁执行业务逻辑且未执行完,如果锁超时,可以通过看门狗续锁的时间,避免其他线程获取锁。通过看门狗实现。

源码分析

初始化锁对象

主要是初始化默认锁的释放时间和uuid,下面会用到。

加锁方法

org.redisson.api.RLock#tryLock方法参数如下:

  • long waitTime:当获取锁失败时,等待waitTime时间后返回获取锁失败。
  • long leaseTime:锁释放时间,当获取到锁以后,这个锁多久失效。也就是redis中存储的锁的key的有效时间。
  • TimeUnit unit:上面两个时间的单位。
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
	//将锁的等待时间转为毫秒数
    long time = unit.toMillis(waitTime);
    //当前时间,从现在起算锁的等待时间
    long current = System.currentTimeMillis();
    //当前线程id,因为是可重入锁,需要用threadId区别是否是当前线程。
    long threadId = Thread.currentThread().getId();
    //获取锁,如果ttl 为null则说明获取锁成功。否则ttl代表这个锁还有多久超时,超时则当前线程有获取到锁的可能
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }
    // System.currentTimeMillis()是当前时间,current是进入tryLock这个方法的时间,time=time-(System.currentTimeMillis() - current)表示还有多少时间获取锁超时,time<=0就说明超时
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {//获取锁超时,返回false。
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
    //记录当前时间
    current = System.currentTimeMillis();
    //订阅监听redis消息,并且创建RedissonLockEntry,其中RedissonLockEntry中比较关键的是一个 Semaphore属性对象,用来控制本地的锁请求的信号量同步,返回的是netty框架的Future实现。
    CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    try {
    	//添加锁的监听,如果锁释放,会进入监听类,将Semaphore释放
    	// 阻塞等待subscribe的future的结果对象,如果subscribe方法调用超过了time,说明已经超过了客户端设置的最大wait time,则直接返回false,取消订阅,不再继续申请锁了。
        subscribeFuture.get(time, TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
        if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
                "Unable to acquire subscription lock after " + time + "ms. " +
                        "Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
            subscribeFuture.whenComplete((res, ex) -> {
                if (ex == null) {
                    unsubscribe(res, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId);
        return false;
    } catch (ExecutionException e) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    try {
    	//经过上面的计算,time是还多长时间获取锁超时,time已经被减掉一部分了。这里继续把消耗的时间减掉。
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {//time<0,获取锁超时,返回false,获取锁失败。
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
    
        while (true) {//死循环,有两个点可以调出循环。获取锁成功跳出、获取锁超时(失败)跳出
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

            // waiting for message
            currentTime = System.currentTimeMillis();
            //下面的阻塞会在释放锁的时候,通过订阅发布及时获取锁
            if (ttl >= 0 && ttl < time) {//如果锁的超时时间小于等待时间,通过SemaphorerelaseryAcquire阻塞锁的释放时间
                commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {//否则,通过Semaphore的tryAcquire阻塞传入的最大等待时间
                commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {//finally中取消订阅
        unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
    }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
}

org.redisson.RedissonLock#tryAcquire方法

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync0(waitTime, leaseTime, unit, threadId));
}

org.redisson.RedissonLock#tryAcquireAsync0方法

    private RFuture<Long> tryAcquireAsync0(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        return getServiceManager().execute(() -> tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }

org.redisson.RedissonLock#tryAcquireAsync

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime > 0) {//如果leaseTime >0,则传入锁释放时间,
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {//否则用默认的锁释放时间30000ms
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
    ttlRemainingFuture = new CompletableFutureWrapper<>(s);

    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // lock acquired
        if (ttlRemaining == null) {
            if (leaseTime > 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
            	//看门狗续时间,在锁要超时,业务还未执行完这种情况续锁时间。需要注意,leaseTime>0的时候不会有看门狗。
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

org.redisson.RedissonLock#tryLockInnerAsync
该方法和redis交互,是锁实现的核心。

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
                "if ((redis.call('exists', KEYS[1]) == 0) " +
                            "or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                    "end; " +
                    "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }

分析这个lua脚本的参数:

  • KEYS[1]:是大key,判断是否有其他线程获取到锁。
  • ARGV[1]:锁的释放时间,这个锁如果不释放,多久会超时。
  • ARGV[2]:uuid+线程id,用于实现可重入锁。

分析lua脚本的逻辑:
if ((redis.call(‘exists’, KEYS[1]) == 0)or (redis.call(‘hexists’, KEYS[1] ,ARGV[2]) == 1))
then
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1);
redis.call(‘pexpire’, KEYS[1], ARGV[1]);
return nil;
end;
return redis.call(‘pttl’, KEYS[1]);

  • redis.call(‘exists’, KEYS[1]) == 0:说明大key不存在,也就是没有其他线程获取锁,可以之间获取锁成功了。
  • redis.call(‘hexists’, KEYS[1] ,ARGV[2]) == 1):redis这大key下,当前线程已经获取到了锁。
  • return redis.call(‘pttl’, KEYS[1]):如果不是上面两种情况,说明其他线程占用锁了,返回这个锁还有多长时间释放。

总结:如果没有其他线程获取锁,或者当前线程已经获取到锁,则锁加上超时时间、当前线程锁的次数加1、然后返回空;否则说明有其他线程已经获取到锁,返回这个锁还有多久超时。

其他

lock、tryLock方法区别,参考这个博客:https://blog.youkuaiyun.com/weixin_37684345/article/details/135815110

SpringBoot日常:redission的接入使用和源码解析 Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格客户端(In-Memory Data Grid)。它不仅提供了一系列的 redis 常用数据结构命令服务,还提供了许多分布式服务,例如分布式锁、分布式对象、分布式集合、分布式远程服务、分布式调度任务服务等等。本文会介绍如何将redission集成到springboot和如何使用 Redisson 操作 Redis 中的字符串、哈希、列表、集合、有序集合,以及布隆过滤器和分布式锁等功能,并解析源码中其最大亮点的看门狗机制。 阅读详情

相关推荐

【redis】一文看完redission看门狗机制源码实现

Hi,今天白梦又来卷了,趁着有时间,看了Redission锁原理以及看门狗的部分源码,主要是和这两个类。看新不看旧,首先引入Redission最新版本。

weixin_38225800的博客 2294

Redisson源码篇

Redisson源码

weixin_44383484的博客 4061

一篇文章看懂Redission原理

一篇文章看懂Redission原理

时间如瑾 代码如诗 5961

redisson源码解析

由于synchronized跟ReetrantLock是JVM级别的锁,在分布式情况下失效,这时候我们通常会选择redisson基于redis封装好的分布式锁。下面我们一起来分析以下redisson的源码。

weixin_53067547的博客 1475

卷积神经网络中的边缘检测:从Canny算法到深度学习(PyTorch实现)

本文系统梳理了边缘检测从经典Canny算法到深度学习方法的演进,重点解析了Canny算法的数学原理及其对卷积神经网络设计的深远影响。通过PyTorch实战,详细演示了如何构建并训练一个基于HED架构的深度学习边缘检测模型,并探讨了模型优化、评估及与传统方法融合的高级技巧。

jenkins8butler的博客 151

rk3588s-摄像头适配gc8034

若还打开失败,需要看下XML中文件配置是否与DTS配置信息一致,或者XML参数配置是否有异常;MIPI摄像头是有上电时序要求,具体时序要查看sensor芯片对应的datasheet进行修改,如果时序不满足,sensor无法正常,此时用i2ctool去查看设备可能无法扫面到设备,即是i2c设备找不到。RK3588或者RK3588S有路MIPI-PHY,驱动开发工程师需要根据对应的硬件原图进行配置,以下会讲解分别使用MIPI-DPHY和MIPI-D/CPHY如何在DTS配置mipi摄像头的回环链路。

Yxx8066的博客 2199

redission分布锁详解

Redission分布式锁原理 Redission中分布式锁lua脚本源码 //KEYS[1]:表示你加锁的那个key,比如说 //RLock lock = redisson.getLock(“myLock”); //这里你自己设置了加锁的那个锁key就是“myLock”。 //ARGV[1]:表示锁的有效期,默认30s //ARGV[2]:表示表示加锁的客户端ID,类似于下面这样: //8743c9c0-0795-4907-87fd-6c719a6b4586:1 if (redis.call('exis

chongbaozhong的博客 1758

3D视觉-激光三角测量法的概述和原理

激光三角法位移测量的原理是,用一束激光以某一角度聚焦在被测物体表面,然后从另一角度对物体表面上的激光光斑进行成像,物体表面激光照射点的位置高度不同,所接受散射或反射光线的角度也不同,用。激光三角测量法是一种通过图像采集系统采集被测物表面漫反射的激光轮廓,进而基于设备已知的相对位置通过算法处理,获得被测物表面的三维轮廓数据,进而还原目标物体三维空间信息的方法。采用激光三角原理和回波分析原理进行非接触位置、位移测量的精密传感器, 广泛应用于位置、位移 、厚度、半径、形状、振动、距离等几何量的工业测量。

qq_20660115的博客 3919

Redission分布式锁源码解析

Redission锁继承Implements Reentrant Lock,所以具备 Reentrant Lock 锁中的一些特性:超时,重试,可中断等。加上Redission中Redis具备分布式的特性,所以非常适合用来做Java中的分布式锁。 下面我们对其加锁、解锁过程中的源码细节进行一一...

chouhui0263的博客 827

发票OFD格式转换成PDF

发票OFD格式转换成PDF

Be_insighted的博客 930

安卓手机游戏《英雄迷宫冒险》存档修改

警告 修改前务必备份存档文件夹。该游戏有自动删除存档的功能,有问题的存档会自动删除。 修改对应的游戏版本为1.5。 存档保存位置 内部存储\Android\data\com.xmmf.yxmgmx.taptap\files 该文件夹中文件如下: data.wsf:数据文件。 wsfSave0:第一个存档。 wsfSave1:第二个存档。 修改金钱的例子 先进游戏确认当前金钱数量。当前金钱是2498,然后退出游戏。 根据金钱数量计算出存档中的内容。2498的十六进制是9C2,存档中的内容在存档中搜索该数

番茄鸡蛋的专栏 9360

Redission分布式锁详解

​ **

qq_39594419的博客 1887

【虚幻引擎UE】UE5 阴影异常与优化

【虚幻引擎UE】UE5 阴影异常与优化解决方案合集

灵境引路人 3万+

Redission实现分布式锁

redission实现分布式锁,根据底层源码,分析重入锁原理,锁重试机制和锁续约(WatchDog机制)

m0_64116616的博客 9425

手撕Lua字节码:从黑盒到透明

lua 字节码 怎么‘手撕’一个 chunk 看看里面啥样?我手里有一个.luac或出来的二进制字节码,想知道里面到底是什么结构、哪些字段、有哪些指令,怎么一步一步“拆箱”,从一个黑盒变成有迹可循的东西?也就是:不满足于只会loadpcall而是想亲眼看看一个 chunk 内部长啥样,甚至写点工具把它解析出来。这篇就用带你从“拿到一坨.luac不搞高深玄学,尽量让你看完就能自己动手。注:以 Lua 5.3/5.4 为主,细节各版本略有不同,但大体套路是一致的。

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 97

Pulseaudio之同步/异步(十一)

PulseAudio是一个声音服务器,一个后台进程从一个或多个音源(进程或输入设备)接受声音输入 然后重定向声音到一个或多个槽(声卡,远程网络PulseAudio服务,或其他进程)。 PulseAudio的主要特点包括: 可对每一个应用程序进行音量控制Per-application volume controls 可扩展的插件与支持可装载模块架构 兼容性许多流行的音频应用程序 支持多重音源和多重输出 低延时操作和支持延迟测量 一个对处理器资源效率零拷贝内存架构 能够发现本地网络上使用PulseAu

Android系统攻城狮 995

Redission分布式锁加锁原理源码解析

1.获取锁 这里是我们自己实现,调用redission的方法,获取锁,然后加锁。lock.lock(expireTime, timeUnit)是关键,我们追进去。 /** * 获取锁,如果没有主动调用unlock解锁,expireTime后会自动释放 * @param lockKey * @param expireTime 如果没有调用unlock解锁,ex...

IT云清 2200

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Redission try lock 参数你真的理解吗?

前几天同事用redission的lock功能,耗时一下午,也没实现抢不到锁立即返回的效果,然后退回使用redis template的setnx方法。review全部代码后发现,老哥这是没有理解redission lock的参数含义,还有个其他C端场景模块代码毫无作用的等待2s,导致并发度始终上不去。当时有点血压飙升,抄起水杯,喝两口压压惊(自己年龄大了要稳重,业务上可能确实着急同事没有仔细研究这块)。今天简单说下这俩参数。

IT枫斗者的博客 1024

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

SpringBoot整合篇 05、Springboot整合Redission

本篇文章介绍了如何在SpringBoot项目中整合Redission,一个Java框架用于简化Redis的使用。文章首先概述了Redission集成的两种场景:单节点和多节点,然后重点介绍了单节点配置。提供了详细的配置步骤,包括pom依赖、application.yaml配置文件和Java配置类。文章还展示了如何使用Redission进行分布式锁的实践,并通过单元测试验证了锁的功能。配套源码可在Gitee和GitHub上找到

每个人都是独一无二的,把握好自己的节奏,跟着自己的心走。 1155

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Redis实战-基于redis和lua脚本实现分布式锁以及Redission源码解析【万字长文】

分布式锁是一种在分布式系统中用于协调多个节点或进程对共享资源进行互斥访问的机制。它的核心目标是确保在分布式环境下,同一时间只有一个节点能够执行关键操作(如修改共享数据、访问数据库等),避免并发导致的数据不一致问题。分布式锁是在分布式系统中协调多个节点对共享资源访问的一种同步机制。同俗一点解释就是,只能管住自己JVM里的线程,而Redis分布式锁相当于一个"全局管理员",能管住所有JVM的线程,让它们在集群里排队用资源。就好比单机游戏(只能在本机内存档)和网游游戏的区别(所有电脑都可以读取存档)

a6543665的博客 2484

Redission可重试、超时续约的实现原理(源码分析)

如果获取锁失败,用语句“PTTL KEYS[1]”返回当前锁的剩余有效时间。最终返回到tryLock()函数里,成为局部变量ttl的值,如果获取锁成功,返回nil,也就是java里的null。执行获取锁的lua脚本时,会返回一个值,time是剩余等待时间的值。

qq_40397392的博客 563

Redission实现分布式锁之源码解析

接上篇Redis分布式锁原理之实现秒杀抢优惠卷业务重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。

me_1984的博客 940

redission学习记录

前言:最近做的一个接口由于没有实现幂等性,老是会出现重复提交导致数据出错的情况。之前只能依靠接口调用方去做逻辑控制避免这种情况,这次决定使用分布式锁来解决之歌问题,之前学习的时候用的是jedis写分布式锁,但是发现确还有些许缺陷,机缘巧合下得知redission框架封装了分布式锁,不但类型全面,而且使用方便,周六特略微学习了一波,今天则记录下来。附上阿里社区的redission中文版官方文档地址:...

qq_35140272的博客 971

Redission 看门狗 学习之笔记

文章目录前言看门狗个人的学习发现单元测试结果发现:lock方法无法续期锁源码走起总结参考网址lockwatchdogtimeout 前言   相信大家用过Redission吧,那你听过看门狗?没有的话,让鸡腿给你慢慢道来。   如果一个场景:现在有A,B在执行业务,A加了分布式锁,但是生产环境是各种变化的,如果万一A锁超时了,但是A的业务还在跑。而这时由于A锁超时释放,B拿到锁,B执行业务逻辑。这......

大鸡腿的博客 8002

深入了解Redission分布式锁

文章目录1. 分布式锁的条件2. 实现分布式锁的方案3. Redission分布式锁原理4.场景应用  现在我们采用的系统都是分布式,比如我们Sychnized上锁,因为他是JVM层面的,所以如果是两台机器的话,这时候它就无能为力了,这时候我们可以用Reddisson。 1. 分布式锁的条件 互斥性 高效性 支持阻塞和非阻塞 支持公平锁和非公平锁 2. 实现分布式锁的方案 .数据库实现(乐观...

Smily-王婷婷 1313

【Redis】Redis如何实现分布式锁及Redission分布式锁源码分析(一)

在单进程的系统中,当存在多个线程可以同时改变某个变量(共享变量)时,就需要注意该变量在多线程环境下是否线程安全,我们一般通过加锁来保证线程安全,即多线程操作下让代码也按照我们期望的方式执行。加锁的目的就是为了保证多个线程在一个时刻只有一个代码块可以执行(通过操作一个每个线程可见的共享变量来实现加锁),常用的单机锁就是synchronized,lock。但是如果部署多台机器,或者需要在不同的服务之间进行互斥操作,那么单机锁的方式就不适用了。

qq_35448165的博客 3746
上一篇: JKD动态代理及CGLIB动态代理
下一篇: Spring的扩展点及实例
ludkjava
博客等级 码龄11年 173粉丝 32原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值