SpringBoot 整合 Redis

1. 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2. 配置

#redis单机配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=root

#redis高可用集群配置
#哨兵监控主redis节点名称,必选 与sentinei.conf的mymaster名称相同
spring.redis.sentinel.master=mymaster
#哨兵节点
spring.redis.sentinel.nodes=127.0.0.1:26380,127.0.0.1:26381,127.0.0.1:26382

#redis分布式配置
spring.redis.cluster.nodes=127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384,127.0.0.1:6385
#缓存策略
spring.cache.type=redis
spring.cache.cache-names=myRedis

3. 实体类必须要序列化

@Entity
@Table(name = "development")
@Data
public class Development implements Serializable{

    @Id
    private Integer id;

    private String devName;

    private String devDesc;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)		// 反序列化
    @JsonSerialize(using = LocalDateTimeSerializer.class)		// 序列化
    private LocalDateTime createTime;
}

4.启用缓存注解@EnableCaching

@SpringBootApplication
@MapperScan("com.example.demo17.mapper")
@EnableCaching
public class Demo17Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo17Application.class, args);
    }

}

5.使用缓存注解,value属性必须指定

    /**
     * 查询方法上要添加@Cacheable 注解 ,@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果
     */
    @Override
    @Cacheable(value = "pc", key = "'product_'+#name")
    public List<Product> findProductsByName(String name) {
        return dao.selectProductsByName(name);
    }
    @Override
    @Cacheable(value = "pc", key = "'product_all'")
    public List<Product> findAllProducts() {
        return dao.selectAllProducts();
    }

    
    /**
     * 对数据进行写操作的方法上添加@CacheEvict 注解 ,用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素 
     */
    @Override
    @CacheEvict(value = "pc", allEntries = true)   // 只要该方法被执行,立即清除pc缓存中的所有数据
    public void saveProduct(Product product) {
        dao.insertProduct(product);
    }

6. 手工操作 Redis

@Api(value = "秒杀接口",tags = "采用分段锁1分钟内秒杀1000个商品")
@RestController
public class SingleRedisForSecKillController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    //锁的前缀
    private static String LOCK_KEY_PREFIX;
    //锁段数量
    private static int SEGMENT_NUM ;
    //锁数组
//    private static ArrayList<RLock> locks;

    @ApiOperation(value = "redis采用分段锁-1分钟内秒杀1000个商品-初始化")
    @PostMapping("/redisson/init/seckill")
    public void initSecKillBySegmentationLock(@RequestBody RequestMessage<GoodsSecKillRequest> requestMessage) {
        /**
         * 使用redisson可重入锁+分段式锁;解决高并发问题:每秒上千订单的高并发场景下如何完成分布式锁的性能优化
         * 1、这种方式适用于一套代码在多个服务器部署,但redis只能有一个,不然可能会出现锁丢失问题
         * 思想:分段式锁
         */
        GoodsSecKillRequest inDto = requestMessage.getRoot();
        int goodsSum = inDto.getGoodsSum();
        String goods = inDto.getGoods();
        //锁数量
        SEGMENT_NUM = (int) Math.ceil(goodsSum/50.0);
        //锁前缀
        LOCK_KEY_PREFIX = goods+"lock";
        //初始化商品
        for (int i = 0; i < SEGMENT_NUM; i++) {
            if (i < SEGMENT_NUM-1){
                stringRedisTemplate.opsForValue().set(goods+i,"50");
            }else {
                stringRedisTemplate.opsForValue().set(goods+i,String.valueOf(goodsSum-50*i));
            }
        }
    }

    @ApiOperation(value = "redis采用分段锁-1分钟内秒杀1000个商品")
    @PostMapping("/redisson/seckill")
    public String secKillBySegmentationLock(@RequestBody RequestMessage<GoodsSecKillRequest> requestMessage) {
        /**
         * 使用redisson可重入锁+分段式锁;解决高并发问题:每秒上千订单的高并发场景下如何完成分布式锁的性能优化
         * 1、这种方式适用于一套代码在多个服务器部署,但redis只能有一个,不然可能会出现锁丢失问题
         * 思想:分段式锁
         */
        GoodsSecKillRequest inDto = requestMessage.getRoot();
        String userId = inDto.getUserId();
        String goods = inDto.getGoods();
        //对订单id取模运算(0-19)
        int segment = Math.abs(userId.hashCode() % SEGMENT_NUM);
        //获取对应锁
        RLock redissonLock = redissonClient.getLock(LOCK_KEY_PREFIX+segment);
        try {
            //tryLock() 方法的作用是尝试获取锁,如果锁已经被其他线程持有,则立即返回
            //boolean tryLock = redissonLock.tryLock();
            //waitTime:等待获取锁的最大时间。如果在此时间内无法获取到锁,则返回失败
            //leaseTime :成功获取锁后的持有时间。在此时间内,锁将保持有效状态,超过该时间后锁将自动释放
//            boolean tryLock = redissonLock.tryLock(600, 100, TimeUnit.SECONDS);
            boolean tryLock = redissonLock.tryLock();
            if(tryLock){
                //方式一:opsForValue()
                String stock = stringRedisTemplate.opsForValue().get(goods+segment);
                int amount = stock == null ? 0 : Integer.parseInt(stock);
                if (amount > 0) {
                    //方式一:opsForValue()
                    stringRedisTemplate.opsForValue().set(goods+segment, String.valueOf(--amount));
                    return "库存剩余" + amount + "台";
                }
                return "库存不足";
            }else {
                return "没有抢到锁";
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //解锁
            redissonLock.unlock();
        }
    }

}

📢:RedisTemplate的script操作

// Lua脚本示例,这里只是一个计数器,你可以替换为你自己的Lua脚本
String script = "local val = redis.call('get', KEYS[1])\n" +
        "if val then\n" +
        "   return redis.call('incr', KEYS[1])\n" +
        "else\n" +
        "   return redis.call('set', KEYS[1], ARGV[1])\n" +
        "end";

// 键
List<String> keys = Lists.newArrayList("mykey");
//参数
Long args = 1L;
// 创建一个Lua脚本对象,script:脚本 Long.class返回值类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script,Long.class);

// 使用RedisTemplate执行Lua脚本
Long execute = redisTemplate.execute(redisScript, keys, args);

📢:RedisTemplate的BitMap操作

//为给定 key(users) 的BitMap 数据的 offset(100066) 位置设置值为 1
redisTemplate.opsForValue().setBit("users",100066,true);
//对 key 所储存的 BitMap 字符串值,获取指定 offset 偏移量上的位值 bitValue
System.out.println(redisTemplate.opsForValue().getBit("users", 100101));
System.out.println(redisTemplate.opsForValue().getBit("users", 100102));
//统计给定字符串中被设置为 1 的 bit 位的数量
Long execute = redisTemplate.execute((RedisCallback<Long>) res -> res.bitCount("users".getBytes(StandardCharsets.UTF_8)));
//返回 key 指定的 BitMap 中第一个值为指定值 bit(非 0 即 1) 的二进制位的位置
Long offset = redisTemplate.execute((RedisCallback<Long>) res -> res.bitPos("users".getBytes(StandardCharsets.UTF_8), true));
System.out.println(execute);
System.out.println(offset);
输出:
true
true
5
100001

RedisTemplate的批量操作

//批量新增
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection stringRedisConnection = (StringRedisConnection) connection;
            redisCheckCacheDTOS.forEach(cacheKey -> stringRedisConnection.hSet(cacheKey.getKey(), cacheKey.getCacheKey(), cacheKey.getValue()));
            return null;
        });
//批量删除
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection stringRedisConnection = (StringRedisConnection) connection;
            redisCheckCacheDTOS.forEach(cacheKey -> stringRedisConnection.hDel(cacheKey.getKey(), cacheKey.getCacheKey()));
            return null;
        });

定制化redisTemplate(可选)

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.StandardCharsets;

@Configuration
public class RedisConfig {
    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        //创建模版
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用 FastJsonRedisSerializer 来序列化和反序列化redis 的 value的值
        FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setCharset(StandardCharsets.UTF_8);
        serializer.setFastJsonConfig(fastJsonConfig);

        // key 的 String 序列化采用 StringRedisSerializer
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        // value 的值序列化采用 fastJsonRedisSerializer
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

定制化缓存管理(可选)

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

    /*
     * 引入 redis 来实现缓存,此时我们便使用 RedisCacheManager 来管理缓存的生命周期、缓存的存储和检索等。
     * 我们在使用 RedisCacheManager 来操作 redis 时,底层操作默认使用的是 RedisTemplate,
     * 而 redisTemplate 是 redisAutoConfiguration 在项目启动时帮我们自动注册的组件,它默认使用的是 JDK 序列化机制。
     * 所以在 redis 存储时,会出现类似乱码的情况出现,所以我们需要来自己配置 redisCacheManager。
     * 注:尤其是当使用@Cacheable @CachePut @CacheEvict注解时redisCacheManager将其序列化成对应格式
     * */
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        //这一句必须要,作用是序列化时将对象全类名一起保存下来
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        genericJackson2JsonRedisSerializer.serialize(om);
        //配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                //设置 key 过期时间
                .entryTtl(Duration.ofSeconds(300))
                //设置key序列化规则
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                //设置value序列化规则
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

定制化 LettuceConnectionFactory(可选)

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
 
        RedisConfiguration redisConfiguration = new RedisStandaloneConfiguration(
                hostName, port
        );
        // 设置选用的数据库号码
        ((RedisStandaloneConfiguration) redisConfiguration).setDatabase(databaseId);
        // 设置 redis 数据库密码
        ((RedisStandaloneConfiguration) redisConfiguration).setPassword(password);
        // 连接池配置
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(maxWaitMillis);
        LettucePoolingClientConfiguration lettucePoolingClientConfiguration
                = LettucePoolingClientConfiguration.builder()
                .poolConfig(poolConfig)
                .commandTimeout(Duration.ofMillis(timeout))
                .build();
        // 根据配置和客户端配置创建连接
        LettuceConnectionFactory factory = new LettuceConnectionFactory(redisConfiguration, lettucePoolingClientConfiguration);
        return factory;
    }

附录:四种缓存注解

(1) @Cacheable

@Cacheable可以标记在一个方法上,也可以标记在一个类上。

  • 当标记在一个方法上时表示该方法是支持缓存的

  • 当标记在一个类上时则表示该类所有的方法都是支持缓存的

作用:对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。 注意:当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的

用法:@Cacheable可以指定三个属性,value、key和condition

value属性指定Cache名称:value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的(即缓存在那个分组中,若redis中不存在对应的分组就创建出该分组并将缓存数据保存其中),对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。

//Cache是发生在cache1上的
@Cacheable(value = "cache1")
public User find(Integer id) {
    return null;
}

//Cache是发生在cache1和cache2上的
@Cacheable(value = {"cache1", "cache2"})
public User find(Integer id) {
    return null;
}

使用key属性自定义key:key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。

//Cache是发生在cache1上的
@Cacheable(value = "cache1")
public User find(Integer id) {
    return null;
}

//Cache是发生在cache1和cache2上的
@Cacheable(value = {"cache1", "cache2"})
public User find(Integer id) {
    return null;
}

condition属性指定发生的条件:有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;

如下示例表示只有当user的id为偶数时才会进行缓存。

@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
    System.out.println("find user by user " + user);
    return user;
}

(2) @CacheEvict

@CacheEvict可以标记在一个方法上,也可以标记在一个类上。

  • 当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作

作用:用于清除缓存

用法:CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation

allEntries属性:allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

 @CacheEvict(value="users", allEntries=true)
   public void delete(Integer id) {
      System.out.println("delete user by id: " + id);
   }

beforeInvocation属性:清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value="users", beforeInvocation=true)
   public void delete(Integer id) {
     System.out.println("delete user by id: " + id);
   }

(3) @CachePut

  • @CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中(更新)。

  • @CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。

//每次都会执行方法,并将结果存入指定的缓存中
	@CachePut("users")
	public User find(Integer id) {
	   return null;
	}

(4) @Caching

@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),
         @CacheEvict(value = "cache3", allEntries = true) })
   public User find(Integer id) {
      return null;
   }
天猫商城是一个基于SSM框架的综合性B2C电商平台,需求设计主要参考天猫商城的购物流程:用户从注册开始,到完成登录,浏览商品,加入购物车,进行下单,确认收货,评价等一系列操作。 作为模拟天猫商城系统的核心组成部分之一,采用SSM框架的天猫数据管理后台包含商品管理,订单管理,类别管理,用户管理和交易额统计等模块,实现了对整个商城的一站式管理和维护。本课程是一门专业的Java微服架构开发实战课程,主要讲解了当下流行的SpringBoot框架、SpringCloud架构以及与第三方技术整合开发实战内容。通过本课程的学习,能够理解并掌握SpringBoot的基础知识,同时能够掌握SpringBoot与常用的第三方技术整合实现实际开发中的业务需求,包括实现Web开发、数据访问、缓存管理、安全管理、消息服务、任务管理等;了解并掌握SpringCloud微服务架构的基础知识及相关组件的应用,掌握微服务架构在企业级开发的实践,建立起微服架构思想。项目技术栈:采用SpringBoot简化商城系统的初始搭建以及开发过程采用SpringMVC+Spring+IBatis完成项目的整合采用Mysql作为数据库存储,Druid配置数据库连接池采用SpringCloud+Netflix 微服务技术栈的实战开发使用Redis完成缓存的数据存储,搭建Redis搭建主从、哨兵、集群应用,保证Redis的高可用使用ElasticSearch全文检索系统进行商品数据搜索,使用ElasticSearch搭建搜索服务的高可用使用Ngnix实现页面动静分离与负载均衡的配置采用FastDFS文件储存系统文件存储,完成广告图片、商品图片的上传和存储系统使用采用CAS+shiro单点登录系统实现用户认证使用ECharts根据后台查询数据生成图表使用POI实现了商城盈利状况的Excel表格导出。商品的详情页使用Thymeleaf完成页面静态化,减少页面数据展示延迟项目中使用SpringBoot下的Aop + 自定义注解完成用户行为记录,日志采集后台管理系统使用Shiro实现登录验证和权限管理(超级管理员、管理员、产品编辑员)项目整合微信完成订单的支付使用Redission完成分布式锁,生成订单的编号使用SpringCloud Alibaba Seat完成下订单模块的分布式事务(新增订单表,库存减少,库存超卖设计)使用RabbitMQ 做消息队列,完成订单未支付自动取消和模块直接的解耦合使用Quartz任务调度,完成缓存的定时刷新,保证缓存的一致性使用本地消息表机制完成消息然队列RabbitMQ消息可靠性传输订单支付模块使用微信扫码支付,并设置订单超时自动取消通过Jquery实现前端校验,通过基于Hibernate的Valida注解实现后端的校验功能使用Base64编码对Json数据传输进行编码和解码项目使用RESTful设计风格实现资源的访问,实现前后端分离项目使用聚合数据第三方短信平台完成用户的登陆功能项目使用SpringBoot整合JavaMail完成邮件的发送项目使用SpringBoot整合Swagger2生成接口文档使用PostMan完成接口的测试项目的测试:SpringTest、dbunit、EasyMock使用Docker 进行应用的自动化打包和发布、自动化测试和持续集成、部署和调整其他应用使用 PowerDesigner,完成数据库的建模项目使用禅道进行BUG管理环境采用Maven实施多模块项目构建,采用Git进行项目版本管理 架构解读:  项目部分截图:              讲义部分截图:          
该项目是采用目前比较流行的SpringBoot/SpringCloud构建微服务电商项目,项目叫 《果然新鲜》,实现一套串联的微服务电商项目。完全符合一线城市微服务电商的需求,对学习微服务电商架构,有非常大的帮助,该项目涵盖从微服务电商需求讨论、数据库设计、技术选型、互联网安全架构、整合SpringCloud各自组件、分布式基础设施等实现一套完整的微服务解决方案。 项目使用分布式微服务框架,涉及后台管理员服务、地址服务、物流服务、广告服务、商品服务、商品类别服务、品牌服务、订单服务 、购物车服务、首页频道服务、公告服务、留言服务、搜索服务、会员服务等。  系统架构图   SpringBoot+SpringCloud+SSM构建微服务电商项目使用SpringCloud Eureka作为注册中心,实现服务治理使用Zuul网关框架管理服务请求入口使用Ribbon实现本地负载均衡器和Feign HTTP客户端调用工具使用Hystrix服务保护框架(服务降级、隔离、熔断、限流)使用消息总线Stream RabbitMQ和 Kafka微服务API接口安全控制和单点登录系统CAS+JWT+OAuth2.0分布式基础设施构建分布式任务调度平台XXL-JOB分布式日志采集系统ELK分布式事务解决方案LCN分布式锁解决方案Zookeeper、Redis分布式配置中心(携程Apollo)高并发分布式全局ID生成(雪花算法)分布式Session框架Spring-Session分布式服务追踪与调用链Zipkin项目运营与部署环境分布式设施环境,统一采用Docker安装使用jenkins+docker+k8s实现自动部署微服务API管理ApiSwagger使用GitLab代码管理(GitHub  GitEE)统一采用第三方云数据库使用七牛云服务器对静态资源实现加速 开发环境要求JDK统一要求:JDK1.8Maven统一管理依赖 统一采用Docker环境部署编码统一采用UTF-8开发工具IDEA 或者 Eclipse 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值