赞
踩
注意有个v8版本,是有方式上不太一样
import ( "context" "errors" "fmt" "github.com/go-redis/redis" "math/rand" "net" "os" "strconv" "testing" "time" ) // 声明一个全局的rdb变量 var rdb *redis.Client //1.初始化连接初始化连接 func initClient() (err error) { rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) _, err = rdb.Ping().Result() if err != nil { fmt.Println("连接redis失败") os.Exit(1) } return nil }
func redisClientInfo() { rdb = redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", DB: 0, Password: "", Network: "tcp", //连接池容量及闲置连接数量 PoolSize: 15, // 连接池最大socket连接数,默认为4倍CPU数, 4 * runtime.NumCPU MinIdleConns: 10, //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量;。 //超时 DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒。 ReadTimeout: 3 * time.Second, //读超时,默认3秒, -1表示取消读超时 WriteTimeout: 3 * time.Second, //写超时,默认等于读超时 PoolTimeout: 4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒 //闲置连接检查包括IdleTimeout,MaxConnAge //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。 IdleCheckFrequency: 60 * time.Second, IdleTimeout: 5 * time.Minute, //闲置超时,默认5分钟,-1表示取消闲置超时检查 //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接 MaxConnAge: 0 * time.Second, //命令执行失败时的重试策略 MaxRetries: 0, // 命令执行失败时,最多重试多少次,默认为0即不重试 MinRetryBackoff: 8 * time.Millisecond, //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔 MaxRetryBackoff: 512 * time.Millisecond, //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔 //可自定义连接函数 Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { netDialer := &net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 5 * time.Minute, } return netDialer.Dial("tcp", "127.0.0.1:6379") }, //钩子函数 //仅当客户端执行命令时需要从连接池获取连接时,如果连接池需要新建连接时则会调用此钩子函数 OnConnect: func(ctx context.Context, cn *redis.Conn) error { fmt.Printf("conn=%v\n", cn) return nil }, }) }
func TestRedis1(t *testing.T) { //1.设置key和value,以及key的过期时间expiration err := rdb.Set("key", "val", 0).Err() if nil != err { fmt.Println("插入失败") return } //2.获取key的值 valStr, err := rdb.Get("key").Result() if nil != err { if redis.Nil == err { fmt.Println("不存在") return } fmt.Println("插入数据异常err:" + err.Error()) return } fmt.Println(valStr) //3.设置一个key的值,并且返回这个key的旧值 rdb.GetSet("key", "val").Result() //4.key不存在,则设置这个key,存在则失败, 成功返回1,失败返回0 rdb.SetNX("", "", 0).Err() //5.批量设置key的值 rdb.MSet("").Err() //6.批量查询key值 rdb.MGet("").Result() //7.删除单个或多个key rdb.Del("key1", "key2").Err() //8.设置指定key的过期时间 rdb.Expire("", 0).Err() //9.针对一个key进行递增操作 rdb.Incr("key").Result() //10.针对一个key进行递减操作 rdb.Decr("key").Result() //11.查看键的类型 str, err := rdb.Type("key").Result() if err == nil { fmt.Println(str) } //12.查看指定key的过期时间 time, err := rdb.TTL("key").Result() if err == nil { fmt.Println(time) } //13.执行自定义命令 rdb.Do("set", "key", 10, "EX", 3600).Err() //14.根据前缀获取Key rdb.Keys("key*") //15.通过游标迭代器查询,非阻塞式查询key,与keys命令的区别: // > scan时间复杂度虽然也是O(N),但是分次进行的,不会阻塞线程。 // > scan命令提供了limit参数,可以控制每次返回结果的最大条数。 //当Scan方法需要的cursor也就是游标为0时,表示一个新的完整迭代 //当执行完Scan方法后,返回的数据实际包含两部分一个是游标,一个是元素值 //如果下次想再此处继续迭代,则将cursor设置为上次返回的 //count: 指定返回结果的最大条数 iter := rdb.Scan(0, "k*", 0).Iterator() for iter.Next() { err := rdb.Del(iter.Val()).Err() if err != nil { panic(err) } } }
func TestRedisHash(t *testing.T) { //1.添加 err := rdb.HSet("key", "field", "val").Err() if nil != err { fmt.Println("插入失败") return } //2.获取指定key的指定field值 rdb.HGet("key", "field").Result() //3.批量设置 fieldMap := make(map[string]interface{}) fieldMap["field1"] = "val1" fieldMap["field2"] = "val2" rdb.HMSet("key", fieldMap) //4.获取key下所有field值,或者获取key下一个或多个field值 rdb.HMGet("key", "field1", "field2") //5.获取指定key下所有field与对应的值 rdb.HGetAll("key") //6.在指定key的指定field上进行累加 rdb.HIncrBy("key", "field", 1) //7.获取指定key下的所有field rdb.HKeys("key") //8.获取指定key下字段数量 rdb.HLen("key") //9.向指定key中添加field如果不存在添加成功返回1 //如果存在添加无效返回0 rdb.HSetNX("key", "field", "val") //10.删除指定key,或删除key下的指定一个或多个field rdb.HDel("key", "field1", "field2") //11.判断指定key,或key下的指定field是否存在 rdb.HExists("", "") //12.获取指定key下所有value rdb.HVals("key") }
func TestRedisList(t *testing.T) { //1.添加 rdb.LPush("name", "marry").Err() rdb.LPush("name", "tom").Err() //2.将一个或多个值插入到列表头部,如果key不存在,创建一个空列表并执行LPUSH操作 rdb.LPush("key", "val").Err() //将一个值插入到已存在的列表头部,列表不存在时操作无效 rdb.LPushX("key", "val") //3.将一个或多个值插入到列表的尾部(最右边)如果列表不存在,创建一个空列表并执行RPUSH操作 rdb.RPush("", "") //将一个值插入到已存在的列表尾部,列表不存在时操作无效 rdb.RPushX("", "") //4.移除并返回列表的第一个元素 rdb.LPop("key").Result() //5.移除并返回列表的最后一个元素 rdb.RPop("key").Result() //6.返回列表的长度,如果列表key不存在,则key被解释为一个空列表,返回 0 rdb.LLen("") //7.返回列表中指定区间内的元素 // 0表示第一个,1表示第二个,-1表示最后一个,-2表示倒数第二个 rdb.LRange("key", 0, 2) //8.根据参数 COUNT 的值,移除列表中与参数 VALUE 相等的元素 //count>0: 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为COUNT //count<0: 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为COUNT的绝对值 //count=0 : 移除表中所有与 VALUE 相等的值 rdb.LRem("key ", 22, "") //9.通过索引获取列表中的元素,其中 -1表示最后一个,-2表示倒数第二个 rdb.LIndex("key", 3) //10.在列表的元素前或者后插入元素,当指定元素不存在时,返回 -1 rdb.LInsert("key", "BEFORD|AFTER", "元素", "插入val") }
func TestRedisSet(t *testing.T) { //1.添加 rdb.SAdd("name", "xh") //2.获取key下元素个数 rdb.SCard("key") //3.判断集合中是否有该值 rdb.SIsMember("key", "val") //4.获取 rdb.SMembers("key") //5.删除key或删除该key下的指定元素 rdb.SRem("key", "val") //6.随机弹出key下的一个元素 rdb.SPop("key") //7.随机弹出key下的指定个数元素 rdb.SPopN("key", 2) }
func TestRedisZSet(t *testing.T) { //1.添加 z := redis.Z{ Score: 2, Member: "kevin", } rdb.ZAdd("key", z) //2.获取集合中元素个数 rdb.ZCard("key") //3.获取集合中指定分数区间的个数 rdb.ZCount("key", "1", "20") //4.对集合中指定成员的分数加上增量 increment rdb.ZIncrBy("key", 20, "val") //5.获取指定区间内的成员,返回时按分数从小到大排序,相同则按照元素排序 rdb.ZRange("key", 1, 10) //6.获取指定区间内的成员,返回时按分数从大到小排序,相同则按照元素排序 rdb.ZRevRange("key", 1, 10) //7.获取指定分数区间的元素,返回时按分数从小到大排序 zRange := redis.ZRangeBy{ Min: "0", Max: "3", } rdb.ZRangeByScore("key", zRange) //8.获取指定分数区间的元素,返回时按分数从大到小排序 rdb.ZRevRangeByScore("key", zRange) //9.返回在分数之类的所有的成员以及分数?? rdb.ZRangeByScoreWithScores("", zRange) //10.删除指定key,或key下的指定一个或多个元素 rdb.ZRem("key", "val1", "") //11.移除有序集中,指定排名(rank)区间内的所有成员 rdb.ZRemRangeByRank("key", 10, 11) //12.移除有序集中,指定分数区间内的所有成员 rdb.ZRemRangeByScore("key", "1", "10") //13.返回指定元素分数 rdb.ZScore("key", "val") //14.返回元素在集合中的排名 rdb.ZRank("key", "val") }
func TestRedisPublishAndSubscribe(t *testing.T) { //发布 rdb.Publish("频道1", "数据") //订阅,监听指定频道获取数据 pubsub := rdb.Subscribe("频道") // 第一种接收消息方法 // ch := pubsub.Channel() // for msg := range ch // fmt.Println(msg.Channel, msg.Payload) // 第二种接收消息方法 for { msg, err := pubsub.ReceiveMessage() if err != nil { panic(err) } fmt.Println(msg.Channel, msg.Payload) } //匹配模式订阅 pubsub = rdb.PSubscribe("mychannel*") defer pubsub.Close() //取消订阅 pubsub.Unsubscribe("mychannel1", "mychannel2") }
func TestRedisTx(t *testing.T) { //1.执行TxPipeline()开启事物,底层会执行MULTI命令开启事物块 pipe := rdb.TxPipeline() //2.执行业务操作 incr := pipe.Incr("tx_pipeline_counter") pipe.Expire("tx_pipeline_counter", time.Hour) //3.如果业务正常执行完毕,调用Exec()提交事物,底层会执行"EXEC"执行事物块中的所有命令 _, err := pipe.Exec() fmt.Println(incr.Val(), err) //4.如果中间有异常可以执行Discard(),底层会执行"DISCARD"取消事务,放弃执行事务块内的所有命令 pipe.Discard() //5.有些场景下需要配合WATCH //使用WATCH命令监视某个键,事物执行EXEC命令的这段时间内 //如果有其他用户抢先对被监视的键进行了替换、更新、删除等操作, //可以选择提交或放弃事物 key := "watch_count" err = rdb.Watch(func(tx *redis.Tx) error { n, err := tx.Get(key).Int() if err != nil && err != redis.Nil { return err } _, err = tx.Pipelined(func(pipe redis.Pipeliner) error { pipe.Set(key, n+1, 0) return nil }) return err }, key) }
- eval: 代表执行lua语言的命令
- script:一段可以在redis服务器运行的 lua5.1的脚本
- numkeys:用于指定健名参数的个数
- key [key …]:从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。这些键可以通过lua的全局变量KEYS数组获取,用 1 访问形式 KEYS[1],2 是 KEYS[2]
- arg [arg …]:附加参数,可以在lua中用全局变量 ARGV 数组访问,访问形式 ARGV[1],ARGC[2] 等
//示例: luaStr:= eval "return KEYS[1],KEYS[2],ARGV[1],ARGV[2]" 2 key1 key2 first second
// eval lua脚本 参数个数(假设参数个数=2) 参数1 参数2 参数1值 参数2值
var incrBy = redis.NewScript(` local key = KEYS[1] local change = ARGV[1] local value = redis.call("GET", key) if not value then value = 0 end value = value + change redis.call("SET", key, value) return value `) var sum = redis.NewScript(` local key = KEYS[1] local sum = redis.call("GET", key) if not sum then sum = 0 end local num_arg = #ARGV for i = 1, num_arg do sum = sum + ARGV[i] end redis.call("SET", key, sum) return sum `) func TestRedisLua(t *testing.T) { fmt.Printf("# INCR BY\n") for _, change := range []int{+1, +5, 0} { num, err := incrBy.Run(rdb, []string{"my_counter"}, change).Int() if err != nil { panic(err) } fmt.Printf("incr by %d: %d\n", change, num) } sum, err := sum.Run(rdb, []string{"my_sum"}, 1, 2, 3).Int() if err != nil { panic(err) } fmt.Printf("sum is: %d\n", sum) }
- 基于redis实现分布式锁,其原理是依赖redis单线程,原子性存储一个key
- 封装一个代表redis锁核心的上下文结构体,内部添加一个context.Context,通过context.Context解决锁超时问题
- 考虑锁重入,与释放错乱问题,锁key 对应的value是代表每一把锁得唯一编号
- 为保证原子性提供获取锁与释放锁的lua脚本
type RedisLock struct {
ctx context.Context //用来解决锁超时问题
timeoutMs int //锁超时时间
key string //锁key,也就是redis存储时的key
id string //锁编号,也就是redis存储时的val
}
1.提供获取锁脚本, 释放锁脚本,
const ( //加锁时执行lua脚本: //考虑锁的可重入性: 加锁时除了保存锁key,还需要保存代表每一把锁的id //在加锁时先get获取,如果该锁key以存在,说明是锁重入刷新ttl, //没有则set, 这种是可重入锁,防止在同一线程中多次获取锁而导致死锁发生 //KEYS[1]: 表示锁key //ARGV[1]: 表示value //ARGV[2]: 表示过期时间 lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2]) return "OK" else --注意:这里执行的是setnx,如果已存在插入失败返回0 return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) end` //解锁时执行lua脚本 //也就是删除指定锁key,必须先匹配id值,防止A超时后,B马上获取到锁,A的解锁把B的锁删了 delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end` )
var ( // 默认超时时间 defaultTimeout = 500 * time.Millisecond // 重试间隔 retryInterval = 10 * time.Millisecond // 上下文取消 ErrContextCancel = errors.New("context cancel") ) //1.初始化锁 func NewRedisLock(ctx context.Context, key string) *RedisLock { timeout := defaultTimeout if deadline, ok := ctx.Deadline(); ok { timeout = deadline.Sub(time.Now()) } rl := &RedisLock{ ctx: ctx, timeoutMs: int(timeout.Milliseconds()), key: key, id: randomStr(), } return rl } //2.尝试加锁方法 func (rl *RedisLock) TryLock() (bool, error) { t := strconv.Itoa(rl.timeoutMs) resp, err := rdb.Eval(lockCommand, []string{rl.key}, []string{rl.id, t}).Result() if err != nil || resp == nil { return false, nil } reply, ok := resp.(string) return ok && reply == "OK", nil } //3.加锁方法 func (rl *RedisLock) Lock() error { for { select { case <-rl.ctx.Done(): return ErrContextCancel default: //续时 b, err := rl.TryLock() if err != nil { return err } if b { return nil } time.Sleep(retryInterval) } } } //4.是否锁方法 func (rl *RedisLock) Unlock() { rdb.Eval(delCommand, []string{rl.key}, []string{rl.id}).Result() } //5.生成锁编号 func randomStr() string { letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, 10) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) }
func BenchmarkRedisLock1(t *testing.B) { x := t.N wg := sync.WaitGroup{} wg.Add(x) now := time.Now() var n int64 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() for i := 0; i < x; i++ { go func() { defer wg.Done() rl := NewRedisLock(ctx, "testLock") if err := rl.Lock(); err != nil { cancel() // 这里cancel,可以在第一个超时或者发生错误后,后边的不再尝试加锁。 return } defer rl.Unlock() atomic.AddInt64(&n, 1) }() } wg.Wait() fmt.Printf("测试数:%d\t成功数:%d\t结果:%t\t耗时: %d \n", x, n, x == int(n), time.Since(now).Milliseconds()) }
import ( "fmt" "github.com/go-redis/redis" "os" "testing" "time" ) // 声明一个全局的rdb变量 var rdb *redis.Client func initClient() *redis.Client { rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) _, err := rdb.Ping().Result() if err != nil { fmt.Println("连接redis失败") os.Exit(1) } return rdb } //基于redis的bitMap,记录用户一个月内访问uv(既一天访问一次统计) func TestRedisSetBitMap(t *testing.T) { //获取redis句柄 redisDb := initClient() //1.组装key: 酒店编码:时间:用户id key := "IHOTEL:2023-03-27:111222AAASSS" now := time.Now() //2.存储为bitmap类型,key,所在月的天数为偏移量,value为1 err := redisDb.Do("setbit", key, now.Day(), 1).Err() if nil != err { fmt.Printf("插入失败 err:%v", err) } //3.获取当天开始时间戳与本月最后一天的结束时间戳,计算实现时间 startTime, _ := GetTimeStartAndEnd(now) lastTime := now.AddDate(0, 1, -1) _, endTime := GetTimeStartAndEnd(lastTime) var duration_Seconds time.Duration = time.Duration(endTime-startTime) * time.Second //4.设置失效时间 err = redisDb.Expire(key, duration_Seconds).Err() if nil != err { fmt.Printf("设置失效时间异常 err: %v", err) } //5.统计指定key的访问量(bitcount也就是统计指定key的valu为1的数量) r, err := redisDb.Do("bitcount", key).Result() if nil != err { fmt.Printf("查询失败 err:%v", err) return } fmt.Printf("查询结果为 result:%v", r) } //获取指定time的开始时间戳与结束时间戳 func GetTimeStartAndEnd(NowTime time.Time) (int64, int64) { var startTime time.Time //NowTime := time.Date(2022, 9, 15, 0, 0, 0, 0, time.Local) if NowTime.Hour() == 0 && NowTime.Minute() == 0 && NowTime.Second() == 0 { startTime = time.Unix(NowTime.Unix()-86399, 0) //当天的最后一秒 } else { startTime = time.Unix(NowTime.Unix()-86400, 0) } currentYear := startTime.Year() currentMonth := startTime.Month() currentDay := startTime.Day() yesterdayStartTime := time.Date(currentYear, currentMonth, currentDay, 0, 0, 0, 0, time.Local).Unix() yesterdayEndTime := time.Date(currentYear, currentMonth, currentDay, 23, 59, 59, 0, time.Local).Unix() return yesterdayStartTime, yesterdayEndTime }
- 执行go get 命令拉取go-redis
- 读取配置参数调用NewClient()获取redis连接
import ( "lmcd_siteserver/config" "lmcd_siteserver/log" "os" "github.com/go-redis/redis" ) var ( rClient = New() ) func New() *redis.Client { log.TraceLog("Redis", "connect redis ...") address, err := config.Conf.GetValue("redis", "address") if err != nil { log.ErrorLog("Redis", "Load redis Error: read config value failed redis address ") return nil } password, err := config.Conf.GetValue("redis", "password") if err != nil { log.ErrorLog("Redis", "Load redis Error: read config value failed redis password ") return nil } database, err := config.Conf.Int("redis", "database") if err != nil { log.ErrorLog("Redis", "Load redis Error: read config value failed redis password ") return nil } client := redis.NewClient(&redis.Options{ Addr: address, Password: password, DB: database, }) if client == nil { log.ErrorLog("Redis", "open redis failed %s ", address) os.Exit(0) } pong, err := client.Ping().Result() if pong == "PONG" { log.TraceLog("Redis", "connect redis OK") } else { log.ErrorLog("Redis", "connect redis failed :"+err.Error()) //os.Exit(0) return nil } return client }
import ( "lmcd_siteserver/lmerror" "time" ) func Set(key, value string, timeout int64) (lmerr *lmerror.LmError) { client := rClient if client == nil { return lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:Set Error "+key+" | "+value) } var duration_Seconds time.Duration = time.Duration(timeout) * time.Second err := client.Set(key, value, duration_Seconds).Err() if err != nil { return lmerror.NewLmError(lmerror.REDIS_SET_ERROR, "redisdatabase:Set Error "+key+" | "+value+" | "+err.Error()) } return nil } func Get(key string) (value string, lmerr *lmerror.LmError) { client := rClient if client == nil { return "", lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:Set Error "+key) } searchvalue, err := client.Get(key).Result() if err != nil { return "", lmerror.NewLmError(lmerror.REDIS_GET_ERROR, "redisdatabase:Get Error "+key+" | "+err.Error()) } return searchvalue, nil } func HGet(key string, index string) (value string, lmerr *lmerror.LmError) { client := rClient if client == nil { return "", lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:Set Error "+key) } result, err := client.HGet(key, index).Result() if err != nil { return "", lmerror.NewLmError(lmerror.REDIS_HGET_ERROR, "redisdatabase:Get Error "+key+" | "+err.Error()) } return result, nil } func HSet(key string, index string, value string, timeout int) (ret int, lmerr *lmerror.LmError) { client := rClient if client == nil { return 0, lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:HSet Error "+key) } n, err := client.Do("hset", key, index, value).Result() if err != nil { return 0, lmerror.NewLmError(lmerror.REDIS_HGET_ERROR, "redisdatabase:HSet Error "+key+" | "+err.Error()) } var duration_Seconds time.Duration = time.Duration(timeout) * time.Second err = client.Expire(key, duration_Seconds).Err() if err != nil { return 0, lmerror.NewLmError(lmerror.REDIS_SETTIME_ERROR, "redisdatabase:Expire Error "+key) } return int(n.(int64)), nil } func HGetAll(key string) (ret map[string]string, lmerr *lmerror.LmError) { client := rClient var result map[string]string if client == nil { return result, lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:Set Error "+key) } result, err := client.HGetAll(key).Result() if err != nil { return result, lmerror.NewLmError(lmerror.REDIS_HGET_ERROR, "redisdatabase:Get Error "+key+" | "+err.Error()) } return result, nil } func HMSet(key string, data map[string]interface{}, timeout int) (lmerr *lmerror.LmError) { client := rClient if client == nil { return lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:Set Error "+key) } err := client.HMSet(key, data).Err() if err != nil { return lmerror.NewLmError(lmerror.REDIS_HSET_ERROR, "redisdatabase:Get Error "+key+" | "+err.Error()) } var duration_Seconds time.Duration = time.Duration(timeout) * time.Second err = client.Expire(key, duration_Seconds).Err() if err != nil { return lmerror.NewLmError(lmerror.REDIS_SETTIME_ERROR, "redisdatabase:Expire Error "+key) } return nil } func Incr(key string) (int, *lmerror.LmError) { client := rClient if client == nil { return 0, lmerror.NewLmError(lmerror.REDIS_CONNECT_ERROR, "redisdatabase:Incr Error "+key) } ret, err := client.Incr(key).Result() if err != nil { client.Set(key, 10000, 0) return 0, nil } return int(ret), nil }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。