研一后端学习--Redis EP03

黑马点评

三、优惠券秒杀

1、全局ID生成器

 

utils/RedisIdWorker
@Component
 public class RedisIdWorker {
 ​
     /**
      * 开始时间戳
      */
     private static final long BEGIN_TIMESTAMP = 1735689600L;
     /**
      * 序列号的位数
      */
     private static final int COUNT_BIT = 32;
 ​
     private StringRedisTemplate stringRedisTemplate;
 ​
     public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     //keyPrefix--->前缀,区分不同的业务,不同的自增长
     public long nextId(String keyPrefix) {
         //1.生成时间戳(当前时间-开始时间 的秒数)
         LocalDateTime now = LocalDateTime.now();
         //当前时间转换成秒数
         long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
         long timeStamp = nowSecond - BEGIN_TIMESTAMP;
 ​
         //2.生成序列号
         //2.1获取当前日期,精准到天(一天一个key)
         String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
         //2.2自增长
         long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
         //3.拼接并返回
         //时间戳左移32位
         return  timeStamp << COUNT_BIT | count;
 ​
     }
 ​
     public static void main(String[] args) {
         //time对应的秒数
         LocalDateTime time = LocalDateTime.of(2025, 1, 1, 0, 0);
         long seconds = time.toEpochSecond(ZoneOffset.UTC);
         System.out.println(seconds);
     }
 }
2、优惠券秒杀下单

VoucherOrderController
 @RestController
 @RequestMapping("/voucher-order")
 public class VoucherOrderController {
 ​
     @Resource
     private IVoucherOrderService iVoucherOrderService;
 ​
     @PostMapping("seckill/{id}")
     public Result seckillVoucher(@PathVariable("id") Long voucherId) {
         return iVoucherOrderService.secKillVoucher(voucherId);
     }
 }
VoucherOrderServiceImpl
@Transactional
 public Result secKillVoucher(Long voucherId) {
     //1.查询优惠券
     SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
     //2.判断秒杀是否开始
     if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
         //返回错误信息
         return Result.fail("秒杀尚未开始!");
     }
     //3.判断秒杀是否结束
     if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
         return Result.fail("秒杀已经结束!");
     }
     //4.判断库存是否充足
     if (voucher.getStock() < 1) {
         //库存不足
         return Result.fail("库存不足!");
     }
     //5.扣减库存
     boolean success = seckillVoucherService.update()
         .setSql("stock = stock - 1")
         .eq("voucher_id", voucherId).update();
     if(!success) {
         //扣减失败
         return Result.fail("库存不足!");
     }
     //6.创建订单
     VoucherOrder voucherOrder = new VoucherOrder();
     //6.1订单id
     long orderId = redisIdWorker.nextId("order");
     voucherOrder.setId(orderId);
     //6.2用户id
     Long userId = UserHolder.getUser().getId();
     voucherOrder.setUserId(userId);
     //6.3代金券id
     voucherOrder.setVoucherId(voucherId);
     save(voucherOrder);
     //7.返回订单id
     return Result.ok(orderId);
 }

乐观锁解决超卖问题
VoucherOrderServiceImpl
@Transactional
 public Result secKillVoucher(Long voucherId) {
     //1.查询优惠券
     SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
     //2.判断秒杀是否开始
     if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
         //返回错误信息
         return Result.fail("秒杀尚未开始!");
     }
     //3.判断秒杀是否结束
     if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
         return Result.fail("秒杀已经结束!");
     }
     //4.判断库存是否充足
     if (voucher.getStock() < 1) {
         //库存不足
         return Result.fail("库存不足!");
     }
     //5.扣减库存
     //乐观锁:判断操作前后库存是否相同
     boolean success = seckillVoucherService.update()
         .setSql("stock = stock - 1")//set stock = stock - 1;
         .eq("voucher_id", voucherId).gt("stock", 0)//where id = ? and stock > 0
         .update();
     if(!success) {
         //扣减失败
         return Result.fail("库存不足!");
     }
     //6.创建订单
     VoucherOrder voucherOrder = new VoucherOrder();
     //6.1订单id
     long orderId = redisIdWorker.nextId("order");
     voucherOrder.setId(orderId);
     //6.2用户id
     Long userId = UserHolder.getUser().getId();
     voucherOrder.setUserId(userId);
     //6.3代金券id
     voucherOrder.setVoucherId(voucherId);
     save(voucherOrder);
     //7.返回订单id
     return Result.ok(orderId);
 }
一人一单

VoucherOrderServiceImpl
public Result secKillVoucher(Long voucherId) {
     //1.查询优惠券
     SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
     //2.判断秒杀是否开始
     if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
         //返回错误信息
         return Result.fail("秒杀尚未开始!");
     }
     //3.判断秒杀是否结束
     if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
         return Result.fail("秒杀已经结束!");
     }
     //4.判断库存是否充足
     if (voucher.getStock() < 1) {
         //库存不足
         return Result.fail("库存不足!");
     }    
     //userId.toString(),toString的底层实现new String,不能保证同一个id每次来都是同一个对象(即同一把锁),需要Intern方法
     Long userId = UserHolder.getUser().getId();
     //锁加在用户上,可能出现用户提交后释放锁,但是数据尚未写入数据库而出现并发安全问题。需要把锁加在函数上。
     //事务失效问题:此处调用createVoucherOrder()方法的是VoucherOrderServiceImpl对象,但是事务起作用应该是spring管理的VoucherOrderServiceImpl代理对象
     synchronized(userId.toString().intern()) {
         //拿到当前对象的代理对象 ps:aspectjweaver依赖  启动类注解@EnableAspectJAutoProxy(exposeProxy = true)
         IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
         return proxy.createVoucherOrder(voucherId);
     }
 }
 ​
 ​
 @Transactional
 //synchronized加在方法上,所有用户共用一把锁,串行执行,性能差。把锁加在用户上。
 public Result createVoucherOrder(Long voucherId) {
     //5.一人一单
     //5.1查询订单
     int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
     //5.2判断是否存在
     if(count > 0) {
         //用户已经购买过了
         return Result.fail("用户已经购买过一次!");
     }
     //6.扣减库存
     //乐观锁:判断操作前后库存是否相同
     boolean success = seckillVoucherService.update()
         .setSql("stock = stock - 1")//set stock = stock - 1;
         .eq("voucher_id", voucherId).gt("stock", 0)//where id = ? and stock > 0
         .update();
     if(!success) {
         //扣减失败
         return Result.fail("库存不足!");
     }
     //7.创建订单
     VoucherOrder voucherOrder = new VoucherOrder();
     //7.1订单id
     long orderId = redisIdWorker.nextId("order");
     voucherOrder.setId(orderId);
     //7.2用户id
     Long userId = UserHolder.getUser().getId();
     voucherOrder.setUserId(userId);
     //7.3代金券id
     voucherOrder.setVoucherId(voucherId);
     save(voucherOrder);
     //8.返回订单id
     return Result.ok(orderId);
 }
3、分布式锁

当存在多个JVM时会存在多个锁监视器

必须使用一个多个JVM都可以看到的监视器

第一版
utils/ILock
 package com.hmdp.utils;
 ​
 public interface ILock {
     /**
      * 尝试获取锁
      * @param timeoutSec 锁持有的超时时间,过期后自动释放
      * @return true表示获取锁成功,false表示获取失败
      */
     boolean tryLock(long timeoutSec);
 ​
     /**
      * 释放锁
      */
     void unlock();
 }
utils/SimpleRedisLock
public class SimpleRedisLock implements ILock {
 ​
     //一个业务一个锁名
     private String name;
     private StringRedisTemplate stringRedisTemplate;
     private static final String KEY_PREFIX = "lock:";
     public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
         this.name = name;
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     @Override
     public boolean tryLock(long timeoutSec) {
         //获取线程标识
         long threadId = Thread.currentThread().getId();
         //获取锁
         Boolean success = stringRedisTemplate.opsForValue()
                 .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
         return Boolean.TRUE.equals(success);
     }
 ​
     @Override
     public void unlock() {
         stringRedisTemplate.delete(KEY_PREFIX + name);
     }
 ​
 }
VoucherOrderServiceImpl
public Result secKillVoucher(Long voucherId) {
     //1.查询优惠券
     SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
     //2.判断秒杀是否开始
     if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
         //返回错误信息
         return Result.fail("秒杀尚未开始!");
     }
     //3.判断秒杀是否结束
     if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
         return Result.fail("秒杀已经结束!");
     }
     //4.判断库存是否充足
     if (voucher.getStock() < 1) {
         //库存不足
         return Result.fail("库存不足!");
     }    
     //userId.toString(),toString的底层实现new String,不能保证同一个id每次来都是同一个对象(即同一把锁),需要Intern方法
     Long userId = UserHolder.getUser().getId();
     //创建锁对象
     SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
     //获取锁
     boolean isLock = lock.tryLock(1200);
     //判断是否获取锁成功
     if(isLock) {
         // 获取锁失败,返回错误或重试
         return Result.fail("不允许重复下单!");        
     } 
     try {
         //获取代理对象(事务)
         IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
         return proxy.createVoucherOrder(voucherId);
     } finally {
         lock.unlock();
     }
 }

存在问题:业务阻塞,导致锁提前释放;线程1、2、3是同一个用户发出的请求

第二版(解决误删问题)

获取锁时存入线程标识,释放锁时判断锁标识是否是自己的。

utils/SimpleRedisLock
public class SimpleRedisLock implements ILock {

    //一个业务一个锁名
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //判断标识是否一致
        if(threadId.equals(id)) {
            //释放锁
        	stringRedisTemplate.delete(KEY_PREFIX + name);
        }        
    }
}
VoucherOrderServiceImpl
 
public Result secKillVoucher(Long voucherId) {
     //1.查询优惠券
     SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
     //2.判断秒杀是否开始
     if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
         //返回错误信息
         return Result.fail("秒杀尚未开始!");
     }
     //3.判断秒杀是否结束
     if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
         return Result.fail("秒杀已经结束!");
     }
     //4.判断库存是否充足
     if (voucher.getStock() < 1) {
         //库存不足
         return Result.fail("库存不足!");
     }    
     //userId.toString(),toString的底层实现new String,不能保证同一个id每次来都是同一个对象(即同一把锁),需要Intern方法
     Long userId = UserHolder.getUser().getId();
     //创建锁对象
     SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
     //获取锁
     boolean isLock = lock.tryLock(1200);
     //判断是否获取锁成功
     if(isLock) {
         // 获取锁失败,返回错误或重试
         return Result.fail("不允许重复下单!");        
     } 
     try {
         //获取代理对象(事务)
         IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
         return proxy.createVoucherOrder(voucherId);
     } finally {
         lock.unlock();
     }
 }

存在问题:在判断完锁标识之后发生阻塞,误删。必须确保判断锁标识和释放锁两个动作的原子性

第三版

unlock.lua
--获取锁中的线程标识 get key
--比较线程标识与锁中的标识是否一致
if(redis.call('get',  KEYS[1]) == ARGV[1]) then
    --释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

utils/SimpleRedisLock
//静态常量和静态代码块,类一加载就初始化
 private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
 static {
     UNLOCK_SCRIPT = new DefaultRedisScript<>();
     UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
     UNLOCK_SCRIPT.setResultType(Long.class)
 }
 public class SimpleRedisLock implements ILock {
 ​
     //一个业务一个锁名
     private String name;
     private StringRedisTemplate stringRedisTemplate;
     private static final String KEY_PREFIX = "lock:";
     private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
     public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
         this.name = name;
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     @Override
     public boolean tryLock(long timeoutSec) {
         //获取线程标识
         String threadId = ID_PREFIX + Thread.currentThread().getId();
         //获取锁
         Boolean success = stringRedisTemplate.opsForValue()
                 .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
         return Boolean.TRUE.equals(success);
     }
     @Override
     public void unlock() {
         //调用lua脚本
         stringRedisTemplate.execute(
                 UNLOCK_SCRIPT,
                 Collections.singletonList(KEY_PREFIX + name),
                 ID_PREFIX + Thread.currentThread().getId());
     }       
 }
Redisson

config/RedissonConfig
@Configuration
 public class RedissonConfig {
     @Bean
     public RedissonClient redissonClient() {
         //配置
         Config config = new Config();
         config.useSingleServer().setAddress("redis://10.8.11.69:6379").setPassword("123456");
         //创建RedissonClient对象
         return Redisson.create(config);
     }
 }
VoucherOrderServiceImpl
//注入RedissonClient对象
@Resource
private RedissonClient redissonClient;

public Result secKillVoucher(Long voucherId) {
    //1.查询优惠券
    SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        //返回错误信息
        return Result.fail("秒杀尚未开始!");
    }
    //3.判断秒杀是否结束
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束!");
    }
    //4.判断库存是否充足
    if (voucher.getStock() < 1) {
        //库存不足
        return Result.fail("库存不足!");
    }
    Long userId = UserHolder.getUser().getId();
    //创建锁对象
    //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
    RLock lock = redissonClient.getLock("lock:order:" + userId);
    //获取锁
    boolean isLock = lock.tryLock();
    if (!isLock) {
        //获取锁失败,返回错误或重试
        return Result.fail("不允许重复下单!");
    }
    try {
        //获取代理对象(事务)
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    }finally {
        lock.unlock();
    }
}

秒杀优化

效率低!

需求一:保存优惠券信息到Redis中

VoucherServiceImpl

 @Resource
 private StringRedisTemplate stringRedisTemplate;
 @Override
     @Transactional
     public void addSeckillVoucher(Voucher voucher) {
         // 保存优惠券
         save(voucher);
         // 保存秒杀信息到数据库
         SeckillVoucher seckillVoucher = new SeckillVoucher();
         seckillVoucher.setVoucherId(voucher.getId());
         seckillVoucher.setStock(voucher.getStock());
         seckillVoucher.setBeginTime(voucher.getBeginTime());
         seckillVoucher.setEndTime(voucher.getEndTime());
         seckillVoucherService.save(seckillVoucher);
         //保存秒杀库存到Redis
         stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
     }
需求二:一人一单秒杀资格判断(基于lua脚本)

seckill.lua

 --1.参数列表
 --1.1优惠券id
 local voucherId = ARGV[1]
 --1.2用户id
 local userId = ARGV[2]
 ​
 --2.数据key
 --2.1库存key
 local stockKey = "seckill:stock" .. voucherId
 --2.2订单key
 local orderKey = "seckill:order" .. voucherId
 ​
 --脚本业务
 --3.1判断库存是否充足
 if(tomember(redis.call('get', stockKey)) <= 0) then
    --库存不足,返回1
     return 1
 end
 --3.2判断用户是否下单 SISMEMBER orderKey userId
 if(redis.call('sismember', orderKey, userId) == 1) then
     --存在,说明是重复下单,返回2
     return 2
 end
 --3.3扣库存 incrby stockKey - 1
 reids.call('incryby', stockKey, -1)
 --3.4下单(保存用户)sadd orderKey userId
 redis.call('sadd', orderKey, userId)
 return 0

VoucherOrderServiceImpl

private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
 static {
     SECKILL_SCRIPT = new DefaultRedisScript<>();
     SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
     SECKILL_SCRIPT.setResultType(Long.class);
 }
 ​
 @Override
 public Result secKillVoucher(Long voucherId) {
     //获取用户
     Long userId = UserHolder.getUser().getId();
     //1.执行lua脚本
     Long result = stringRedisTemplate.execute(
         SECKILL_SCRIPT,
         Collections.emptyList(),
         voucherId.toString(), userId.toString()
     );
     //2.判断结果是否为0
     int r = result.intValue();
     if(r != 0){
         //2.1 不为0,没有购买资格
         return Result.fail(r == 1 ? "库存不足" : " 不能重复下单");
     }
     //2.2 为0,有购买资格,把下单信息保存到阻塞队列
     long orderId = redisIdWorker.nextId("order");
     //TODO 保存阻塞队列
     //3.返回订单id
     return Result.ok(orderId);
 }
需求三、四:
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
     static {
         SECKILL_SCRIPT = new DefaultRedisScript<>();
         SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
         SECKILL_SCRIPT.setResultType(Long.class);
     }
 //创建阻塞队列
 private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
 //创建线程池
 private static final ExecutorService SECKILL_ORDER_EXCUTOR = Executors.newSingleThreadExecutor();
 ​
 //注解@PostConstruct:在当前类初始化完毕后执行
 @PostConstruct
 private void init() {
     SECKILL_ORDER_EXCUTOR.submit(new VoucherOrderHandler());
 }
 //线程任务
 private class VoucherOrderHandler implements Runnable{
 @Override
     public void run() {
         while(true) {
             try {
                 //1.获取队列中的订单信息
                 VoucherOrder voucherOrder = orderTasks.take();
                 //2.创建订单
                 handleVoucherOrder(voucherOrder);
             } catch (Exception e) {
                 log.error("处理订单异常", e);
             }
         }
     }
 }
 ​
 private void handleVoucherOrder(VoucherOrder voucherOrder) {
     //1.获取用户
     Long userId = voucherOrder.getUserId();
     //2.创建锁对象
     RLock lock = redissonClient.getLock("lock:order:" + userId);
     //获取锁
     boolean isLock = lock.tryLock();
     if (!isLock) {
         //获取锁失败,返回错误或重试
         log.error("不允许重复下单");
         return;
     }
     try {
         proxy.createVoucherOrder(voucherOrder);
 ​
     }finally {
         lock.unlock();
     }
 }
 ​
 private IVoucherOrderService proxy;
 @Override
 public Result secKillVoucher(Long voucherId) {
     //获取用户
     Long userId = UserHolder.getUser().getId();
     //1.执行lua脚本
     Long result = stringRedisTemplate.execute(
             SECKILL_SCRIPT,
             Collections.emptyList(),
             voucherId.toString(), userId.toString()
     );
     //2.判断结果是否为0
     int r = result.intValue();
     if(r != 0){
         //2.1 不为0,没有购买资格
         return Result.fail(r == 1 ? "库存不足" : " 不能重复下单");
     }
 ​
     //2.2 为0,有购买资格,把下单信息保存到阻塞队列
     VoucherOrder voucherOrder = new VoucherOrder();
     //2.3订单id
     long orderId = redisIdWorker.nextId("order");
     voucherOrder.setId(orderId);
     //2.4用户id
     voucherOrder.setUserId(userId);
     //2.5代金券id
     voucherOrder.setVoucherId(voucherId);
     //2.6创建阻塞队列
     orderTasks.add(voucherOrder);
 ​
     //3获取代理对象
     proxy = (IVoucherOrderService) AopContext.currentProxy();
     //4.返回订单id
     return Result.ok(orderId);
 ​
 }
 ​
 @Transactional
 public void createVoucherOrder(VoucherOrder voucherOrder) {
     //5.一人一单
     Long userId = voucherOrder.getUserId();
     //5.1查询订单
     int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
     //5.2判断是否存在
     if (count > 0) {
         //用户已经购买过
         log.error("用户已经购买过一次!");
         return;
     }
 ​
     //6.扣减库存
     boolean success = iSeckillVoucherService.update()
         .setSql("stock = stock - 1") //set stock = stock - 1
         .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
         .update();
     if (!success) {
         log.error("库存不足!");
         return;
     }
     //7.创建订单
     save(voucherOrder);
 }
 ​

JVM内存限制,高并发情况下,大量订单出现可能会超出JVM的上限。

数据存储在内存中,出现宕机或者重启,阻塞队列中的数据都会丢失。

Redis消息队列

1、基于List结构模拟消息队列

2、基于PubSub(发布订阅)

3、基于Stream的消息队列

最后一点没看下去。。。。天呐这一部分好多,还没有消化。继续加油!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值