使用Redis实现分布式锁解决商品超卖问题

1.业务描述

模拟一个商品的下订单业务。
用户下订单

第一步:用户单击下单接口
在这里插入图片描述
第二步:执行下单业务

  1. 先从Redis总获取库存,判断库存是否充足
    在这里插入图片描述
    在这里插入图片描述

  2. 当库存>=下单数量时

在这里插入图片描述

	扣减库存
	下单成功
	扣减库存

在这里插入图片描述

  1. 当库存<下单数量时
    在这里插入图片描述

    库存不足
    下单失败
    

2.超卖问题

商品超卖是指在电子商务系统或其他销售系统中,某个商品的库存数量被错误地减少到负数,导致卖出的商品数量超过了实际库存数量。这是一个常见的问题,特别是在高并发的情况下,可能会出现多个用户同时购买同一件商品,而系统没有正确处理这种竞争条件,导致超卖。
超卖问题可能会导致以下不良后果:

  • 库存不准确:超卖会导致库存数量不准确,系统显示的库存与实际库存不一致。

  • 用户体验差:用户购买的商品实际上已经售罄,但系统仍然接受订单,这会给用户带来不满和失望。

  • 损失和纠纷:商家需要履行超卖订单,或者退款给用户,这可能导致商家的损失和用户不满,甚至引发纠纷。

为了解决商品超卖问题,可以采取以下一些策略:
锁定库存:在用户下单时,首先锁定相应数量的库存。这可以通过分布式锁或数据库事务来实现。锁定库存后,其他用户将无法购买相同商品,直到订单完成或取消。

  • 扣减库存原子操作:扣减库存应当是一个原子操作,确保只有一个线程可以修改库存数量。这可以通过数据库事务或原子性的内存操作来实现。

  • 超卖检测:在扣减库存时,需要检查库存是否足够。如果库存不足,应拒绝订单或采取其他措施,而不是扣减库存。

  • 队列管理:使用队列来处理订单,确保每个订单按照先后顺序进行处理。这可以避免竞争条件。

  • 库存同步:定期或实时同步库存数量,确保库存数量准确。

  • 使用分布式锁:在分布式系统中,使用分布式锁可以避免不同节点之间的竞争条件。

解决商品超卖问题需要综合考虑业务逻辑、并发控制和系统架构等因素,以确保系统的高可用性和数据一致性。

3.使用Redis分布式锁解决

3.1 版本1

定义一个锁,尝试使用锁来控制多线程并发,实现多个线程同步访问Redis库,解决了单体项目超卖问题。

public String sale1(int count){
        String retMessage = "";
        lock.lock();
        try{
            // 1.查询库存
            String resultFromRedis=stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            lock.unlock();
        }
        return retMessage;
    }

缺点:无法解决分布式环境下产品超卖问题。

3.2 版本2

利用Redis的setnx命令,实现分布式锁,以递归的方式重试,解决了分布式环境下商品超卖问题。

public String sale2(int count){
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        // 尝试获取
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if(!flag){
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 递归尝试获取锁
            sale2(count);
        }else{
            try{
                // 1.查询库存
                String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
                // 判断库存是否足够
                Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
                if(num-count > 0){
                    stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                    retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                    System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");

                }else{
                    retMessage="商品已售罄!非常抱歉!";
                    System.err.println(retMessage);
                }
            }finally {
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage;
    }

缺点:由于使用了递归调用,当持有锁的线程,长时间未释放锁时,容易出现栈溢出。

3.3 版本3

利用Redis的setnx命令,实现分布式锁,以自旋的方式重试,解决了版本2中的栈溢出问题。

public String sale3(int count){
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        // 循环尝试获取
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        }
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage;
    }

缺点:当持有锁的微服务宕机时,锁没法释放,导致后面微服务无法获取锁。

3.4 版本4

在版本3的基础上,加入key的过期时间,解决了版本3出现的微服务无法获取锁的问题。

public String sale4(int count){
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        // 循环尝试获取
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        }
        //设置过期时间
        stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage;
    }

缺点:加锁和设置过期时间不是原子操作,出现原子问题

3.5 版本5

在版本4的基础上,加锁和设置时间使用一条原子命令,解决了版本4的问题

public String sale5(int count){
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        // 循环尝试获取
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        }
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage;
    }

缺点:由于多线程并发,引入误删除锁问题

3.6 版本6

只允许自己删除自己的锁,不可以删除别人添加的锁。

public String sale6(int count){
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        // 循环尝试加锁
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        }
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            //删除之前判断下是不是自己加的锁
            if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)){
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage;
    }

缺点:判断是不是自己的锁和删除操作不具有原子性

3.7 版本7

使用lua脚本解决版本6出现的问题。

 public String sale7(int count){
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        // 循环尝试加锁
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        }
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            //lua脚本:先判断一个key的value是否跟预期的一样,一样的话就删除这个key,否则什么也不做
            // if(条件) then //小括号可省略
            //   业务代码
            // elseif(条件) then
            //   业务代码
            // elseif(条件) then
            //   业务代码
            // elseif(条件) then
            //   业务代码
            // ...
            // else
            //   业务代码
            // end  //if终止符
            // eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock-test 112233
            // lua脚本执行语法:EVLA "脚本" 参数
            //删除之前判断下是不是自己加的锁
            String lua_script=
                    "if redis.call('get',KEYS[1]) == ARGV[1] " +
                            "then return redis.call('del',KEYS[1]) " +
                            "else " +
                            "return 0 " +
                            "end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(lua_script, Boolean.class), Arrays.asList(key), value);
        }
        return retMessage;
    }

缺点:当业务方法调用其他方法且其他方法需要加分布式锁的时候,未考虑可重入性,这个时候会其他方法也会尝试加锁。

3.8 版本8

考虑可重入性,维护一个重入度,使用hset key value recount,为了保证多个命令原子性,使用lua脚本。

public String sale8(int count){
        // 加锁的lua脚本,等价于lock方法
//     if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then
//            redis.call('hincrby',KEY[1],ARGV[1],1)
//            redis.call('expire',KEY[1],ARGV[2])
//            return 1
//      else
//        return 0
//     end

        // 解锁的lua脚本,等价于unlock方法
//     if(redis.call('exist',KEY[1],ARGV[1]) ==0 ) then
//        return nil
//     elseif(redis.call('hincrby',KEY[1],ARGV[1],-1) == 0) then
//        return redis.call('del',KEY[1])
//     else
//        return 0
//     end
        String retMessage = "";
        String key = "redis-lock";
        String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
        Integer expireTime = 50;
        String lua_lock="if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then" +
                "            redis.call('hincrby',KEY[1],ARGV[1],1)" +
                "            redis.call('expire',KEY[1],ARGV[2])" +
                "            return 1" +
                "        else" +
                "            return 0" +
                "        end";
        // 循环尝试加锁
        while(!stringRedisTemplate.execute(new DefaultRedisScript<>(lua_lock, Boolean.class), Arrays.asList(key), value, String.valueOf(expireTime))){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
        }
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            String lua_unlock="if(redis.call('exist',KEY[1],ARGV[1]) ==0 ) then" +
                    "             return nil" +
                    "          elseif(redis.call('hincrby',KEY[1],ARGV[1],-1) == 0) then" +
                    "             return redis.call('del',KEY[1])" +
                    "          else" +
                    "             return 0" +
                    "          end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(lua_unlock, Long.class), Arrays.asList(key), value);
        }
        return retMessage;
    }

缺点:耦合度高

3.9 版本9

将锁封装为一个类,实现Lock规范,降低耦合

 private MyRedisLock myRedisLock = new MyRedisLock(stringRedisTemplate,"redis-lock");
 public String sale9(int count){
        String retMessage = "";
        myRedisLock.tryLock();
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            myRedisLock.unlock();
        }
        return retMessage;
    }

自定义锁实现Lock接口

public class MyRedisLock implements Lock {

    private StringRedisTemplate stringRedisTemplate;
    private String key;
    private String value;
    private long expireTime;

    public MyRedisLock(StringRedisTemplate stringRedisTemplate, String key,String value) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.key = key;
        this.value= value;
        this.expireTime = 50L;
    }

    public MyRedisLock(StringRedisTemplate stringRedisTemplate, String key) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.key = key;
        this.value= UUID.randomUUID()+":"+Thread.currentThread().getId();
        this.expireTime = 50L;
    }

    @Override
    public void lock() {
        tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {tryLock(-1L, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if(time == -1){
            String lua_lock="if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then" +
                    "            redis.call('hincrby',KEY[1],ARGV[1],1)" +
                    "            redis.call('expire',KEY[1],ARGV[2])" +
                    "            return 1" +
                    "        else" +
                    "            return 0" +
                    "        end";
            while(!stringRedisTemplate.execute(new DefaultRedisScript<>(lua_lock,Boolean.class), Arrays.asList(key),value,String.valueOf(expireTime))){
                TimeUnit.MILLISECONDS.sleep(50);
            }
            System.out.println("锁的名字:"+key);
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
        String lua_unlock="if(redis.call('exist',KEY[1],ARGV[1]) ==0 ) then" +
                "             return nil" +
                "          elseif(redis.call('hincrby',KEY[1],ARGV[1],-1) == 0) then" +
                "             return redis.call('del',KEY[1])" +
                "          else" +
                "             return 0" +
                "          end";
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(lua_unlock, Long.class), Arrays.asList(key), value);
        System.out.println("锁的名字:"+key);
        if(null == flag){
            throw new RuntimeException("当前锁不存在!");
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }

}

缺点:通用性不好

3.10 版本10

引入工厂模式,实现通配型

@Autowired
    private MyLockFactory myLockFactory1;
    public String sale10(int count){
        Lock myRedisLockFromFactory = myLockFactory1.getLock("REDIS");
        String retMessage = "";
        myRedisLockFromFactory.tryLock();
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            myRedisLockFromFactory.unlock();
        }
        return retMessage;
    }

工厂类:

@Component
public class MyLockFactory {

    private String key;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private String value;

    public MyLockFactory() {
        this.value = UUID.randomUUID()+":"+Thread.currentThread().getId();
    }

    public Lock getLock(String lockType){
        if(lockType == null) return null;
        if(lockType.equalsIgnoreCase("REDIS")){
            this.key="redis-lock";
            return new MyRedisLock(stringRedisTemplate,key,value);
        }else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
            this.key="zookeeper-lock";
            return null;//返回zookeeper版本的分布式锁
        }else if(lockType.equalsIgnoreCase("MYSQL")){
            this.key="zookeeper-lock";
            return null;//返回MySQL版本的分布式锁
        }else
            return null;
    }
}

缺点:没有自动续期

3.11 版本11

定时任务+lua脚本实现自动续期

@Autowired
    private MyLockFactory myLockFactory2;
    public String sale11(int count){
        // 自动续期脚本
//         if(redis.call('hexists',KEY[1],ARGC[1] ==1 ) then
//            return redis.call('expire',KEY[1],ARGV[2])
//         else
//            return 0;
//         end
        Lock myRedisLockFromFactory = myLockFactory2.getLock("REDIS");
        String retMessage = "";
        myRedisLockFromFactory.tryLock();
        try{
            // 1.查询库存
            String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
            // 判断库存是否足够
            Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
            if(num-count > 0){
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
                retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
                System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
                // 模拟长业务
                try {TimeUnit.SECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}

            }else{
                retMessage="商品已售罄!非常抱歉!";
                System.err.println(retMessage);
            }
        }finally {
            myRedisLockFromFactory.unlock();
        }
        return retMessage;
    }

在自定义分布式锁加入定时任务:

private void reNewExpire() {
        String lua_reNewExpire="if(redis.call('hexists',KEY[1],ARGC[1] ==1 ) then" +
                "                   return redis.call('expire',KEY[1],ARGV[2])" +
                "                   else" +
                "               return 0;" +
                "               end";
       new Timer().schedule(new TimerTask() {
           @Override
           public void run() {
               if(stringRedisTemplate.execute(new DefaultRedisScript<>(lua_reNewExpire, Boolean.class), Arrays.asList(key), value, String.valueOf(expireTime))){
                   reNewExpire();
               }
           }
       },(this.expireTime*1000) / 3);//每三分之一过期时长执行一次

    }

重写加锁流程:

 @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if(time == -1){
            String lua_lock="if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then" +
                    "            redis.call('hincrby',KEY[1],ARGV[1],1)" +
                    "            redis.call('expire',KEY[1],ARGV[2])" +
                    "            return 1" +
                    "        else" +
                    "            return 0" +
                    "        end";
            while(!stringRedisTemplate.execute(new DefaultRedisScript<>(lua_lock,Boolean.class), Arrays.asList(key),value,String.valueOf(expireTime))){
                TimeUnit.MILLISECONDS.sleep(50);
            }
            // 后台程序,自动续期
            reNewExpire();
            System.out.println("锁的名字:"+key);
            return true;
        }
        return false;
    }

缺点:安全性不能提供保证,例如,当Redis服务器宕机。
改进:使用基于Redis官方提供的RedLock算法来实现多个实例下的分布式锁。例如基于Java的Redisson。

4.总结

使用Redis实现分布式锁是一种有效的方式来管理并发访问共享资源。Redis提供了原子性的操作,特别适合用于构建分布式锁。在这种锁的设计中,每个请求都尝试获取锁,只有一个请求成功,其他请求被阻塞。利用Redis的SETNX(SET if Not eXists)命令,可以确保锁的原子性,从而避免竞争条件。

这种分布式锁的优点包括快速响应、可靠性和简单性。但需要注意的是,为了避免死锁,锁应该具有超时机制,以确保即使持有锁的客户端崩溃,锁最终会被释放。此外,分布式锁的实现需要谨慎处理锁的释放,确保只有持有锁的客户端才能释放它。总之,Redis分布式锁是构建分布式系统中的重要工具,可用于解决并发访问共享资源时的竞争条件问题。
但是需要考虑更多的细节,一步一步改进。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值