zoukankan      html  css  js  c++  java
  • Redis

    首先,我们知道,mysql是持久化存储,存放在磁盘里面,检索的话,会涉及到一定的IO,为了解决这个瓶颈,于是出现了缓存,比如现在用的最多的 memcached(简称mc)。
    首先,用户访问mc,如果未命中,就去访问mysql,之后像内存和硬盘一样,把数据复制到mc一部分。
      
    redis和mc都是缓存,并且都是驻留在内存中运行的,这大大提升了高数据量web访问的访问速度。然而mc只是提供了简单的数据结构,比如 string存储;redis却提供了大量的数据结构
    比如string、list、set、hashset、sorted set这些,这使得用户方便了好多,毕竟封装了一层实用的功能,同时实现了同样的效果,当然用redis而慢慢舍弃mc。   
    内存和硬盘的关系,硬盘放置主体数据用于持久化存储,而内存则是当前运行的那部分数据,CPU访问内存而不是磁盘,这大大提升了运行的速度,当然这是基于程序的局部化访问原理。
      
    推理到redis+mysql,它是内存+磁盘关系的一个映射,mysql放在磁盘,redis放在内存,这样的话,web应用每次只访问redis,如果没有找到的数据,才去访问Mysql。   然而redis+mysql和内存+磁盘的用法最好是不同的

    redis:remote dictionary service(远程字典服务器),开源免费,用C语言编写,是一个高性能的KV键值对服务器
    相比memcached
    1、redis支持数据的持久化,可以将内存中数据保持在磁盘,重启时可以再次加载进行使用;
    2、支持的数据结构更多如:string、list、set、hashset、sorted set
    3、支持的数据备份
    redis的键是一直保存在内存中的,而值并不是,当redis发现内存达到某个阈值时,会将一些key对应的值保存到磁盘中

    注意:redis是单线程程序,顺序执行所有指令,其它指令必须等到当前的指令执行完了才可以继续。


    redis在spring boot中的简单使用:
    1、引入jar包:
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>    
    

     2、在spring boot2之后,对redis连接的支持,默认就采用了lettuce(之前是Jedis)。这就一定程度说明了lettuce 和Jedis的优劣。

      Jedis:是老牌的Redis的Java实现客户端,提供了比较全面的Redis命令的支持,

      Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列

           Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器

     比较:

      Jedis在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个jedis实例增加物理连接

      lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访,所以一个连接实例可以满足多线程环境下的并发访问,一个连接实例不够的情况也可以按需增加连接实例

    3、redis配置(默认是不使用连接池的,只有配置 redis.lettuce.pool下的属性的时候才可以使用到redis连接池)

    spring.redis.database=0
    spring.redis.password=Test123456
    spring.redis.host=172.19.1.159
    spring.redis.port=6379
    spring.redis.timeout=5000
    # 连接池最大连接数(使用负值表示没有限制) spring.redis.lettuce.pool.max-active=50
    # 连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=20
    # 连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=10
    # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.lettuce.pool.max-wait=6000

     4、redis工具类:

    package com.aikucun.bill.util;
    
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    import javax.annotation.Resource;
    
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.support.atomic.RedisAtomicLong;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    
    /**
     * @author: wangzhikun
     * @date: 2019-04-09
     */
    
    @Component
    public final class RedisUtil {
        @Resource 
        private RedisTemplate<String, Object> redisTemplate;
    
        // =============================common============================
    
        /**
         * 指定缓存失效时间
         *
         * @param key  键
         * @param time 时间(秒)
         * @return
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据key 获取过期时间
         *
         * @param key 键 不能为null
         * @return 时间(秒) 返回0代表为永久有效
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        /**
         * 判断key是否存在
         *
         * @param key 键
         * @return true 存在 false不存在
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除缓存
         *
         * @param key 可以传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    redisTemplate.delete(key[0]);
                } else {
                    redisTemplate.delete(CollectionUtils.arrayToList(key));
                }
            }
        }
    
        // ============================String=============================
    
        /**
         * 普通缓存获取
         *
         * @param key 键
         * @return*/
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 普通缓存放入
         *
         * @param key   键
         * @param value 值
         * @return true成功 false失败
         */
    
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 普通缓存放入并设置时间
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
    
        public boolean set(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 递增
         *
         * @param key   键
         * @param delta 要增加几(大于0)
         * @return 134
         */
    
        public long incr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递增因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
    
        /**
         * 递减
         *
         * @param key   键
         * @param delta 要减少几(小于0)
         * @return
         */
    
        public long decr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递减因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, -delta);
        }
    
        // ================================Map=================================
    
        /**
         * HashGet
         *
         * @param key  键 不能为null
         * @param item 项 不能为null
         * @return*/
        public Object hget(String key, String item) {
            try {
                return redisTemplate.opsForHash().get(key, item);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 获取hashKey对应的所有键值
         *
         * @param key 键
         * @return 对应的多个键值
         */
    
        public Map<Object, Object> hmget(String key) {
            return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * HashSet
         *
         * @param key 键
         * @param map 对应多个键值
         * @return true 成功 false 失败
         */
        public boolean hmset(String key, Map<String, Object> map) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * HashSet 并设置时间
         *
         * @param key  键
         * @param map  对应多个键值
         * @param time 时间(秒)
         * @return true成功 false失败
         */
    
        public boolean hmset(String key, Map<String, Object> map, long time) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         *
         * @param key   键
         * @param item  项
         * @param value 值
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张hash表中放入数据,如果不存在将创建 并设置新的超时时间
         *
         * @param key   键
         * @param item  项
         * @param value 值
         * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value, long time) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除hash表中的值
         *
         * @param key  键 不能为null
         * @param item 项 可以使多个 不能为null
         */
    
        public void hdel(String key, Object... item) {
            redisTemplate.opsForHash().delete(key, item);
        }
    
        /**
         * 判断hash表中是否有该项的值
         *
         * @param key  键 不能为null
         * @param item 项 不能为null
         * @return true 存在 false不存在
         */
    
        public boolean hHasKey(String key, String item) {
            return redisTemplate.opsForHash().hasKey(key, item);
        }
    
        /**
         * hash递增 如果不存在,就会创建一个 并把新增后的值返回
         *
         * @param key  键
         * @param item 项
         * @param by   要增加几(大于0)
         * @return
         */
        public long hincr(String key, String item, long by) {
            try {
                return redisTemplate.opsForHash().increment(key, item, by);
            } catch (Exception e) {
                e.printStackTrace();
                return 1;
            }
        }
    
        /**
         * hash递减
         *
         * @param key  键
         * @param item 项
         * @param by   要减少记(小于0)
         * @return
         */
    
        public double hdecr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, -by);
        }
    
    
        // ============================set=============================
    
        /**
         * 根据key获取Set中的所有值
         *
         * @param key 键
         * @return
         */
        public Set<Object> sGet(String key) {
            try {
                return redisTemplate.opsForSet().members(key);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 根据value从一个set中查询,是否存在
         *
         * @param key   键
         * @param value 值
         * @return true 存在 false不存在
         */
    
        public boolean sHasKey(String key, Object value) {
            try {
                return redisTemplate.opsForSet().isMember(key, value);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将数据放入set缓存
         *
         * @param key    键
         * @param values 值 可以是多个
         * @return 成功个数
         */
    
        public long sSet(String key, Object... values) {
            try {
                return redisTemplate.opsForSet().add(key, values);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 将set数据放入缓存
         *
         * @param key    键
         * @param time   时间(秒)
         * @param values 值 可以是多个
         * @return 成功个数
         */
    
        public long sSetAndTime(String key, long time, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().add(key, values);
                if (time > 0) {
                    expire(key, time);
                }
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 获取set缓存的长度
         *
         * @param key 键
         * @return 358
         */
    
        public long sGetSetSize(String key) {
            try {
                return redisTemplate.opsForSet().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 移除值为value的
         *
         * @param key    键
         * @param values 值 可以是多个
         * @return 移除的个数
         */
    
        public long setRemove(String key, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().remove(key, values);
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        // ===============================list=================================
    
        /**
         * 获取list缓存的内容
         *
         * @param key   键
         * @param start 开始
         * @param end   结束 0 到 -1代表所有值
         * @return 391
         */
    
        public List<Object> lGet(String key, long start, long end) {
            try {
                return redisTemplate.opsForList().range(key, start, end);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 获取list缓存的长度
         *
         * @param key 键
         * @return 405
         */
    
        public long lGetListSize(String key) {
            try {
                return redisTemplate.opsForList().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 通过索引 获取list中的值
         *
         * @param key   键
         * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
         * @return 420
         */
        public Object lGetIndex(String key, long index) {
            try {
                return redisTemplate.opsForList().index(key, index);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @return 436
         */
    
        public boolean lSet(String key, Object value) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒)
         * @return 453
         */
    
        public boolean lSet(String key, Object value, long time) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @return 472
         */
    
        public boolean lSet(String key, List<Object> value) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒)
         * @return 490
         */
    
        public boolean lSet(String key, List<Object> value, long time) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据索引修改list中的某条数据
         *
         * @param key   键
         * @param index 索引
         * @param value 值
         * @return 509
         */
    
        public boolean lUpdateIndex(String key, long index, Object value) {
            try {
                redisTemplate.opsForList().set(key, index, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 移除N个值为value
         *
         * @param key   键
         * @param count 移除多少个
         * @param value 值
         * @return 移除的个数
         */
    
        public long lRemove(String key, long count, Object value) {
            try {
                Long remove = redisTemplate.opsForList().remove(key, count, value);
                return remove;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        public long getAndIncrement(String key, int defaultValue) {
            long incrementId = defaultValue;
            try {
                RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
                if (counter.get() <= defaultValue) {
                    counter.set(defaultValue);
                }
                incrementId = counter.getAndIncrement();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return incrementId;
        }
    }
    View Code

    5、测试用例

     1 package com.aikucun.bill.controller;
     2 
     3 import org.junit.Test;
     4 import org.junit.runner.RunWith;
     5 import org.springframework.beans.factory.annotation.Autowired;
     6 import org.springframework.boot.test.context.SpringBootTest;
     7 import org.springframework.test.context.junit4.SpringRunner;
     8 
     9 import com.aikucun.bill.util.RedisUtil;
    10 
    11 @RunWith(SpringRunner.class)
    12 @SpringBootTest
    13 public class RedisInfoTest {
    14 
    15     @Autowired
    16     private RedisUtil redisUtil;
    17 
    18     @Test
    19     public void test() {
    20         System.out.println("--------------------");
    21         redisUtil.set("PANSHI-BILL-TEST1", "wwwwwwwwwwwwwwwwwww");
    22         Object value1 = redisUtil.get("PANSHI-BILL-TEST1");
    23         Object value2 = redisUtil.get("wwww");
    24         System.out.println("value1:" + value1 + "            value2:" + value2);
    25     }
    26 }
    View Code

    6、分布式锁:

    有3种:

    1、基于数据库乐观锁

    如:SELECT * from goods where ID =1 for update;

       UPDATE goods set stock = stock - 1;

    2、基于zookeeper:

    大致思想即为:每个客户端对某个功能加锁时,在zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

    优点:锁安全性高,zk可持久化   缺点: 性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能

    3、基于redis:(推荐)

    1)方法一:springboot2.x 以上使用redis时,用了lettuce封装,比起jedis线程安全:

    需要的依赖:

        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>2.0.5.RELEASE</version>
        </dependency>
    View Code

    封装工具类:

    package com.aikucun.delivery.util;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.connection.RedisStringCommands;
    import org.springframework.data.redis.connection.ReturnType;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.types.Expiration;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.nio.charset.Charset;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Description: 分布式锁工具类
     * @Author:
     * @CreateDate:
     */
    @Component
    @Slf4j
    public class RedisLock {
        public static final String UNLOCK_LUA;
    
        /**
         * 释放锁脚本,原子操作
         */
        static {
            StringBuilder sb = new StringBuilder();
            sb.append("if redis.call("get",KEYS[1]) == ARGV[1] ");
            sb.append("then ");
            sb.append("    return redis.call("del",KEYS[1]) ");
            sb.append("else ");
            sb.append("    return 0 ");
            sb.append("end ");
            UNLOCK_LUA = sb.toString();
        }
    
        @Resource
        private RedisTemplate redisTemplate;
    
        /**
         * 获取分布式锁,原子操作
         * @param lockKey
         * @param requestId 唯一ID, 可以使用UUID.randomUUID().toString();
         * @param expire
         * @param timeUnit
         * @return
         */
        public boolean tryLock(String lockKey, String requestId, long expire, TimeUnit timeUnit) {
            try {
                RedisCallback<Boolean> callback = (connection) -> {
                    return connection.set(lockKey.getBytes(Charset.forName("UTF-8")), requestId.getBytes(Charset.forName("UTF-8")), Expiration.seconds(timeUnit.toSeconds(expire)), RedisStringCommands.SetOption.SET_IF_ABSENT);
                };
                return (Boolean) redisTemplate.execute(callback);
            } catch (Exception e) {
                log.error("redis lock error.", e);
            }
            return false;
        }
    
        /**
         * 释放锁
         * @param lockKey
         * @param requestId 唯一ID
         * @return
         */
        public boolean releaseLock(String lockKey, String requestId) {
            RedisCallback<Boolean> callback = (connection) -> {
                return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1, lockKey.getBytes(Charset.forName("UTF-8")), requestId.getBytes(Charset.forName("UTF-8")));
            };
            return (Boolean) redisTemplate.execute(callback);
        }
    
        /**
         * 获取Redis锁的value值
         * @param lockKey
         * @return
         */
        public String get(String lockKey) {
            try {
                RedisCallback<String> callback = (connection) -> {
                    return new String(connection.get(lockKey.getBytes()), Charset.forName("UTF-8"));
                };
                return (String) redisTemplate.execute(callback);
            } catch (Exception e) {
                log.error("get redis occurred an exception", e);
            }
            return null;
        }
    
    }
    View Code

    2)方法二:也可以用spring-data-redis加jedis或者lettuce

    @Repository
    public class RedisLock {
    
        private static final String unlockScript =
                  "if redis.call("get",KEYS[1]) == ARGV[1]
    "
                + "then
    "
                + "    return redis.call("del",KEYS[1])
    "
                + "else
    "
                + "    return 0
    "
                + "end";
    
        private StringRedisTemplate redisTemplate;
    
        public RedisLock(StringRedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        public String lock(String name, long expire, long timeout) throws InterruptedException {
            Assert.isTrue(timeout>0 && timeout<60000, "timeout must greater than 0 and less than 1 min");
    
            long startTime = System.currentTimeMillis();
            String token;
            do{
                token = tryLock(name, expire);
                if(token == null) {
                    if((System.currentTimeMillis()-startTime) > (timeout-50))
                        break;
                    Thread.sleep(50); //try 50 per sec
                }
            }while(token==null);
    
            return token;
        }
      
        public String tryLock(String name, long expire) {
            Assert.notNull(name, "Lock name must not be null");
            Assert.isTrue(expire>0, "expire must greater than 0");
    
            String token = UUID.randomUUID().toString();
            RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
            RedisConnection conn = factory.getConnection();
            try{
                Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")), 
                        Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
                if(result!=null && result)
                    return token;
            }finally {
                RedisConnectionUtils.releaseConnection(conn, factory);
            }
            return null;
        }
    
        public boolean unlock(String name, String token) {
            Assert.notNull(name, "Lock name must not be null");
            Assert.notNull(token, "Token must not be null");
    
            byte[][] keysAndArgs = new byte[2][];
            keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8"));
            keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
            RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
            RedisConnection conn = factory.getConnection();
            try {
                Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
                if(result!=null && result>0)
                    return true;
            }finally {
                RedisConnectionUtils.releaseConnection(conn, factory);
            }
            
            return false;
        }
    }
    View Code

      





  • 相关阅读:
    CenterNet-TensorRT错误记录
    NAS研究要点分析
    conda如何安装从源下载的离线安装包
    Xavier上pytorch半精度inference问题
    Xavier 使用便携程序
    Xavier疑问
    Python输入(Leetcode
    兴趣爱好
    生活目标
    TX2装机教程
  • 原文地址:https://www.cnblogs.com/wzk-0000/p/6866333.html
Copyright © 2011-2022 走看看