一、简单介绍
redis最主要的功能:
1.1 做缓存,一些经常查询的数据又不会修改的数据就可以放在redis中;
1.2 可以做redis队列,比如短信邮件可以放入到redis队列中。
1.3 redis-session共享
1.4 redis-锁
缺点:数据多了会消耗内存,
优点:但是查询数据库快,可以减轻数据库的压力。
首先redis有16个数据库,那么根据数据库的下标去区分数据到底存入到那个数据库。数据是保存在内存中的,
Redis是一个高效的内存数据库,他所支持包括 String、List 、Set、SotreSet 和Hash等数据类型的存储。
在redis通常根据key查询value值
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
二、redisRedsi缓存雪崩、缓存穿透、数据库和redis一致性等问题
2.1缓存雪鹏
定义:
当 redis服务挂掉时,大量请求数据库,对数据库产生巨大的压力,导致数据库瘫痪。
场景:
把所有存入redis的所有数据设置相同过期的时间,过期时间失效后,就会大量请求数据库。
如何解决?
1、在缓存的时候我们给过期时间设置一个随机数,但是也要根据业务场景需求来设置
2、事发前:实现redis的高可用、主从架构+sentinel 或者 redis cluster
3、事发后:万一redis真的挂了、可以设置本地缓存ehcache+限流hystrix
4、事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
2.2 缓存穿透:
定义:
大量到数据库请求一些不存在的数据,查询一个数据库不存在的数据。请求的数据在缓存大量不命中,导致大量请求数据库。
场景:
比如我们表的数据的id是从1开始的正数,如果在这里有黑客攻击我们的系统,会设置一些负数的id到数据库查询数据,
查询出来返回的数据为null,在这里,由于缓存不命中,并且处于容错考虑,从数据库查询的数据为null就不写到redis,
这将导致每次查询数据都会到数据库查询,失去了缓存的意义。这样数据库迟早也会挂掉
如何解决缓存穿透?
1、由于请求的参数是不合法(-1) 每次请求都是不存在的数据,于是我们可以使用布隆过滤器(BloomFilter) 或者 压缩filter提前拦截,
不合法就不能访问数据库。
2、当我们从数据库查询出来的数据为null时,也把他设置到缓存redis中,下次请求的时候就到redis中查询了,
在这里后台可以判断,如果为null,那么设置一个较短的过期时间,到期自动就失效,否则就是按正常操作。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
2.3 缓存与数据库双写一致性:
对于读操作流程:
先到redis缓存中查询数据,如果为null,那么再到数据库查询出来再设置到redis中去,最后将数据返回给请求。
定义:
如果只是简单查询,缓存数据和数据库数据没有什么问题,当我们更新的时候就可能导致数据库数据和缓存数据不一致了。
数据库库存为 999 缓存数据为1000 这就是更新导致的情况。
解决方案:
1、比如操作菜单的时候,当我们增加 、删除、修改菜单时,操作成功之后就应该立刻根据菜单的key从redis缓存中把数据给删除,
第二次查询 的时候肯定为null,从数据库查询再设置到redis中。这是马上见效情况,
2、不是马上见效的情况,就是设置过期时间来确定,比如我们商城中web页面根据店铺搜索出来的数据有最新的4张照片
当我们在商家后台添加一个商品时,就应该显示在最新添加的照片,此时就不能按照删除key来操作redis了,因为多个商家添加多个商品,
就失去了缓存的意义,那么会根据用户需求来设置过期时间,这里的redis缓存就可能和数据库不一致,需要过期时间来控制数据。
因为缓存时间到了,就会被删除,再到数据库查询设置到redis中去。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
2.3.1高并发情况下:
操作一:
先更新数据库,再删除缓存
正常的情况是这样的:
1、 先操作数据库,成功
2、再删除缓存,也成功
3、 如果原子性被破坏了
第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据。
如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。
如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:
缓存刚好失效
线程A查询数据库,得一个旧值
线程B将新值写入数据库
线程B删除缓存
线程A将查到的旧值写入缓存
要达成上述情况,还是说一句概率特别低:
因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,
而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率
基本并不大。对于这种策略,其实是一种设计模式:Cache Aside Pattern
解决一:
删除缓存失败的解决思路:
将需要删除的key发送到消息队列中
自己消费消息,获得需要删除的key
不断重试删除操作,直到成功
操作二:
先删除缓存,再更新数据库
正常情况是这样的:
1、先删除缓存,成功;
2、再更新数据库,也成功;
3、如果原子性被破坏了:
第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的。
如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的。
看起来是很美好,但是我们在并发场景下分析一下,就知道还是有问题的了:
线程A删除了缓存
线程B查询,发现缓存已不存在
线程B去数据库查询得到旧值
线程B将旧值写入缓存
线程A将新值写入数据库
所以也会导致数据库和缓存不一致的问题。
解决二:
并发下解决数据库与缓存不一致的思路:
将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
对比两种策略:
先删除缓存、再更新数据库
在高并发下表现的不如意,在原子性被破环时表现优异
先更新数据库,再删除缓存(Cache Aside Pattern设计模式))
在高并发先表现优异,在原子性被破坏时表现不如意
- 1
- 2
- 3
- 4
- 5
- 6
三、redis分页加排序操作
介绍:
Redis是一个高效的内存数据库,他所支持包括 String、List 、Set、SotreSet 和Hash登数据类型的存储。在redis通常根据key查询
value值,redis没有条件查询,在面对一些需要分页或者排序的场景时(培训 时间线)redis就不太好处理。
项目中案列使用:
需要将每个主题下的用户的评论组装好写到redis中,每个主题会有一个topicId,每一条评论会和topicid关联起来,
得到的大致数据模型为{ topicId: 'xxxxxxxx', comments: [ { username: 'niuniu', createDate: 1447747334791,
content: '在Redis中分页', commentId: 'xxxxxxx', reply: [ { content: 'yyyyyy' username: 'niuniu' }, ... ] }, ... ]}
将评论数据从mysql查询出来组装好存入到redis后,从上面的数据可以看出都是key value形式,所以会使用到hash
进行存储, hash肯定是不能分页的,那么可能会用到StoreSet
五大类型介绍:
1、String:主要用于存储字符串、显然不支持分页排序
2、Hash: 主要用于存储key-value形式数据,评论模型中全是key-value,所以会使用到
3、Set: 主要存储无序集合 无序 排除
4、stortSet :主要存储有序集合,StortSet添加元素指令Zadd key score member [[score , member] ]
会给每个元素的member绑定一个用于排序的值score , StroreSet会很据score的值的大小进行排序,
在这里可以将一个需要排序的字段当作score排序,StoreSet指令中的Zervrange key start stop由可以返回指定区间的成员,
意思就是可以用来分页 , 还有一个好处就是SortedSet的指令Zerm key member 可以根据key移除指定的成员,
可以满足删除评论的要求,所以 SortedSet是用来分页 的。
5、List: 主要用来存储一个列表,列表中的每一个元素按元素的插入时的顺序进行保存,如果我们将评论模型按字段(排好)放入redis后再
插入list中,就可以做到排序,但是其中数据删掉就乱了,list中的lrange key start stop 指令还能做分页,
那么单纯使用list也能做到分页排序了,但是在这里,如果评论被删除,就需要到数据库重新查询一次放入到redis中,
这样的话性能也不好,而且也不太优雅,就需要更新redis中的数据了,如果在这里可以删除指定的数据那就更好了,
但是list中有lpop,rpop这两个指令,他们只能删除列的表头和表尾的数据,不能指定删除,所以在这里list也不太好,
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
下图所示:
在上图的SortSet结构中将每个主题的topicId作为set的key,将与该主题关联的评论的createDate和commentId分别作为set的score
和member,commentId的顺序就根据createDate的大小进行排列。
当需要查询某个主题某一页的评论时,就可主题的topicId通过指令zrevrange topicId (page-1)×10 (page-1)×10+perPage这样就能
找出某个主题下某一页的按时间排好顺序的所有评论的commintId。page为查询第几页的页码,perPage为每页显示的条数。
当找到所有评论的commentId后,就可以把这些commentId作为key去Hash结构中去查询该条评论对应的内容。
这样就利用SortSet和Hash两种结构在Redis中达到了分页和排序的目的。
- 1
- 2
- 3
- 4
- 5
- 6
四、redis-session操作
四、大家最喜欢的代码操作:
4.1 properties中的配置:
redis.host=127.0.0.1 本机地址 需要开启
redis.port=6379 redis的端口号
redis.database=15 redis数据库的下标,有了这个操作就是把数据存入到下标为15的数据库中
redis.password=20162016 redis密码
redis.timeout=3000 超时时间
- 1
- 2
- 3
- 4
- 5
4.2 applicationContext.xml中的配置:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
4.2 导包pom.xml:
redis.clients
jedis
2.9.0
- 1
- 2
- 3
- 4
- 5
五、java代码
5.1 、工具类
@Component
public class RedisHelper {
@Autowired
private JedisPool jedisPool; //如果注入这个就是默认把数据存入到redis的第一个数据库
@Autowired
private JedisPool jedisPool1; //如果注入这个就是把数据存入到redis的16个数据库 下标为15
/**
* cache前缀
*/
@Value("${cachePrefix}") //这个写一个前缀 。类似于一个文件夹 在RedisDesktopManager 可视化工具中可以查看
private String cachePrefix;
/**
* 根据key获取缓存数据
*
* @param key
* @return
*/
public String get(String key) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.get(cachePrefix + key);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 获取指定前缀的所有key
*
* @param prefix
* @return
*/
public Set keys(String prefix) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.keys(cachePrefix + prefix);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 根据key设置缓存数据,如果以前存在更新,如果以前没有添加
*
* @param key
* @param value
*/
public void set(String key, String value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(cachePrefix + key, value);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 根据key设置缓存数据,如果以前存在更新,如果以前没有添加
*
* @param key
* @param value
* @param expire 过期时间,单位秒
*/
public void set(String key, String value, int expire) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(cachePrefix + key, value);
jedis.expire(cachePrefix + key, expire);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 根据key删除缓存数据
*
* @param key
*/
public void del(String key) {
Jedis jedis = jedisPool.getResource();
try {
jedis.del(cachePrefix + key);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 设置集合 这个的话简单介绍一下,他主要在我们的项目中和es结合使用的,比如订单和商品是发布的es中,因为es查询速度很快
* 那么可变参数 我们只需要把商品的id和订单的id存入到一个数组中,再调用此方法,到时候再es中根据redis中下面smembers这个方法
* 拿到所有的id,再到数据库查询出来一个对象存入到es中,到时候商品和订单就是从数据库查询出来的。
* @param name
* @param value
*/
public void sadd(String name, String... value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.sadd(cachePrefix + name, value);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 获取集合
* @param name
*/
public void scard(String name){
Jedis jedis = jedisPool.getResource();
try {
Long scard = jedis.scard(cachePrefix + name);
System.out.println("scard=="+scard);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 获取指定键名的集合中的所有成员
*
* @param name
* @return
*/
public Set smembers(String name) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.smembers(cachePrefix + name);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 返回集合中的随机元素
*
* @param name
* @param count
* @return
*/
public List srandmember(String name, int count) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.srandmember(cachePrefix + name, count);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 移除集合
*
* @param name
* @return
*/
public void spop(String name) {
Jedis jedis = jedisPool.getResource();
try {
jedis.spop(cachePrefix + name);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 移除集合中的一个或多个元素
*
* @param name
* @return
*/
public void srem(String name, String... key) {
Jedis jedis = jedisPool.getResource();
try {
jedis.srem(cachePrefix + name, key);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 添加到hash
*
* @param name
* @param key
* @param value
*/
public void hset(String name, String key, String value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.hset(cachePrefix + name, key, value);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 从hash中读取
*
* @param name
* @param key
* @return
*/
public String hget(String name, String key) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.hget(cachePrefix + name, key);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 从hash中删除
*
* @param name
* @param key
*/
public void hdel(String name, String key) {
Jedis jedis = jedisPool.getResource();
try {
jedis.hdel(cachePrefix + name, key);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 设置key的过期时间
* @param name
* @param seconds
*/
public void expire(String name, int seconds){
Jedis jedis = jedisPool.getResource();
try {
jedis.expire(cachePrefix + name, seconds);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 检查键是否存在
* @param name
* @return
*/
public Boolean exists(String name){
Jedis jedis = jedisPool.getResource();
try {
return jedis.exists(cachePrefix + name);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 1
5.2、redis队列代码
@Component
public class QueueHelper {
@Autowired
private JedisPool jedisPool;
/**
* Queue前缀
*/
@Value("${queuePrefix}")
private String prefix;
/**
* 读取队列
* @return
*/
public String pop(String name) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.lpop(prefix + name);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
/**
* 写入队列 简单介绍一下:value就是我们发送短信的对象,比如把电话号码 、发布的内容、封转到一个对象中,再把此对象转换为json字符串的 格式传进来,到时候再定时任务中再调上面那个方法查询出来,再去调发布短信和邮件的方法就可以了,注意:此时的定时任务需要配置为一直运行。
*
* @param value
*/
public void push(String name, String value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.rpush(prefix + name, value);
} finally {
jedisPool.returnResourceObject(jedis);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
5.3、redis锁
package net.shopnc.b2b2c.lbjt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Collections;
@Component
public class SaveGoodsIdToRedisHelper {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
private static String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//在这里 redis锁的单位是毫秒
public static final int EXPIRATIONTIME = 180000;
@Autowired
private JedisPool jedisPool2;
/**
* Queue前缀
*/
@Value("${queuePrefix}")
private String prefix;
/**
* 检查键是否存在
*
* @param name
* @return
*/
public Boolean exists(String name) {
Jedis jedis = jedisPool2.getResource();
try {
return jedis.exists(prefix + name);
} finally {
jedisPool2.returnResourceObject(jedis);
}
}
//设置订单锁
public void setOrderLock(String name, String requestId) {
Jedis jedis = jedisPool2.getResource();
try {
String set = jedis.set(name, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRATIONTIME);
// System.out.println(set);
} finally {
jedisPool2.returnResourceObject(jedis);
}
}
//判断key是否存在
public String getOrderValue(String name) {
Jedis jedis = jedisPool2.getResource();
try {
String s = jedis.get(name);
return s;
} finally {
jedisPool2.returnResourceObject(jedis);
}
}
//订单解锁
public String getOrderLock(String name, String value) {
Jedis jedis = jedisPool2.getResource();
try {
Object eval = jedis.eval(script, Collections.singletonList(name), Collections.singletonList(value));
return "";
} finally {
jedisPool2.returnResourceObject(jedis);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
5.4、redis分页排序
//redis分页
public List redisPageSize(String key) {
int pageNo = 2;
int pageSize = 2;
Jedis jedis = jedisPool.getResource();
try {
int start = pageSize * (pageNo - 1); // 因为redis中list元素位置基数是0 0 6
int end = start + pageSize - 1; //5 11 指的是下标
List results = jedis.lrange(key, start, end);// 从start算起,start算一个元素,到结束那个元素
for (String str : results) {
System.out.println("str====" + str);
}
return results;
} finally {
jedisPool.returnResourceObject(jedis);
}
}
//redis分页+排序
public void redisQueryPageAndSort() {
Jedis jedis = jedisPool1.getResource();
Map map = new HashMap();
for(int i=0 ; i<10 ; i++){
jedis.zadd("topicId", i, "name" + i);
map.put("name"+i , "到底怎么回事"+i);
}
jedis.hmset("user", map);
int currentPage =1;
int pageSize = 10 ;
int offset = (currentPage-1)*10;
LinkedHashSet sets = (LinkedHashSet) jedis.zrevrangeByScore("topicId" ,"80", "1", offset, pageSize);
String s1 = sets.toString();
System.out.println(sets.toString());
Object[] objects = sets.toArray();
String[] strings = new String[sets.size()];
for(int i=0 ; i 注意结果是一个泛型的List
//第一个参数是存入redis中map对象的key,后面跟的是放入map中的对象的key,后面的key可以跟多个,是可变参数
List rsmap = jedis.hmget("user", strings);
for (String s : rsmap) {
System.out.println(s);
}
System.out.println(rsmap);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
评论记录:
回复评论: