Redis 主从复制、读写分离、高可用(七)-part 3

15 篇文章 0 订阅

这里我用docker实现:


结构:

[root@14ba6e5d00c2 redis]# ls
redis-3.2.8  redis-6379  redis-6380  redis-6381

配置文件和前面改动一样,config 要注意的是:

#bind 127.0.0.1
bind 0.0.0.0

6379的配置 改动的地方 

bind 0.0.0.0

port 6379

daemonize yes

pidfile "/sys/fs/cgroup/redis_6379.pid"

logfile "/sys/fs/cgroup/redis_6379.log"


dbfilename "dump6379.rdb"


dir "/sys/fs/cgroup"

appendonly no

appendfilename "appendonly.aof"





sentinel.conf

sentinel monitor mymaster 127.0.0.1 6379 1

准备工作完成~~~~

ps:启动各个redis,和sentinel,记得把 主从关系搭建好!



java:代码 贴一些关键的(读写分离,自动读取或写入 最新的redis,和发现最新的从redis):

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import redis.clients.jedis.JedisPoolConfig;
import cn.com.qiqi.springMvcTest.core.JedisSentinelPoolExt;
import cn.com.qiqi.springMvcTest.core.RedisDataSource;
import cn.com.qiqi.springMvcTest.core.RedisDataSourceImpl;

@Configuration  
@PropertySource(value="classpath:/redis.properties")
public class RedisConfig {
	@Value("${redis.masterName}")
	private String masterName;
	@Value("${redis.sentinals}")
	private String sentinal_hostAndPorts;
	
	@Bean(name="redisDataSource")
	public RedisDataSource redisDataSource(){
		JedisPoolConfig poolConfig=new JedisPoolConfig();
		poolConfig.setMaxTotal(1000);
		poolConfig.setMaxIdle(32);
		poolConfig.setTestOnBorrow(true);
		poolConfig.setMaxWaitMillis(100*1000);
		System.out.println(masterName+":"+sentinal_hostAndPorts);
		JedisSentinelPoolExt jedisSentinelPoolExt=new JedisSentinelPoolExt(masterName, sentinal_hostAndPorts, poolConfig, 100, 200, 10, true);
		 RedisDataSourceImpl dataSourceImpl  =new RedisDataSourceImpl();
		 dataSourceImpl.setJedisSentinelPool(jedisSentinelPoolExt);
		return dataSourceImpl;
	}
	
	
}


properties:

#redis 
#masterName
redis.masterName=mymaster
#sentinal-HostAndPort
redis.sentinals=192.168.1.107:26379


核心代码(利用前面学过的):

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.util.Pool;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 高级哨兵池
 */
public class JedisSentinelPoolExt extends Pool<Jedis> {

    protected GenericObjectPoolConfig poolConfig;
    protected int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
    protected int soTimeout = Protocol.DEFAULT_TIMEOUT;
    protected String password;
    protected int database = Protocol.DEFAULT_DATABASE;
    protected String clientName;

    protected Set<MasterListener> masterListeners = new HashSet<MasterListener>();
    protected Set<SlaveDownListener> slaveDownListeners = new HashSet<SlaveDownListener>();
    protected Set<SlaveUpListener> slaveUpListeners = new HashSet<SlaveUpListener>();

    protected List<HostAndPort> readHaps = new ArrayList<HostAndPort>();
    protected List<JedisPoolExt> jedisReadPools = new CopyOnWriteArrayList<JedisPoolExt>();
    protected Set<String> _sentinels = new LinkedHashSet<String>();
    protected Map<HostAndPort, Integer> failMap = new ConcurrentHashMap<HostAndPort, Integer>();
    private String masterName;
    private int failMax;
    protected boolean masterWriteOnly;
    protected long subscribeRetryWaitTimeMillis = 5000;

    protected Logger log = Logger.getLogger(getClass().getName());

    private volatile JedisFactory factory;
    private volatile HostAndPort currentHostMaster;

    public JedisSentinelPoolExt(String masterName, String sentinels, final GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, int failMax, boolean masterWriteOnly) {
        this(masterName, sentinels, poolConfig, null, connectionTimeout, soTimeout, failMax, masterWriteOnly, null);
    }

    public JedisSentinelPoolExt(String masterName, String sentinels, final GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, int failMax, boolean masterWriteOnly, String clientName) {
        this(masterName, sentinels, poolConfig, null, connectionTimeout, soTimeout, failMax, masterWriteOnly, clientName);
    }

    public JedisSentinelPoolExt(String masterName, String sentinels, final GenericObjectPoolConfig poolConfig, final String password, int connectionTimeout, int soTimeout, int failMax, boolean masterWriteOnly) {
        this(masterName, sentinels, poolConfig, password, connectionTimeout, soTimeout, failMax, masterWriteOnly, null);
    }

    public JedisSentinelPoolExt(String masterName, String sentinels, final GenericObjectPoolConfig poolConfig, final String password, int connectionTimeout, int soTimeout, int failMax, boolean masterWriteOnly, String clientName) {

        String[] strings = sentinels.split(",");
        Collections.addAll(_sentinels, strings);

        this.poolConfig = poolConfig;
        this.connectionTimeout = connectionTimeout;
        this.soTimeout = soTimeout;
        if (!StringUtils.isBlank(password)) {
            this.password = password;
        }
        this.database = Protocol.DEFAULT_DATABASE;
        this.failMax = failMax;
        this.masterName = masterName;
        this.masterWriteOnly = masterWriteOnly;
        if (!StringUtils.isBlank(clientName)) {
            this.clientName = clientName;
        }

        HostAndPort master = initSentinels(_sentinels, masterName);
        initPool(master);

        initReadPool();
    }

    private void initReadPool() {
        updateReadPools(readHaps);
    }

    /**
     * 获取写实例
     * @return
     */
    public WriteJedis getWriteResource() {
        return new WriteJedis(getResource());
    }

    /**
     * 获取读实例
     * @return
     */
    public ReadJedis getReadResource() {
        if (jedisReadPools.size() == 0) {
            throw new RedisException("there is no jedis for read");
        }

        // 从redis读池中随机获取一个实例
        Random rand = new Random();
        int randNum = rand.nextInt(jedisReadPools.size());
        JedisPoolExt jedisPoolExt = jedisReadPools.get(randNum);
        HostAndPort hostAndPort = jedisPoolExt.getHostAndPort();
        JedisPool jedisPool = jedisPoolExt.getJedisPool();
        ReadJedis readJedis;
        try {
            Jedis jedis = jedisPool.getResource();
            readJedis = new ReadJedis(jedis, jedisPool);
        } catch (Throwable e) {
            // 添加失败列表
            addFailMap(hostAndPort, jedisPool);

            // 重试其它连接池
            if (jedisReadPools.size() > 1) {
                while (true) {
                    // 随机获取另一个读节点,仍然连接失败就抛异常
                    int randNum1 = rand.nextInt(jedisReadPools.size());
                    if (randNum1 != randNum) {
                        JedisPoolExt jedisPoolExt1 = jedisReadPools.get(randNum1);
                        JedisPool jedisPool1 = jedisPoolExt1.getJedisPool();
                        try {
                            Jedis jedis = jedisPool1.getResource();
                            readJedis = new ReadJedis(jedis, jedisPool1);
                        } catch (Exception e1) {
                            addFailMap(hostAndPort, jedisPool);
                            throw new RedisException("JedisSentinelPoolExt getReadResource retry error", e1);
                        }
                        return readJedis;
                    }
                }
            } else {
                throw new RedisException(e);
            }
        }
        return readJedis;
    }

    /**
     * 连接失败的实例,放入失败记录列表,超过指定失败次数的实例,会从读节点列表中丢弃
     * @param hostAndPort
     * @param jedisPool
     */
    private void addFailMap(HostAndPort hostAndPort, JedisPool jedisPool) {
        if (failMax != 0 && jedisReadPools.size() > 1) {
            Integer failTimes = failMap.get(hostAndPort);
            if (failTimes == null) {
                failTimes = 1;
            } else {
                failTimes++;
            }

            if (failTimes >= failMax) {
                failMap.remove(hostAndPort);
                jedisPool.close();
                removeFromReadPool(hostAndPort);
            } else {
                failMap.put(hostAndPort, failTimes);
            }
        }
    }

    public void resetReadPool() {
        initSentinels(_sentinels, masterName);
        initReadPool();
        failMap.clear();
    }

    public void destroy() {
        for (MasterListener m : masterListeners) {
            m.shutdown();
        }
        for (SlaveDownListener m : slaveDownListeners) {
            m.shutdown();
        }
        for (SlaveUpListener m : slaveUpListeners) {
            m.shutdown();
        }
        super.destroy();
    }

    public HostAndPort getCurrentHostMaster() {
        return currentHostMaster;
    }

    protected void initPool(HostAndPort master) {
        if (!master.equals(currentHostMaster)) {
            currentHostMaster = master;
            if (factory == null) {
                factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
                        soTimeout, password, database, clientName);
                initPool(poolConfig, factory);
            } else {
                factory.setHostAndPort(currentHostMaster);
                // although we clear the pool, we still have to check the
                // returned object
                // in getResource, this call only clears idle instances, not
                // borrowed instances
                internalPool.clear();
            }

            log.info("Created JedisPool to master at " + master);
        }
    }

    protected HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

        HostAndPort master = null;
        boolean sentinelAvailable = false;

        log.info("Trying to find master from available Sentinels...");

        for (String sentinel : sentinels) {
            final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));

            log.fine("Connecting to Sentinel " + hap);

            Jedis jedis = null;
            try {
                jedis = new Jedis(hap.getHost(), hap.getPort());


                List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

                // connected to sentinel...
                sentinelAvailable = true;

                if (masterAddr == null || masterAddr.size() != 2) {
                    log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
                            + ".");
                    continue;
                }

                master = toHostAndPort(masterAddr);
                log.fine("Found Redis master at " + master);

                initReadHaps(jedis, masterName, master);

                break;
            } catch (JedisConnectionException e) {
                log.warning("Cannot connect to sentinel running @ " + hap + ". Trying next one.");
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }

        if (master == null) {
            if (sentinelAvailable) {
                // can connect to sentinel, but master name seems to not
                // monitored
                throw new JedisException("Can connect to sentinel, but " + masterName
                        + " seems to be not monitored...");
            } else {
                throw new JedisConnectionException("All sentinels down, cannot determine where is "
                        + masterName + " master is running...");
            }
        }

        log.info("Redis master running at " + master + ", starting Sentinel listeners...");

        for (MasterListener masterListener : masterListeners) {
            masterListener.shutdown();
        }
        masterListeners.clear();

        for (SlaveDownListener slaveDownListener : slaveDownListeners) {
            slaveDownListener.shutdown();
        }
        slaveDownListeners.clear();

        for (SlaveUpListener slaveUpListener : slaveUpListeners) {
            slaveUpListener.shutdown();
        }
        slaveUpListeners.clear();

        for (String sentinel : sentinels) {
            final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
            MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
            masterListeners.add(masterListener);
            masterListener.start();

            SlaveDownListener slaveDownListener = new SlaveDownListener(masterName, hap.getHost(), hap.getPort());
            slaveDownListeners.add(slaveDownListener);
            slaveDownListener.start();

            SlaveUpListener slaveUpListener = new SlaveUpListener(masterName, hap.getHost(), hap.getPort());
            slaveUpListeners.add(slaveUpListener);
            slaveUpListener.start();

        }

        return master;
    }

    private void initReadHaps(Jedis jedis, String masterName, HostAndPort master) {
        List<Map<String, String>> slaveList = jedis.sentinelSlaves(masterName);
        initReadHaps(slaveList, master);
    }

    /**
     * 初始化读池
     * @param slaveList
     * @param master
     * @return
     */
    private List<HostAndPort> initReadHaps(List<Map<String, String>> slaveList, HostAndPort master) {
        // master允许读时,才放入读池
        if (!masterWriteOnly) {
            readHaps = createReadHaps(slaveList, master);
        } else {
            readHaps = createReadHaps(slaveList);
        }
        return readHaps;
    }

    /**
     * 将slave节点作为读节点放入读池
     * @param slaveList
     * @return
     */
    private List<HostAndPort> createReadHaps(List<Map<String, String>> slaveList) {
        List<HostAndPort> haps = new ArrayList<>();

        for (Map<String, String> slave : slaveList) {
            HostAndPort hap = new HostAndPort(slave.get("ip"), Integer.valueOf(slave.get("port")));
            haps.add(hap);
        }

        return haps;
    }

    private List<HostAndPort> createReadHaps(List<Map<String, String>> slaveList, HostAndPort master) {
        List<HostAndPort> haps = createReadHaps(slaveList);
        haps.add(master);
        return haps;
    }

    /**
     * 读池增加新节点
     * @param hostAndPort
     */
    protected synchronized void insertReadPool(HostAndPort hostAndPort) {
        boolean exists = false;
        for (JedisPoolExt oldJedisPool : jedisReadPools) {
            if (oldJedisPool.getHostAndPort().equals(hostAndPort)) {
                exists = true;
                break;
            }
        }
        if (!exists) {
            JedisPoolExt newJedisPool = new JedisPoolExt(poolConfig, hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout, soTimeout, password, Protocol.DEFAULT_DATABASE, clientName);
            jedisReadPools.add(newJedisPool);

            // 新来的节点,可能是之前挂掉后恢复的节点,所以要检查其是否存在于失败列表中,要去除
            failMap.remove(hostAndPort);

            log.info("Add JedisReadPool at " + hostAndPort);
        }
    }

    /**
     * 读池中去掉一个读节点
     * @param hostAndPort
     */
    protected synchronized void removeFromReadPool(HostAndPort hostAndPort) {
        for (JedisPoolExt jedisPoolExt : jedisReadPools) {
            if (jedisPoolExt.getHostAndPort().equals(hostAndPort)) {
                jedisReadPools.remove(jedisPoolExt);
                log.info("Remove JedisReadPool at " + hostAndPort);
                break;
            }
        }
    }

    /**
     * 替换读池
     * @param newHaps
     */
    protected synchronized void updateReadPools(List<HostAndPort> newHaps) {

        // 拿来新的读节点,在老池里补上
        for (HostAndPort hap : newHaps) {
            insertReadPool(hap);
        }

        // 把老池里有,但不存在于新节点里的节点去掉
        for (int i = 0; i < jedisReadPools.size(); i++) {
            boolean needRemove = true;
            for (HostAndPort hap : newHaps) {
                if (jedisReadPools.get(i).getHostAndPort().equals(hap)) {
                    needRemove = false;
                    break;
                }
            }
            if (needRemove) {
                jedisReadPools.remove(i);
                i--;
            }
        }
    }

    protected HostAndPort toHostAndPort(List<String> getMasterAddrByNameResult) {
        String host = getMasterAddrByNameResult.get(0);
        int port = Integer.parseInt(getMasterAddrByNameResult.get(1));
        return new HostAndPort(host, port);
    }

    @Override
    public Jedis getResource() {
        while (true) {
            Jedis jedis = super.getResource();
            jedis.setDataSource(this);

            // get a reference because it can change concurrently
            final HostAndPort master = currentHostMaster;
            final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient()
                    .getPort());

            if (master.equals(connection)) {
                // connected to the correct master
                return jedis;
            } else {
                jedis.close();
            }
        }
    }

    /**
     * @deprecated starting from Jedis 3.0 this method won't exist. Resouce cleanup should be done
     * using @see {@link Jedis#close()}
     */
    public void returnBrokenResource(final Jedis resource) {
        if (resource != null) {
            returnBrokenResourceObject(resource);
        }
    }

    /**
     * @deprecated starting from Jedis 3.0 this method won't exist. Resouce cleanup should be done
     * using @see {@link Jedis#close()}
     */
    public void returnResource(final Jedis resource) {
        if (resource != null) {
            resource.resetState();
            returnResourceObject(resource);
        }
    }

    /**
     * master切换事件监听
     */
    protected class MasterListener extends Thread {

        protected String masterName;
        protected String host;
        protected int port;
        protected Jedis j;
        protected AtomicBoolean running = new AtomicBoolean(false);

        public MasterListener(String masterName, String host, int port) {
            this.masterName = masterName;
            this.host = host;
            this.port = port;
        }

        public void run() {

            running.set(true);

            while (running.get()) {

                j = new Jedis(host, port);

                try {
                    j.subscribe(new JedisPubSub() {
                        @Override
                        public void onMessage(String channel, String message) {
                            log.info("Sentinel " + host + ":" + port + " published: " + channel + " " + message + ".");

                            String[] switchMasterMsg = message.split(" ");

                            if (switchMasterMsg.length > 3) {

                                if (masterName.equals(switchMasterMsg[0])) {
                                    initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));

                                    if (masterWriteOnly) {
                                        // 如果master只做写,则将新的master从读池去掉
                                        removeFromReadPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                                    } else {
                                        // 如果master同时允许读写,则将旧master从读池去掉
                                        removeFromReadPool(toHostAndPort(Arrays.asList(switchMasterMsg[1], switchMasterMsg[2])));
                                    }
                                } else {
                                    log.fine("Ignoring message on +switch-master for master name "
                                            + switchMasterMsg[0] + ", our master name is " + masterName);
                                }

                            } else {
                                log.severe("Invalid message received on Sentinel " + host + ":" + port
                                        + " on channel +switch-master: " + message);
                            }

                        }
                    }, "+switch-master");
                } catch (JedisConnectionException e) {
                    runningSleep(running.get(), host, port, subscribeRetryWaitTimeMillis);
                }
            }
        }

        public void shutdown() {
            shutdownListener(j, running, host, port);
        }
    }

    /**
     * slave故障恢复&新增slave节点 事件监听
     */
    protected class SlaveUpListener extends Thread {

        protected String masterName;
        protected String host;
        protected int port;
        protected Jedis j;
        protected AtomicBoolean running = new AtomicBoolean(false);

        public SlaveUpListener(String masterName, String host, int port) {
            this.masterName = masterName;
            this.host = host;
            this.port = port;
        }

        public void run() {
            running.set(true);

            while (running.get()) {

                j = new Jedis(host, port);
                try {
                    j.subscribe(new JedisPubSub() {
                        @Override
                        public void onMessage(String channel, String message) {
                            log.info("Sentinel " + host + ":" + port + " published: " + channel + " " + message + ".");

                            String[] switchMasterMsg = message.split(" ");

                            if (switchMasterMsg.length > 7) {
                                String slaveHost = switchMasterMsg[2];
                                int slavePort = Integer.valueOf(switchMasterMsg[3]);
                                HostAndPort hap = new HostAndPort(slaveHost, slavePort);
                                insertReadPool(hap);
                            } else {
                                log.severe("Invalid message received on Sentinel " + host + ":" + port
                                        + " on channel " + channel + ": " + message);
                            }
                        }
                    }, "-sdown", "+slave");
                } catch (JedisConnectionException e) {
                    runningSleep(running.get(), host, port, subscribeRetryWaitTimeMillis);
                }
            }
        }

        public void shutdown() {
            shutdownListener(j, running, host, port);
        }
    }

    protected void shutdownListener(Jedis j, AtomicBoolean running, String host, int port) {
        try {
            log.fine("Shutting down listener on " + host + ":" + port);
            running.set(false);
            j.disconnect();
        } catch (Exception e) {
            log.log(Level.SEVERE, "Caught exception while shutting down: ", e);
        }
    }

    protected void runningSleep(boolean running, String host, int port, long waitTimeMillis) {
        if (running) {
            log.severe("Lost connection to Sentinel at " + host + ":" + port
                    + ". Sleeping " + waitTimeMillis + "ms and retrying.");
            try {
                Thread.sleep(waitTimeMillis);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        } else {
            log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
        }
    }

    /**
     * slave挂掉事件监听
     */
    protected class SlaveDownListener extends Thread {

        protected String masterName;
        protected String host;
        protected int port;
        protected Jedis j;
        protected AtomicBoolean running = new AtomicBoolean(false);

        public SlaveDownListener(String masterName, String host, int port) {
            this.masterName = masterName;
            this.host = host;
            this.port = port;
        }

        public void run() {

            running.set(true);

            while (running.get()) {

                j = new Jedis(host, port);

                try {
                    j.subscribe(new JedisPubSub() {
                        @Override
                        public void onMessage(String channel, String message) {
                            log.info("Sentinel " + host + ":" + port + " published: " + channel + " " + message + ".");

                            String[] switchMasterMsg = message.split(" ");

                            if (switchMasterMsg.length > 7) {
                                String slaveHost = switchMasterMsg[2];
                                int slavePort = Integer.valueOf(switchMasterMsg[3]);
                                HostAndPort hap = new HostAndPort(slaveHost, slavePort);
                                removeFromReadPool(hap);
                            } else if (switchMasterMsg.length != 4) {
                                // master +sdown length=4
                                log.severe("Invalid message received on Sentinel " + host + ":" + port
                                        + " on channel +sdown: " + message);
                            }
                        }
                    }, "+sdown");
                } catch (JedisConnectionException e) {
                    runningSleep(running.get(), host, port, subscribeRetryWaitTimeMillis);
                }
            }
        }

        public void shutdown() {
            shutdownListener(j, running, host, port);
        }
    }

}

代码包: 地址


使用:

@Controller
public class HomeController {

	@Autowired
	private  RedisDataSource redisDataSource;

	  private static int z=1;  
	    @RequestMapping(value = "/setKey", method = RequestMethod.GET) 
	    @ResponseBody
	    public String setKey() { 
		    	z++;
		    	String ss=String.valueOf(z);

		    	redisDataSource.getRedisWriteClient().set(ss, ss);
		    	return ss;
	    }  
	    
	    @RequestMapping(value = "/getKey", method = RequestMethod.GET) 
	    @ResponseBody
	    public String getKey(HttpServletRequest request) { 
	    	String ss=request.getParameter("key");
	  
	    	return  redisDataSource.getRedisReadClient().get(ss);
	    	
	
	    }  






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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_yuki_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值