SpringBoot整合Redis实现缓存操作实践

目录

SpringBoot使用Redis的核心逻辑

SpringBoot的Redis配置类

序列化与反序列化

JDK Serialization (Java 默认序列化)

String Serialization (字符串序列化)

JSON Serialization (JSON 序列化)

封装Reids操作接口

定义通用接口

接口实现

模板对象线程安全问题

自定义序列化器线程安全

自定义键值转换器线程安全

关联@Cacheable注解使用Redis

@Cacheable基础使用

@Cacheable

@CachePut

@CacheEvict

@Caching

@Cache适配Redis作为缓存存储器


SpringBoot使用Redis的核心逻辑

关于Redis相关的组件主要由Reids连接池(RedisConnectionFactory),对象模板(RedisTemplate)、缓存管理器(RedisManager),根据不同的缓存需求场景构造对象模板、缓存管理器对象时指定不同的序列化器,并将其注册到连接池中。

其中可以根据不同项目、场景对于序列化、CacheManger的需求,定义自定义的序列化器和缓存管理器进行扩展。

SpringBoot的Redis配置类

引入Redis的依赖:

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

进行yml文件相关配置

spring:
  redis:
    host: 127.0.0.1  # Redis 服务器的主机地址
    port: 6379       # Redis 服务器的端口号
    password: XXX    # Redis 服务器的密码(如果存在)
    database: 0      # 使用的数据库索引,默认为 0
    timeout: 5000    # 连接超时时间(毫秒),默认为 2000 毫秒
    lettuce:
      pool:
        max-active: 8  # 连接池最大连接数(使用负值表示没有限制)
        max-wait: -1   # 连接池最大阻塞等待时间(负值表示不限制)
        max-idle: ¾    # 连接池中的最大空闲连接
        min-idle: 0    # 连接池中的最小空闲连接

进行configuration类编写,进行对象模板的构造,Spring Boot 提供了原生的Start来适配Redistribution,原则上不需要额外构建RedisConnectionFactory 连接池的Bean, RedisTemplate 和 StringRedisTemplate 两种对象模板是Spring框架自带的操作 Redis的对象模板。原则上是不需要进行额外的操作,这里主要是对对象模板的序列化方式进行指定。

部分SpringBoot版本直接引用spring-boot-starter-data-redis可能不会自动化配置生成RedisConnectionFactory ,此种情况需要进行手动创建连接池的bean

Redis连接池Bean配置

@Configuration
public class RedisConfigure {
    
    @Autowired
    RedisProperty reidis;

    @Bean
    public RedisConnectionFactory getRedisConnectionFactory(){
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(reidis.getHost()); //通过实体获取yml配置的内容
        configuration.setPort(redis.getPort());
        configuration.setDatabase(redis.getDataBase());
        //如果还有其他参数同样注册上 
        return new LettuceConnectionFactory(configuration);
    }
}

对象模板注册:

@Configuration
public class RedisConfig {

   @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 设置键的序列化器
        template.setKeySerializer(new StringRedisSerializer());

        // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
        Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
        //针对对象类型,使用默认的对象序列化
        ObjectMapper om = new ObjectMapper();
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jacksonSeial.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        // 设置哈希键和值的序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 启用事务支持
        template.setEnableTransactionSupport(true);

        // 设置默认序列化器
        template.setDefaultSerializer(jackson2JsonRedisSerializer);

        // 设置错误处理器
        template.setErrorHandler(new DefaultErrorHandler());

        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(connectionFactory);
        template.afterPropertiesSet();
        return template;
    }
}
}

// 自定义错误处理器
public class DefaultErrorHandler implements ErrorHandler {
    @Override
    public void handleError(Throwable t) {
        // 自定义错误处理逻辑
        System.err.println("Redis error occurred: " + t.getMessage());
    }
}

针对 RedisTemplate的配置,主要是是序列化,包括键/值序列化、默认序列化、针对Hash类型的数据的序列化等

序列化与反序列化

Redis 支持多种序列化和反序列化机制,序列化主要是将复杂的数据结构转换为可以在 Redis 中存储的字节流,以及将这些字节流转回原始的数据结构。根据业务、项目中的数据结构特点、Redis的使用,要选择适合的序列化机制,具体的序列化机制有以下几种:

JDK Serialization (Java 默认序列化)

优点: JDK 序列化是一种通用的序列化方法,它可以序列化任何实现了 Serializable 接口的对象。
缺点: 序列化后的数据较大,效率较低,且不兼容非 Java 环境。
示例:

import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
String Serialization (字符串序列化)

优点: 简单直观,适用于简单的字符串数据。
缺点: 不适合复杂对象。如果需要缓存Java中的对象,则不建议使用该序列化方法
示例:

import org.springframework.data.redis.serializer.StringRedisSerializer;
StringRedisSerializer stringSerializer = new StringRedisSerializer();
JSON Serialization (JSON 序列化)

优点: 产生的数据较小,易于阅读和调试,跨语言支持良好。
缺点: 序列化和反序列化的性能可能不如原生二进制格式。
示例:
 

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;

ObjectMapper objectMapper = new ObjectMapper();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);

封装Reids操作接口

定义通用接口

创建基本的Redis服务类接口,便于使用:

/**
 * Redis 缓存数据操作
 */
public interface RedisCacheServer {
    /**
     * 缓存 String 内容
     * @param key 键
     * @param value 值
     */
    void setString(final String key, final String value);

    /**
     * 删除 String 缓存
     * @param key 键
     */
    void removeString(final String key);

    /**
     * 读取 String 缓存
     * @param key 键
     * @return 值
     */
    String getString(final String key);

    /**
     * 读取 String 缓存,如果键不存在则返回缺省值
     * @param key 键
     * @param defaultStr 缺省值
     * @return 值或缺省值
     */
    String getString(final String key, final String defaultStr);


    /**
     * 缓存哈希表
     * @param key 键
     * @param map 哈希表
     */
    <T> void setHash(final String key, final Map<String, T> map);

    /**
     * 删除哈希表缓存
     * @param key 键
     */
    void removeHash(final String key);

    /**
     * 读取哈希表
     * @param key 键
     * @return 哈希表
     */
    <T> Map<String, T> getHash(final String key);

    /**
     * 读取哈希表,如果键不存在则返回缺省值
     * @param key 键
     * @param defaultMap 缺省哈希表
     * @return 哈希表或缺省值
     */
    <T> Map<String, T> getHash(final String key, final Map<String, T> defaultMap);


    /**
     * 向列表中添加元素
     * @param key 键
     * @param values 值
     */
    <T> void addList(final String key, final List<T> values);

    /**
     * 删除列表缓存
     * @param key 键
     */
    void removeList(final String key);


    /**
     * 获取全部数据
     * @param key key
     * @return List
     * @param <T>
     */
    <T> List<T> getList(final String key);


    /**
     * 从列表中读取指定范围的元素
     * @param key 键
     * @param start 开始索引
     * @param end 结束索引
     * @return 元素列表
     */
    <T> List<T> getList(final String key, final long start, final long end);


    /**
     * 向集合中添加成员
     * @param key 键
     * @param members 成员
     */
    <T> void addSet(final String key, final Set<T> members);

    /**
     * 删除集合缓存
     * @param key 键
     */
    void removeSet(final String key);

    /**
     * 读取集合中的所有成员
     * @param key 键
     * @return 成员集合
     */
    <T> Set<T> getSet(final String key);


    /**
     * 向有序集合中添加成员
     * @param key 键
     * @param scoreMembers 分数-成员映射
     */
    <T> void addZSet(final String key, final Map<T, Double> scoreMembers);

    /**
     * 删除有序集合缓存
     * @param key 键
     */
    void removeZSet(final String key);

    /**
     * 从有序集合中读取指定范围的成员
     * @param key 键
     * @param start 开始索引
     * @param end 结束索引
     * @return 成员集合
     */
    <T> Set<T> getZSet(final String key, final long start, final long end);

    /**
     * 获取有序集合中成员的分数
     * @param key 键
     * @param member 成员
     * @return 分数
     */
    <T> Double getZSetScore(final String key, final T member);


}

接口实现

对应实现类

@Component
public class RedisCacheServerImpl implements RedisCacheServer {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 字符串操作
    @Override
    public void setString(final String key, final String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    @Override
    public void removeString(final String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    public String getString(final String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    @Override
    public String getString(final String key, final String defaultStr) {
        String result = stringRedisTemplate.opsForValue().get(key);
        return result != null ? result : defaultStr;
    }

    // 哈希表操作
    @Override
    public <T> void setHash(final String key, final Map<String, T> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    @Override
    public void removeHash(final String key) {
        redisTemplate.delete(key);
    }

    @Override
    public <T> Map<String, T> getHash(final String key) {
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        if (entries == null || entries.isEmpty()) {
            return Collections.emptyMap();
        }
        return entries.entrySet().stream()
                .collect(Collectors.toMap(
                        e -> (String) e.getKey(),
                        e -> (T) e.getValue()
                ));
    }

    @Override
    public <T> Map<String, T> getHash(final String key, final Map<String, T> defaultMap) {
        Map<String, T> result = getHash(key);
        return result != null && !result.isEmpty() ? result : defaultMap;
    }

    // 列表操作
    @Override
    public <T> void addList(final String key, final List<T> values) {
        redisTemplate.opsForList().rightPushAll(key, values);
    }

    @Override
    public void removeList(final String key) {
        redisTemplate.delete(key);
    }

    @Override
    public <T> List<T> getList(final String key) {
        Long size = redisTemplate.opsForList().size(key);
        if (size == null || size == 0) {
            return null;
        }
        return (List<T>) redisTemplate.opsForList().range(key, 0, size - 1);
    }

    @Override
    public <T> List<T> getList(final String key, final long start, final long end) {
        return (List<T>) redisTemplate.opsForList().range(key, start, end);
    }

    // 集合操作
    @Override
    public <T> void addSet(final String key, final Set<T> members) {
        redisTemplate.opsForSet().add(key, members.toArray(new Object[0]));
    }

    @Override
    public void removeSet(final String key) {
        redisTemplate.delete(key);
    }

    @Override
    public <T> Set<T> getSet(final String key) {
        return (Set<T>) redisTemplate.opsForSet().members(key);
    }

    // 有序集合操作
    @Override
    public <T> void addZSet(final String key, final Map<T, Double> scoreMembers) {
        for (Map.Entry<T, Double> entry : scoreMembers.entrySet()) {
            redisTemplate.opsForZSet().add(key, entry.getKey(), entry.getValue());
        }
    }

    @Override
    public void removeZSet(final String key) {
        redisTemplate.delete(key);
    }

    @Override
    public <T> Set<T> getZSet(final String key, final long start, final long end) {
        return (Set<T>) redisTemplate.opsForZSet().rangeByScore(key, start, end);
    }

    @Override
    public <T> Double getZSetScore(final String key, final T member) {
        return redisTemplate.opsForZSet().score(key, member);
    }
}

模板对象线程安全问题

总的来说,Redis 服务器是单线程的,因此它是线程安全的。但是在客户端应用程序中,特别是多线程环境下,需要确保正确地管理和使用 RedisTemplate 和相关资源的线程问题。

RedisTemplate StringRedisTemplate 是 Spring Data Redis 提供的线程安全的模板类。它们内部使用连接池来管理 Redis 连接,连接池本身也是线程安全的。

只需要确保在配置 RedisTemplate 和 StringRedisTemplate 时没有引入任何非线程安全的组件。 也就是需要保证模板对象时所使用的序列化器键和值的转换器以及其他相关组件必须是线程安全的

自定义序列化器线程安全

常见的序列化器包括 JdkSerializationRedisSerializer, StringRedisSerializer, GenericJackson2JsonRedisSerializer 等,这些序列化器通常是线程安全的。需要注意的是自定义的序列化器,需要确保它是线程安全的。

public class CustomSerializer implements RedisSerializer<MyObject> {
    @Override
    public byte[] serialize(MyObject t) throws SerializationException {
        // 确保这里的实现是线程安全的
        // 例如,不要在这里使用静态变量或共享状态
        // 可以使用线程局部变量(ThreadLocal)来存储临时状态
        return ...;
    }

    @Override
    public MyObject deserialize(byte[] bytes) throws SerializationException {
        // 确保这里的实现是线程安全的
        return ...;
    }
}
自定义键值转换器线程安全

RedisTemplate 和 StringRedisTemplate 也可以使用键和值的转换器。这些转换器也应该设计成线程安全的。 

public class CustomKeyConverter implements Converter<String, String> {
    @Override
    public String convert(String source) {
        // 确保这里的实现是线程安全的
        // 例如,不要在这里使用静态变量或共享状态
        return "prefix:" + source;
    }
}

关联@Cacheable注解使用Redis

接下来基于SpringBoot的@Cache相关注解,适配到Redis作为主缓存,基于注解进行数据操作。

@Cacheable基础使用

@Cache 注解家族是 Spring Framework 提供的一种声明式缓存机制,可以非常方便地在方法级别启用缓存。使用该家族注解必须在启动类或者配置类中通过@EnableCaching开启缓存使用:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

关于@Cache家族主要有以下几个常用注解及其使用方法:

@Cacheable

作用:@Cacheable 注解用于标记一个方法,表示该方法的结果是可以被缓存的。当方法被调用时,如果缓存中存在结果,则直接返回缓存中的结果,否则执行方法并将结果存入缓存。
常用参数:

  • value 或 cacheNames:指定缓存的名称。
  • key:指定缓存的键。可以使用 SpEL 表达式。
  • unless:条件表达式,只有当该表达式为 false 时才缓存结果。
  • condition:条件表达式,只有当该表达式为 true 时才缓存结果。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    //缓存结果不为null的数据,key为参数的id
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        // ……读取数据库,组装数据
        return new User(id, "John Doe");
    }
}
@CachePut

作用:@CachePut 注解用于标记一个方法,表示该方法的结果应该总是被放入缓存中,无论缓存中是否已经存在该结果。该方法总是会被执行。
常用参数:

  • value 或 cacheNames:指定缓存的名称。
  • key:指定缓存的键。可以使用 SpEL 表达式。
  • unless:条件表达式,只有当该表达式为 false 时才更新缓存。
  • condition:条件表达式,只有当该表达式为 true 时才更新缓存。
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @CachePut(value = "users", key = "#user.id", unless = "#result == null")
    public User updateUser(User user) {
        // 模拟更新用户信息
        return user;
    }
}
@CacheEvict

作用:@CacheEvict 注解用于标记一个方法,表示该方法在执行完毕后应该清除缓存中的某些条目。
常用参数:

  • value 或 cacheNames:指定缓存的名称。
  • key:指定缓存的键。可以使用 SpEL 表达式。
  • allEntries:布尔值,如果为 true,则清空整个缓存。
  • beforeInvocation:布尔值,默认为 false。如果为 true,则在方法调用之前清除缓存。
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        // 模拟删除用户
    }

    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUsers() {
        // 清空所有用户的缓存
    }
}
@Caching

作用:@Caching 注解用于组合多个 @Cacheable、@CachePut 和 @CacheEvict 注解,可以在一个方法上同时应用多个缓存操作。
常用参数:

  • cacheable:数组,包含多个 @Cacheable 注解。
  • put:数组,包含多个 @CachePut 注解。
  • evict:数组,包含多个 @CacheEvict 注解。
import org.springframework.cache.annotation.Caching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Caching(
        put = {
            @CachePut(value = "users", key = "#user.id", unless = "#result == null")
        },
        evict = {
            @CacheEvict(value = "oldUsers", allEntries = true)
        }
    )
    public User updateUserAndClearOldUsers(User user) {
        // 更新用户信息并清空 oldUsers 缓存
        return user;
    }
}

@Cache适配Redis作为缓存存储器

相关Spring的缓存注解适配器的原理可以参考设计模式:从Spring的缓存实现来理解适配器模式-优快云博客

使用@Cache家族注解以Redis进行数据交互,主要需要向Redis的连接池注册一个RedisManager组件。

以下我们自定义一个支持指定缓存时长的缓存管理器

public class CustomRedisCacheManager extends RedisCacheManager {

    private final ReentrantLock lock = new ReentrantLock();

    public CustomRedisCacheManager(RedisCacheWriter connectionFactory, RedisCacheConfiguration defaultCacheConfig) {
        super(connectionFactory, defaultCacheConfig);
    }


    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        //创建一个可以在@Cache中通过#符指定缓存时长的对象
        String[] arrays = StringUtils.delimitedListToStringArray(name,"#");
        name = arrays[0];
        //如果存在#则代表用户使用了缓存时长,此数据超时就要清除
        if(arrays.length > 1){
            long timeOut = Long.parseLong(arrays[1]);
            cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(timeOut));
        }
        return super.createRedisCache(name, cacheConfig);
    }

    public void clearCache() {
        try {
            lock.lock();  // 确保线程安全
            for (String cacheName : getCacheNames()) {
                getCache(cacheName).clear();
            }
        } finally {
            lock.unlock();
        }
    }


}

在之前的Redis配置类中多注入一个CacheManger的Bean:

@Configuration
public class RedisConfig {

   @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 设置键的序列化器
        template.setKeySerializer(new StringRedisSerializer());

       // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
        Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
        //针对对象类型,使用默认的对象序列化
        ObjectMapper om = new ObjectMapper();
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jacksonSeial.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        // 设置哈希键和值的序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 启用事务支持
        template.setEnableTransactionSupport(true);

        // 设置默认序列化器
        template.setDefaultSerializer(jackson2JsonRedisSerializer);

        // 设置错误处理器
        template.setErrorHandler(new DefaultErrorHandler());

        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(connectionFactory);
        template.afterPropertiesSet();
        return template;
    }

     @Bean
    public CacheManager getCacheManager(RedisConnectionFactory factory){
        // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
        Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
        //针对对象类型,使用默认的对象序列化
        ObjectMapper om = new ObjectMapper();
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jacksonSeial.setObjectMapper(om);

        // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
        StringRedisSerializer stringSerial = new StringRedisSerializer();


        RedisCacheConfiguration defaultConfigure = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1))// 默认通过Cache注解的缓存数据过期时间为1天
                .computePrefixWith(cacheName -> "web:test:"+cacheName) //指定通过Cache注解缓存的数据存放的层级
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerial))//指定序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial));
        CustomRedisCacheManager redisCacheManager = new CustomRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory),defaultConfigure);
        return redisCacheManager;

    }



}
}

至此,则可以通过@Cache家族注解向Redis中进行数据交互了:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    //缓存结果不为null的数据,key为参数的id, 数据会缓存 10 S,10S后自动消除
    @Cacheable(value = "users#600", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        // ……读取数据库,组装数据
        return new User(id, "John Doe");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

糖拌西红柿多放醋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值