Redis实战——商户查询缓存

Redis从入门到精通(五)Redis实战(二)商户查询缓存 Redis实现(二):缓存介绍、查询商户信息的传统做法、查询商户信息添加Redis缓存、数据一致性问题及其解决方案、缓存穿透问题及其解决方案、缓存击穿问题及其解决方案。 阅读详情

目录

缓存简单介绍

Redis缓存的大致实现图 

缓存更新策略

三种方案 

三个问题 

先操作缓存 还是 先操作数据库?

先删除缓存再操作数据库

正常情况:

异常情况: 

 先操作数据库,再删除缓存

正常情况:

异常情况:

总结

缓存的三个问题

缓存穿透

缓存雪崩​

缓存击穿​


缓存简单介绍


Redis缓存的大致实现图 

缓存更新策略

解决:当数据库中的数据进行了更改,Redis缓存并不知晓,在缓存中查询到仍然是旧数据

三种方案 

三个问题 

先操作缓存 还是 先操作数据库?

示例:

缓存与数据库事先存入的初始值都为10

先删除缓存再操作数据库

正常情况:

       线程1删除缓存,然后更新数据库 v = 20;

       线程2查询缓存未命中,而后查询数据库 v,并写入缓存

 

异常情况: 

        当线程1删除缓存后,由于更新数据库的耗时较长,线程2插入,执行查询缓存,未命中查询数据库v = 10,并写入缓存;线程1而后更新数据库 v = 20,这导致缓存中数据变成了旧数据,缓存中的数据和数据库中的数据不一致

 

 先操作数据库,再删除缓存

正常情况:

        线程2更新数据库 v= 20,再删除缓存;

        线程1查询缓存未命中,查询数据库 v = 20,并写入缓存

异常情况:

        线程1查询的时候,缓存恰好失效,查询缓存未命中,而后查询数据库,被线程2插入,执行更新数据库,删除缓存操作,线程1再写入缓存,这就导致线程1写入缓存的数据为旧数据

总结

方案二更好,方案二发生的情况要求  有线程查询的时候,缓存恰好失效;发生的概率相较于方案一的异常状况更加小

缓存的三个问题

缓存穿透

解决方法:(1)缓存空对象:当缓存未命中,数据库也未命中时,缓存null,而后每次查询

                  (2)布隆过滤:

修改流程,缓存空对象

实现

    public Shop queryWithPassThrough(Long id) {
        String key=CACHE_SHOP_KEY+id;
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(JsonShop)){//null,"","\n"都属于false
            Shop shop = JSONUtil.toBean(JsonShop, Shop.class);
            return shop;
        }

        //判断命中的是否为空值
        if(JsonShop!=null){
            return null;
        }

        Shop shop = getById(id);
        if(shop==null){
            //缓存空对象
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }


缓存雪崩


缓存击穿

利用互斥锁与逻辑过期(旧的key仍存在)解决缓存击穿

互斥锁与逻辑过期的优缺点

        简单实现互斥锁:利用Redis中的SETNX命令实现互斥锁的效果,释放锁就相当于删除key(一般设置有效期)

获取锁与释放锁

(不直接return flag的原因:防止在拆箱装箱过程中出现空指针,所以使用hutool中的工具类)

    private boolean trylock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

具体实现

public Shop queryWithMuteX(Long id) {
        String key=CACHE_SHOP_KEY+id;

        // redis查看缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(JsonShop)){//null,"","\n"都属于false
            Shop shop = JSONUtil.toBean(JsonShop, Shop.class);
            return shop;
        }

        //判断命中的是否为空值
        if(JsonShop!=null){
            return null;
        }
        //实现缓存重建
        //1.尝试获取互斥锁

        String lockKey="lock:shop"+id;
        Shop shop = null;
        try {
            boolean isLock = trylock(lockKey);
            //2.判断是否获取成功
            if(!isLock){
                //失败,休眠并重试
                Thread.sleep(50);
                return queryWithMuteX(id);
            }
            //成功,根据id查询数据库
            shop = getById(id);

            //模拟重建的延时
            Thread.sleep(200);

            if(shop==null){
                //缓存空对象
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //3.释放互斥锁
            unlock(lockKey);
        }

        return shop;
    }

 


不需对原来的shop进行字段的修改,重新写一个类,使其进行继承或设置

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

声明线程池 

链接:java并发编程:Executor、Executors、ExecutorService

//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

实现:

@Override
    public Shop queryWithLogicalExpire(Long id) {
        String key=CACHE_SHOP_KEY+id;

        // redis查看缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(JsonShop)){
            //不存在直接返回null
            return null;
        }
        //命中,需要json反序列化为对象
        RedisData redisData = JSONUtil.toBean(JsonShop, RedisData.class);
//        Object data = redisData.getData(); //本质是JSONObject

        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺信息
            return shop;
        }

        //过期,需要缓存重建

        //1.尝试获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        boolean islock = trylock(lockKey);
        //2.判断是否成功
        if(islock){
            //成功开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }

        //返回过期的商铺信息
        return shop;
    }


优化:对上述查询缓存进行封装

 实现:(采用泛型与函数式编程)

@Slf4j
@Component
public class CacheClient {
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private boolean trylock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
        //设置逻辑过期
        RedisData redisData=new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

        //写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit) {
        String key=keyPrefix+id;
        String Json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(Json)){//null,"","\n"都属于false
            return JSONUtil.toBean(Json, type);
        }

        //判断命中的是否为空值
        if(Json!=null){
            return null;
        }

        R r = dbFallback.apply(id);
        if(r==null){
            //缓存空对象
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        this.set(key,r,time,unit);
        return r;
    }

    public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit) {
        String key=keyPrefix+id;

        // redis查看缓存
        String JsonShop = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(JsonShop)){
            //不存在直接返回null
            return null;
        }
        //命中,需要json反序列化为对象
        RedisData redisData = JSONUtil.toBean(JsonShop, RedisData.class);
//        Object data = redisData.getData(); //本质是JSONObject

        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺信息
            return r;
        }

        //过期,需要缓存重建

        //1.尝试获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        boolean islock = trylock(lockKey);
        //2.判断是否成功
        if(islock){
            //成功开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //查询数据库
                    R r1 = dbFallback.apply(id);

                    //重建缓存
                    this.setWithLogicalExpire(key,r1,time,unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }

        //返回过期的商铺信息
        return r;
    }
}

与之前的差异

    @Override
    @Transactional
    public Result queryById(Long id) {
        //缓存穿透
//        Shop shop = queryWithPassThrough(id);
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //互斥锁解决缓存击穿
//        Shop shop=queryWithMuteX(id);
        
        //利用逻辑过期解决缓存击穿
//        Shop shop=queryWithLogicalExpire(id);
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        if(shop==null){
            return Result.fail("店铺不存在!");
        }

        return Result.ok(shop);
    }

点餐系统数据库设计--SQL Server 友情链接 1、学生成绩管理系统数据库设计–MySQL 学生成绩管理系统数据库设计–MySQL 2、医疗信息管理系统数据库–MySQL 医疗信息管理系统数据库–MySQL 3、邮件管理数据库设计–MySQL 邮件管理数据库设计–MySQL 4、SQL Server医疗信息管理系统数据库【英文版-源码】–(Medical Management System Database) SQL Server医疗信息管理系统数据库【英文版-源码】–(Medical Management System Database) 5 阅读详情

相关推荐

单细胞转录组数据分析全流程:从原始数据到细胞注释

本文详细介绍了单细胞转录组数据分析的全流程,从原始数据读取、环境搭建、数据质控与标准化,到降维聚类、细胞类型注释,以及高级质控与多样本整合。重点阐述了如何利用Seurat等工具进行数据处理,并通过标志基因比对与自动注释方法完成细胞身份鉴定,为研究者提供了一套清晰、可操作的分析指南。

coffee的博客 553

封装RedisTemplate的RedisUtil工具类

public class RedisUtil { private RedisTemplate<String, Object> redisTemplate; public void setRedisTemplate(RedisTemplate<Strin...

choufei4244的博客 1456

QT串口通信控制LED上位机开发教程

串口通信是嵌入式系统与PC端交互的基础技术,通过UART协议实现设备间的数据传输。QT框架提供了跨平台的串口编程支持,结合其GUI模块可以快速开发上位机控制程序。在工业自动化和物联网领域,这种上下位机架构广泛应用于设备监控和控制场景。本案例展示了如何使用QT的SerialPort模块实现LED控制功能,包含串口参数配置、指令发送等核心功能实现,为嵌入式GUI开发提供实践参考。

weixin_42598278的博客 583

【StringRedisTemplate常见用法】

StringRedisTemplate常见用法

qq_41027259的博客 1万+

【YOLOV5-5.x 源码讲解】整体项目文件导航

前言 这个项目是github的开源项目,YOLOV5:https://github.com/ultralytics/yolov5,目前已经有14.1k个Star 和 4.9k 个Folk了,非常的火。下面我会给大家逐个的文件介绍这个项目中的所有代码,希望能帮到大家。 我下的2021年4月12日更新的v5.0版本,整个项目我做了一点点的文件位置的改变,也加了点自己在其他论文中学到的tricks,不过我没有删除它源码任何一个代码。下面我会按文件逐个函数的进行攻破,因为我本人也还是研一,肯定会有很多的不足,希望大

qq_38253797的博客 6万+

STM32CUBEIDE调试中HardFault_Handler崩溃的故障分析器实战解析

本文详细解析STM32CUBEIDE调试中HardFault_Handler崩溃问题的实战处理方法。通过内置故障分析器自动诊断硬件异常原因,如内存越界、堆栈溢出等,提供图形化界面简化调试流程,并分享配置步骤、典型案例及高级调试技巧,帮助开发者快速定位和解决嵌入式系统崩溃问题。

night的博客 645

Redis的StringRedisTemplate在SpringBoot中的使用完全整理之opsForValue

opsForValue() 目录 opsForValue() set(String k,String v) set(String k,String v,long l) set(String k,String v,long l,TimeUtil timeUtil)与set(String k,String v,Duration timeout) setBit(String k,lon...

baidu_17353319的博客 1万+

鱼眼相机去畸变(图像拉直/展开/矫正)算法及实战总结

1、算法说明经纬度矫正法, 可以把鱼眼图想象成半个地球, 然后将地球展开成地图,经纬度矫正法主要是利用几何原理, 对图像进行展开矫正。经过P点的入射光线没有透镜的话,本应交于相机成像平面的e点。然而,经过鱼眼相机的折射,光线会交于相机成像平面的d点,就产生了畸变,因此畸变图像整体上呈现出像素朝图像中心点聚集的态势。而去畸变,就是将折射到d点的点,重新映射回到e点,因此去畸变之后的图像与原始的鱼眼图像相比,仿佛是把向心聚集的像素又重新向四周铺展开来。AVM环视系统——鱼眼相机去畸变算法 - 知乎。

大胡子大叔的专栏 2万+

Java的List和Json转换以及StringRedisTemplate往redis存泛型对象

List转Json List<User> user= new ArrayList(); String str = JSON.toJSONString(user); Json 转List方法一 List<User> user= JSON.parseArray(json,User.class); 如果是泛型方法需要使用TypeReference Json ...

深度Java 5149

Mamba SSM 实战:IMDb 文本分类 + Mamba-ssm 全流程

Mamba SSM 文本分类链路:IMDb 二分类 → MambaLMHeadModel (d_model=256, n_layer=8, d_state=16, exp2=2) → 末 token 分类头。5 epoch 达 89.2% 准确率 (与 BERT-base 持平), 但显存降低 44% (3.5GB vs 6.2GB)。建议序列长度 ≥ 1024 再选 Mamba, 短序列 (< 512) 用 BERT 更高效。扩展时注意d_state。

m0_71589036的博客 58

PyTorch 面试题及参考答案(精选100道)

模块是 PyTorch 中用于自动求导的核心模块。它为张量上的所有操作提供了自动求导的功能。在深度学习中,反向传播是一种用于计算损失函数相对于模型参数的梯度的算法。通过梯度,我们可以使用优化算法(如随机梯度下降)来更新模型的参数,从而最小化损失函数。模块的主要作用就是自动计算这些梯度。当我们创建一个张量时,如果将其属性设置为True,那么 PyTorch 会跟踪该张量上的所有操作。在完成前向传播计算出损失函数后,调用方法,模块会自动根据链式法则反向传播计算出所有需要求导的张量的梯度。

大模型大数据攻城狮的专栏 1681

SpringBoot StringRedisTemplate(Redis)的使用

【代码】SpringBoot-StringRedisTemplate(Redis)的使用。

weixin_62971115的博客 479

java实现长短期记忆人工神经网络LSTM(附完整源码)

java实现长短期记忆人工神经网络LSTM(附完整源码)

希望我的博客,能帮上你解决学习中工作中所遇到的问题 1692

02实战篇Redis01(短信登录、商户查询缓存)

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的前言什么是缓存?就像自行车,越野车的避震器举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样;

qq_56947957的博客 1187

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

【Redis】Redis实战:黑马点评之商户查询缓存

在原来的业务逻辑中,如果用户查询缓存未命中,会去直接查询数据库,在这种情况下,如果用户查询的是热点数据且缓存重建比较复杂,就容易出现缓存击穿问题,这里我们可以假设商铺查询就是热点数据,需要针对缓存击穿问题设计相应的解决方案 相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是进行查询之后,如果从缓存未命中,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得锁,则休眠,过一段时间再对上述业务逻辑进行重试,获取到锁的线程可以需要去查询数据库并将数

qq_50675319的博客 1649

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Redis——Redis实战:2 商户查询缓存

对缓存做了介绍,并通过案例讲解了如何添加Redis缓存以及缓存更新策略,然后讲解了缓存穿透、缓存雪崩和缓存击穿,最后封装了一个Redis工具类。

念头通达 1113

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

【Redis实战篇】商户查询缓存

了解什么是缓存、如何使用缓存、缓存的更新策略、双写一致问题、认识什么是缓存穿透、缓存雪崩、缓存击穿等,以及在项目中的运用。

qq_63320529的博客 1355

Redis实战篇-商户查询缓存

Redis实战篇-商户查询缓存,主要讲解了缓存穿透、缓存雪崩、缓存击穿的解决方案,并且封装了对应的配置类

2302_76649176的博客 1328

黑马程序员Redis入门到实战教程---Redis快速入门(02实战篇-短信登录、商户查询缓存)(2025JavaReview)

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的前言什么是缓存?就像自行车,越野车的避震器举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样;

Frank---7的博客 1123

Redis实战篇《黑马点评》2 商户查询缓存

什么是缓存?缓存就像自行车、越野车的避震器举个例子越野车、山地自行车都有避震器,防止车体加速之后因惯性,在U型地形上飞跃硬着陆导致损坏,像个弹簧意义同样,在实际开发中,系统也需要避震器,防止过高的数据量猛冲系统,导致其操作线程无法及时处理信息而瘫痪在实际开发中,对企业来讲,产品口碑、用户评价都是致命的,所以企业非常重视缓存技术缓存(Cache)就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地。

wyznan的博客 902

[Redis实战]商户查询缓存

缓存穿透产生的原因是什么?

m0_71229255的博客 1186

Redis项目实战——商户查询缓存

部分热点key(被高并发访问且缓存重建业务较复杂的key)在缓存中失效或过期,如果第一次请求未命中,会去数据库查询并写入缓存,但若在此期间有大量同样的请求涌入,就会有大量请求涌到数据库要求查询,给数据库造成巨大压力。客户端请求的数据在缓存和数据库中都没有,无法建立缓存,如果多次请求这个不存在的数据,这些请求就会全部打到数据库,给数据库造成压力。大量redis缓存中的key同时失效或过期,或者redis服务器宕机,大量请求直接到达数据库,给数据库造成巨大压力。宕机一个redis还有其他redis。

weixin_46838605的博客 1369

实战之-Redis商户查询缓存

核心思路如下:修改ShopController中的业务逻辑,满足下面的需求:根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间根据id修改店铺时,先修改数据库,再删除缓存修改重点代码1:修改的queryById方法设置redis缓存时添加过期时间修改重点代码2。

· 1138

Redis项目实战--商户查询缓存

1、解决缓存空对象的缺点的方式:每次新增数据时,主动的将数据插入到redis缓存中,覆盖之前的null。2、布隆过滤器中存放的是一个个字节,并非真正的数据。1、判断String类型是否为空:StrUtil.isNotBlank(shopJson)。当shopJson为null、""、" "是均返回false。1、需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题2、注意:这里的互斥锁不是我们平常用的synolize锁或者lock锁:这种锁,我们拿到了可以执行代码,没有拿到则继续等待。

wjf83902的博客 1015

Redis-Day3实战篇-商户查询缓存(缓存的添加和更新, 缓存穿透/雪崩/击穿, 缓存工具封装)

Redis-Day3实战篇-商户查询缓存(缓存的添加和更新, 缓存穿透/雪崩/击穿, 缓存工具封装)

Y_cen的博客 1834

03、Redis实战:商户查询缓存、缓存更新策略、缓存穿透、缓存雪崩、缓存击穿

什么是缓存?就像自行车,越野车的避震器举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样;同样,实际开发中,系统也需要"避震器",防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪;这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存技术;缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据。

u012011912的博客 1433
上一篇: 亿级别大数的排序查找
下一篇: 深入理解Maven
486过于烦躁
博客等级 码龄6年 17粉丝 34原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值