ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

redis高级用法

2021-06-28 18:30:59  阅读:199  来源: 互联网

标签:脚本 AOF set redis Redis 高级 用法 key


redis高级特性

一、发布订阅

概述

发布订阅功能,可以类比的理解为广播,发布端发布消息至redis,订阅端通过监听redis的channel获取消息内容。

在这里插入图片描述

如上图所示,publisher1可以发布消息到java-channel和python-channel;subscriber3也同时可以订阅java-channel、python-channel、ruby-channel的消息。

  • 订阅端可以同时订阅多个channel,可以通过监听方法的入参来区分channel。
  • 发布端可以,分别向多个channel发布消息,并不支持一次发布到多个channel。
  • 发布订阅不确保订阅端一定收到消息,假如订阅端出现网络异常的问题。
常用命令
# 同时订阅多个channel
subscribe java-channel python-channel ruby-channel

# 发布消息,向java-channel发布hello
publish java-channel hello

# 取消订阅
unsubscrube java-channel
按规则(pattern)订阅频道

支持?和*占位符。问号代表一个字符,星号代表0个或多个字符。

# 订阅上面的的3个channel
subscribe *-channel

二、reids事务

1.为什么要使用事务

redis的单个命令是原子性,要么成功,要么失败,不存在并发干扰。

但是多个命令执行个,如何保证原子性,这里就需要使用到事务。

redis事务的特点

  • 按照进入队列的顺序执行。
  • 不会受到客户端请求的干扰。
  • 事务不能嵌套,多个muti效果一样。
2.事务的使用

redis事务的使用有4个命令,分别是: muti (开启事务),exec (执行事务),discard(取消事务)、watch(监视)

案例:jack和tom各有100块,jack想tom转账20块。

set jack 100
set tom 100
multi
decrby jack 20
incrby tom 20
exec
get jack
get tom
3.watch 命令介绍

为防止事务过程中某个key值被客户端请求修改,带来非预期的结果,redis提供了watch命令。

它可以看作是redis事务提供CAS乐观锁(Compare and Swap)。

watch命令可以监视一个或者多个key,一旦一个key被修改或者删除,如果开启事务之后,被监视的key在exec执行之前被修改,那么整个事务都会被取消。

案例:

client1client2
set test 1000
watch test
multi
incrby test 100
decrby test 100
exec 【返回nil
get test ==900

当client2在exec执行之前修改了test的值,这时候client1的事务失效,所以get test == 900

4.事务可能遇到的问题

事务执行出现问题,一般有两种。

一种在执行exec之前;

另一种在执行exec之后;

执行exec之前

比如:入队的命令存在语法错误、参数数量、参数名等等(编译器错误)。

multi
set xiaoming 200
set xiaohong yes
hset bobo 666
exec
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set xiaoming 1000
QUEUED
127.0.0.1:6379> set xiaohong yes
QUEUED
127.0.0.1:6379> hset bobo 666
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 

执行exec之后

案例,如下,对String使用了Hash的命令,参数个数正确,但是数据类型错误,这是一种运行时错误。

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 1111
QUEUED
127.0.0.1:6379> hset k1 a b
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"1111"

最后我们发现set k1 1111的命令是成功的,也就是在这种发生了运行时异常的情况下,只有错误的命令没有执行,但其他命令不受到影响

这里为什么不回滚呢?

官方的解释如下:

  • redis命令只会因为语法错误而失败,也就是说,从实用性的角度来说,失败的命令是由代码错误造成的,而这些错误应该在开发的过程种被发现,而不应该出现在生产环境中。(这是程序员的锅)
  • 因为不需要对回滚进行支持,所以redis的内部可以保持简单并且快速。而要知道的是:回滚不能解决代码的问题。

三、Lua脚本

Lua 是一种轻量级脚本语言,它是用C语言编写的,跟数据的存储过程有点类型。

redis从2.6版本开始映入Lua脚本,也就是说redis可以用Lua来执行redis命令。

https://redis.io/commands/eval

使用Lua给redis带来的好处?

  1. 一次性发送多个命令,减少网络开销。
  2. Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保证原子性
  3. 对于复杂的的组合命令,我们可以放在文件中,可以是实现命令的复用。
1.Redis中使用Lua脚本

使用eval [ɪ’væl] 方法,语法

redis> eval lua-script key-num [keyl key2 key3 ...][valuel value2 value3 ...]
  • eval代表执行Lua语言的命令。
  • lua- script代表Lua语言脚本内容。
  • key-num表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。
  • [key1 key2 key3 …]是key作为参数传递给Lua语言,也可以不填,但是需要和 key-num的个数对应起来。
  • [value1 value2 vaue3 …]这些参数传递给Lua语言,它们是可填可不填的。

实例如下

计算 2 + 3 = 5

127.0.0.1:6379> eval "return KEYS[1] + ARGV[1]" 1 2 3 
(integer) 5
2.lua脚本执行redis命令

命令格式

使用 redis.call(command, key [param1, param2…])进行操作。语法格式:

redis.call(command,key [param1,param2])
  • command是命令,包括set、get、del等。
  • key是被操作的键。
  • param1 param2…代表给key的参数。

实例:执行redis命令 set test 123

# 写死的方式
eval "return redis.call('set','test','123')" 0
# 传值的方式
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 test 123
3.Lua脚本文件

脚本执行命令

redis-cli --eval [lua脚本][key...]空格,空格[args...]

案例1.执行下redis的set命令。

test1.lua脚本内容如下

redis.call('set',KEYS[1],ARGV[1])
return redis.call('get',KEYS[1])

调用脚本

redis-cli --eval test1.lua yangfan , 88

案例2.对IP进行限流

需求:每个用户在Ⅹ秒内只能访问Y次。

设计思路

首先是数据类型。用 String的key记录,用 value记录访问次数。几秒钟和几次要用参数动态传进去。

拿到护P以后,对P+1。如果是第一次访问,对key设置过期时间(参数1)。否则

判断次数,超过限定的次数(参数2),返回0。如果没有超过次数则返回1。超过时间,

key过期之后,可以再次访问。

KEY]是P,ARGV[是过期时间X,ARGV[2]是限制访问的次数Y。

脚本内容

-- ip_limit.lua
-- IP限流,对某个IP频率进行限制,5分钟 访问10次
local num = redis.call('incr',KEYS[1])
if tonumber(num) == 1 then
        redis.call('expire',KEYS[1],ARGV[1])
        return 1
elseif tonumber(num) > tonumber(ARGV[2]) then
        return 0
else
        return 1
end

执行脚本

redis-cli --eval ip_limit:192.168.0.1 5 , 10
4.缓存Lua脚本

为什么要缓存脚本?

在Lua脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis服务端,会产生比较大的网络开销。为了解决这个问题, Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。

如何缓存?

  1. 通过script load 命令,在服务端缓存lua脚本生成一个摘要码

    127.0.0.1:6379> script load "return 'hello world'"
    "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
    
  2. 使用evalsha 通过摘要码执行缓存的脚本

    127.0.0.1:6379> evalsha "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
    "hello world"
    
   



**案例1:**实现乘法的功能

​```lua
local curVal = redis.call("get",KEYS[1])
if curVal == false then
        curVal = 0
else
        curVal = tonumber(curVal)
en
curVal = curVal * tonumber(ARGV[1])
redis.call("set",KEYS[1],curVal)
return curVal

执行过程

127.0.0.1:6379> set num 2

# script load 缓存单行lua脚本(redis客服端执行命令)
127.0.0.1:6379> script load 'local curVal = redis.call("get",KEYS[1]);if curVal == false then curVal = 0 else curVal = tonumber(curVal) end;curVal = curVal * tonumber(ARGV[1]);redis.call("set",KEYS[1],curVal);return curVal'
"9d136078b9c1e7669ff934a01a495b0233414187"

# 调用
127.0.0.1:6379> evalsha "9d136078b9c1e7669ff934a01a495b0233414187" 1 num 2
(integer) 4

5.脚本超时

Redis的指令执行是单线程的,如果执行Lua脚本超时或者陷入死循环,则客户端命令进入等待状态。

lua脚本执行 超时时间设置

lua-time-limit 5000 # 默认5秒

如果超过5秒,其他客户端不会等待,而是直接报“BUSY”错误。

(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

这样也是不行的,怎么可以直接返回客户端报错呢,如果出现 这种问题,则可以使用一下两种方法来解决。

第一种

使用 script kill 来杀死执行的脚本

127.0.0.1:6379> script kill
OK

但是,不是所有的Lua脚本执行都可以kill。如果当执行Lua脚本对Redis的数据进行了修改(SET/DEL等),kill是不能终止脚本运行的。如执行如下脚本:

eval "redis.call('set','testlua','666') while true end" 0

这个时候执行kill,会报如错误

(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.

第二种

如果遇到上面这种问题,这使用如下方法解决

127.0.0.1:6379> shutdown nosave

正常关机是shutdown。shutdown nosave 和shutdown的区别在于shutdown nosave关机不会持久化。意味着上次快照后的数据会丢失。

总结:

我们有些特殊的需求可以使用lua脚本,但是一定注意耗时问题,不要出现脚本超时的情况

四、Redis为什么这么快?

1.benchmark测试脚本

benchmark测试脚本,可以用来测试redis存取数据的并发量。

脚本的使用说明,可以参考官网,链接https://redis.io/topics/benchmarks

redis-benchmark -t set,lpush -n 100000 -q

# 结果
SET: 74239.05 requests per second
LPUSH: 79239.30 requests per second

Redis支持10w QPS看看你的机器,测试出来的结果是什么样

2.Redis速度快的原因
  • 纯内存结构
  • 请求单线程
  • IO多路复用
纯内存结构

KV结构的内存数据库,时间复杂度为O(1)。

请求单线程

这里的单线程是处理请求是单线程来是实现的,也可以把它叫做主线程。

从4.0的版本以后,还引入了一些处理其他任务的线程,比如,清理脏数据、无用连接的释放、大key的删除等。

接收请求处理成单线程的好处?

  1. 没有创建线程、销毁线程带来的消耗
  2. 避免上下文切换导致的cup资源消耗
  3. 避免线程竞争带来的问题,如加锁、释放锁等

在Redis的官方解释中,Redis单线程已经够用,CPU不是Redis的瓶颈。Redis的瓶颈最有可能是带宽和内存。

IO多路复用

I/O是网络I/O;多路是多个TCP连接(socket或channel);复用是指负责一个或多个线程。

它的基本原理就是,不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。

在这里插入图片描述

Redis中使用的I/O多路复用模型,可以理解为上图所示,

客户端,产生多个不同操作的socket请求。服务端,I/O多路复用器会把消息放入队列,然后通过文件事件分派器,转发到不同的事件处理中。

总结:
  1. 由于redis处理请求单线程,所有不要在生成环境执行耗时命令,如keys *、flushall、flushdb。

五、Redis内存回收

1.过期策略
  • 立即过期(主动淘汰)
  • 惰性过期(被动淘汰)
  • 定期过期
立即过期(主动淘汰)

给每个key创建一个定时,过期立即删除。这种方式对内存很友好,但是占用CPU资源,监视key的过期。

惰性过期(被动淘汰)

只有key被访问的时候,判断过期时间,过期则进行删除。这样有些过期的key没被删除,会浪费内存。

定期过期

每隔一段时间,扫描一定数量的数据的expires字典中一定数量的key,并清除其中已经过期的key。该策略是个折中方案。通过调整定时扫描的间隔和每次扫描的耗时,可以在不同情况时间CPU和内存的最优的平衡效果。

总结

Redis中使用了惰性过期和定时过期两种过期策略,并不是实时的清除过期key。

2.淘汰策略

Redis的内存淘汰策略,是指当内存使用达到最大内存极限时,决定清理掉哪些数据,以保证新数据的存入。

Redis最大内存可以通过redis.conf中如下参数配置

# maxmemory <bytes>	

如果不置maxmemory或设置为0,32位系统最多使用3GB,64位系统不限制内存。

Reids客户端动态修改

redis>config set maxmemory 2GB
redis淘汰策略

官网对介绍 https://redis.io/topics/lru-cache

redis.conf中配置

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, when there are no suitable keys for
# eviction, Redis will return an error on write operations that require
# more memory. These are usually commands that create new keys, add data or
# modify existing keys. A few examples are: SET, INCR, HSET, LPUSH, SUNIONSTORE,
# SORT (due to the STORE argument), and EXEC (if the transaction includes any
# command that requires memory).
#
# The default is:
#
maxmemory-policy noeviction  #这里是没有进行配置,默认值

由上面的配置文件可知,内存淘汰策略选项有8种如下:

volatile-lru -> Evict using approximated LRU, only keys with an expire set.
allkeys-lru -> Evict any key using approximated LRU.
volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key having an expire set.
allkeys-random -> Remove a random key, any key.
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
noeviction -> Don’t evict anything, just return an error on write operations.

由后缀来看分为:

LRU(Least Recently Used):最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。

LFU (Least Frequently/ˈfriːkwəntli/ Used) :最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

Random:随机删除。

由前缀来区分:

volatile是针对设置了ttl的key,

allkeys是针对所有key。

策略含义
volatile-lru根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够内存为止。如果没有可删除的键对象,回退到noeviction策略。
allkeys-lru根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。
volatile-lfu在设置过期属性键中,选择最不常用的删除。
allkeys-lfu在所有的键中选择最不常用的,不管数据有没有设置超时属性。
volatile-random在设置超时属性的键中随机选择。
allkeys-random随机删除所有的键,直到腾出足够的内存为止。
volatile-ttl根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
noeviction默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息,此时Redis只响应读操作。

如果没有设置ttl或者没有符合前提条件的key被淘汰,那么volatile-lru、volatile-random、volatile-ttl相当于noeviction(不做内存回收)。

动态修改淘汰策略

redis> config set maxmemory-policy volatile-lru

建议使用volatile-lru,在保证服务正常的情况下,优先删除最近最少使用的key。

六、持久化

官网介绍 https://redis.io/topics/persistence

Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis提供了两种持久化的方案,一种是RDB快照(Redis DataBase),一种是AOF(Append Only File)。持久化是Redis跟 Memcache的主要区别之一。

1、RDB

RDB是Redis 默认的持久化方案(注意如果开启了AOF,优先用AOF)。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启会通过加载dump.rdb 文件恢复数据。

RDB触发条件
配置触发

通过配置redis.conf,配置方法如下

save 900 1 # 900秒内至少有一个key 被修改(包括添加)
save 300 10 # 400秒内至少有10个key被修改
save 60 10000 # 60秒内至少有10000个key被修改

以上配置满足任意一个都会触发。可以使用lastsave查看最近一次生成快照的时间,rdb文件位置和目录配置如下:

#文件路径,
dir ./
#文件名称
dbfilename dump.rdb
#是否以LZF压缩rdb 文件
rdbcompression yes
#开启数据校验
rdbchecksum yes

参数说明
dirrdb文件默认在启动目录下(相对路径),config get dir获取
dbfilename文件名称
rdbcompression开启压缩可以节省存储空间,但是会消耗一些CPU的计算时间,默认开启
rdbchecksum使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
shutdown触发RDB

保证服务器正常关闭,数据不丢失。

flushall触发

通过rdb将没有持久化的数据,进行持久化,然后在删除rdb文件。

手动触发

通过如下命令可以让Redis生成快照。

  • save

  • bgsave

    save:在执行的数据,会阻塞redis服务器,所以慎用。

    bgsave:后台异步执行快照,快照同时还可以响应客户端请求。

RDB优缺点
优点
  1. RDB是一个非常紧凑(compact)的文件,它保存了redis在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。(文件小,易备份)
  2. 生成RDB文件的时候,redis主进程会tork()一个子进程米处理所月休仔上TF,土进程不需要进行任何磁盘I〇操作。(异步生成RDB)
  3. RDB 在恢复大数据集时的速度比AOF的恢复速度要快。(数据恢复快)
缺点
  1. RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork 操作创建子进程,频繁执行成本过高。(不能实时持久化)
  2. 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。(出现意外,容易丢数据)

2、AOF

Append Only File

AOF: Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。

Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。

AOF配置说明
#-开关
appendonly no
#文件名
appendfilename "appendonly.aof"
参数说明
appendonlyRedis 默认只开启RDB持久化,开启AOF需要修改为yes
appendfilename “appendonly.aof”路径也是通过dir参数配置config get dir
数据是实时持久化到磁盘吗?

由于操作系统的缓存机制,AOF数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到AOF文件?

参数说明
appendfsync everysecAOF持久化策略(硬盘缓存到磁盘),默认everysec
no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;
always表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低;
everysec表示每秒执行一次 fsync,可能会导致丢失这1s 数据。通常选择everysec ,兼顾安全性和效率。
文件越来越大怎么办?

可以使用命令bgrewriteaof来重写。

AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。

#重写触发机制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
参数说明
auto-aof-rewrite-percentage默认值为100。aof自动重写配置,当目前aof 文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-min-size默认64M。设置允许重写的最小aof 文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。
参数说明
no-appendfsync-on-rewrite在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和 always的 aof模式来说,执行 fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议修改为yes . Linux的默认fsync策略是30秒。可能丢失30秒数据。
aof-load-truncatedaof 文件可能在尾部是不完整的,当redis启动的时候,aof 文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象。redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是 no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为yes。
优缺点

优点:

  1. AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据而已。(实时持久化)

缺点

  1. 对于具有相同数据的的Redis,AOF文件通常会比RDF 文件体积更大(RDB存的是数据快照)。
  2. 虽然AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB比AOF具好更好的性能保证

3、方案比较

如果可以忍受一小段时间内数据的丢失,毫无疑问使用RDB是最好的,定时生成RDB快照(snapshot)非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快。
否则就使用AOF重写。

但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

标签:脚本,AOF,set,redis,Redis,高级,用法,key
来源: https://blog.csdn.net/qq_16116881/article/details/118309418

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有