16_分布式锁

分布式锁的面试题

问题1:Redis除了拿来做缓存,你还见过基于Redis的什么用法?

答:传统五大数据类型的落地应用;做分布式锁

问题2:Redis做分布式锁的时候有需要注意的问题?

问题3:如果是Redis是单点部署的,会带来什么问题?

​ 那你准备怎么解决单点问题呢?

问题4:集群模式下,比如主从模式,有没有什么问题呢?

问题5:那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈

问题6:Redis分布式锁如何续期?看门狗知道吗?


Base案例(boot+redis)

使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)

1、建Module

两个微服务:boot_redis01、boot_redis02

2、改POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.hhf</groupId>
    <artifactId>boot_redis01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3、建YML
server.port=1111
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379 
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认0
spring.redis.lettuce.pool.min-idle=0 

4、启动类
@SpringBootApplication
public class Application01 {
    public static void main(String[] args) {
        SpringApplication.run(Application01.class, args);
    }
}

5、config
@Configuration
public class RedisConfig {
    /**
     * 保证不是序列化后的乱码配置
     */
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

6、controller
@RestController
public class RedisController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods() {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    }
}

7、测试

访问:

http://localhost:1111/buy_goods

http://localhost:2222/buy_goods

在这里插入图片描述

在这里插入图片描述

正常访问!


吐槽

1.单机版没加锁

没有加锁,并发下数字不对,出现超卖现象

思考:加synchronized?加ReentrantLock?还是都可以?

答 :视业务需求而定。synchronized加锁的话会一直进行阻塞直到获得锁,而ReentrantLock可以使用tryLock()设置获取锁的时间,如果获取不到锁可以中断做其它事情。


解决办法

修改为2.0版本:

@GetMapping("/buy_goods")
public String buy_Goods() {
    synchronized (this) {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    }
}

2.nginx分布式微服务架构

分布式部署后,单机锁还是出现超卖现象,需要分布式锁

在这里插入图片描述


Nginx配置负载均衡

我这里使用的docker配置Nginx实现负载均衡

详细参考:Dokcer部署NginxNginx实现负载均衡


下面进行测试

手动方式:

在这里插入图片描述
在这里插入图片描述

可以发现可以轮询访问微服务。


高并发模拟:模拟2000个线程同时访问微服务

在这里插入图片描述
在这里插入图片描述


结果:

在这里插入图片描述
在这里插入图片描述

出现重复消费问题!


解决办法

上redis分布式锁setnx:Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理.

修改为3.0

public static final String REDIS_LOCK_KEY = "lock";

@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    //setIfAbsent() 就是如果不存在就新建
    Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
    if (!lockFlag) {
        return "抢锁失败,┭┮﹏┭┮";
    } else {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
            stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    }
}

3.程序异常

出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁

加锁解锁,lock/unlock必须同时出现并保证调用

解决办法

修改为4.0

@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

    try {
        //setIfAbsent() 就是如果不存在就新建           
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx   
        if (!lockFlag) {
            return "抢锁失败,┭┮﹏┭┮";
        } else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    } finally {
        stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁 
    }
}

4.微服务宕机

部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key

解决办法

修改为5.0

@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        //setIfAbsent() 就是如果不存在就新建 
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx   
        stringRedisTemplate.expire(REDIS_LOCK_KEY, 10L, TimeUnit.SECONDS);
        if (!lockFlag) {
            return "抢锁失败,┭┮﹏┭┮";
        } else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    } finally {
        stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁 
    }
}

5.过期时间设置不能保证原子性

设置key+过期时间分开了,必须要合并成一行具备原子性

解决办法

修改为6.0:换重载方法,同时设置过期时间

@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        //setIfAbsent() 就是如果不存在就新建
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
        if (!lockFlag) {
            return "抢锁失败,┭┮﹏┭┮";
        } else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    } finally {
        stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
    }
}

6.误释放锁

张冠李戴,删除了别人的锁

解决办法

修改为7.0版本:对要删除的锁进行判断

@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        //setIfAbsent() 就是如果不存在就新建
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
        if (!lockFlag) {
            return "抢锁失败,┭┮﹏┭┮";
        } else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    } finally {
        if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
            stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁    
        }
    }
}

7.释放锁不是原子性

finally块的判断+del删除操作不是原子性的

解决办法1:使用Redis事务
@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        //setIfAbsent() 就是如果不存在就新建
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
        if (!lockFlag) {
            return "抢锁失败,┭┮﹏┭┮";
        } else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    } finally {
        while (true) {
            stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁     
            if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
                stringRedisTemplate.setEnableTransactionSupport(true);
                stringRedisTemplate.multi();//开始事务      
                stringRedisTemplate.delete(REDIS_LOCK_KEY);
                List<Object> list = stringRedisTemplate.exec();
                if (list == null) {  //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除                     
                    continue;
                }
            }                
            //如果删除成功,释放监控器,并且break跳出当前循环   
            stringRedisTemplate.unwatch();
            break;
        }
    }
}

解决办法2:使用Lua脚本

Redis可以通过eval命令保证代码执行的原子性

public class RedisUtils {
    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool = new JedisPool(jedisPoolConfig, "ip", 6379, 100000);
    }

    public static Jedis getJedis() throws Exception {
        if (null != jedisPool) {
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool is not ok");
    }
}

@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try {
        //setIfAbsent() 就是如果不存在就新建
        Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
        if (!lockFlag) {
            return "抢锁失败,┭┮﹏┭┮";
        } else {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    } finally {
        Jedis jedis = RedisUtils.getJedis();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1]" + "then " + "return redis.call('del', KEYS[1])" + "else " + "  return 0 " + "end";
        try {
            Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
            if ("1".equals(result.toString())) {
                System.out.println("------del REDIS_LOCK_KEY success");
            } else {
                System.out.println("------del REDIS_LOCK_KEY error");
            }
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }
    }
}

8.redis续期问题

确保redisLock过期时间大于业务执行时间的问题

集群+CAP对比zookeeper

  • Redis:AP:redis异步复制造成的锁丢失, 比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。此时如果集群模式下,就得上Redisson来解决
  • Zookeeper:CP

解决办法

redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现

配置类:

@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

controller:

@Autowired
private Redisson redisson;

@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
    redissonLock.lock();
    try {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    } finally {
        redissonLock.unlock();
    }
}

9.完善

可能出现错误:

在这里插入图片描述

是在并发多的时候就可能会遇到这种错误,可能会被重新抢占

不见得当前这个锁的状态还是在锁定,并且本线程持有

解决办法
@GetMapping("/buy_goods")
public String buy_Goods() {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
    redissonLock.lock();
    try {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if (goodsNumber > 0) {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
        } else {
            System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    } finally {
        //还在持有锁的状态,并且是当前线程持有的锁再解锁   
        if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
            redissonLock.unlock();
        }
    }

总结

synchronized 单机版oK,上分布式

===> nginx分布式微服务 单机锁不行

===>取消单机锁 上redis分布式锁setnx

===> 只加了锁,没有释放锁, 出异常的话,可能无法释放锁,必须要在代码层面finally释放锁

===> 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定

===> 为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行的原子性操作

===>必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3

===> lua或者事务

===> redis集群环境下,我们自己写的也不OK直接上RedLock之Redisson落地实现


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值