[关闭]
@1234567890 2017-05-22T13:30:13.000000Z 字数 11090 阅读 1365

Redis

redis


总结

字符串的增删改查

  1. #增加一个key为ay_key的值
  2. 127.0.0.1:6379> set ay_key "ay"
  3. OK
  4. #查询ay_key的值
  5. 127.0.0.1:6379> get ay_key
  6. "ay"
  7. #修改ay_key的值
  8. 127.0.0.1:6379> set ay_key "new_ay"
  9. OK
  10. 127.0.0.1:6379> get ay_key
  11. "new_ay"
  12. #修改ay_key名称
  13. 127.0.0.1:6379> rename ay_key new_ay_key
  14. OK
  15. 127.0.0.1:6379> keys *
  16. 1) "new_ay_key"
  17. #删除ay_key
  18. 127.0.0.1:6379> del ay_key
  19. (integer) 0
  20. #查询是否存在ay_key 0
  21. 127.0.0.1:6379> exists ay_key
  22. (integer) 0

Set集合的增删改查

  1. #删除当前选择数据库中的所有key
  2. 127.0.0.1:6379> flushdb
  3. OK
  4. #生成set集合,添加4个数据
  5. 127.0.0.1:6379> sadd set_ay_key "ay" "al" "xy" "xl"
  6. (integer) 4
  7. #查询set里面所有值
  8. 127.0.0.1:6379> smembers set_ay_key
  9. 1) "xy"
  10. 2) "al"
  11. 3) "ay"
  12. 4) "xl"
  13. #删除value为"xl" , 返回 1 如果没有返回 0
  14. 127.0.0.1:6379> srem set_ay_key "xl"
  15. (integer) 1
  16. 127.0.0.1:6379> smembers set_ay_key
  17. 1) "xy"
  18. 2) "al"
  19. 3) "ay"
  20. #添加value为"xl"
  21. 127.0.0.1:6379> sadd set_ay_key "xl"
  22. (integer) 1
  23. 127.0.0.1:6379> smembers set_ay_key
  24. 1) "xy"
  25. 2) "al"
  26. 3) "ay"
  27. 4) "xl"
  28. #添加value为"xl" 添加不进去,但也不报错,set是不允许重复的
  29. 127.0.0.1:6379> sadd set_ay_key "xl"
  30. (integer) 0
  31. #不多解释
  32. 127.0.0.1:6379> sadd set_ay_key "xl"
  33. (integer) 0
  34. #不多解释
  35. 127.0.0.1:6379> sadd set_ay_key "xl"
  36. (integer) 0

List集合的增删改查

  1. #添加key为list_ay_key的list集合
  2. 127.0.0.1:6379> lpush list_ay_key "ay" "al" "xy" "xl"
  3. (integer) 4
  4. #查询key为list_ay_key的集合
  5. 127.0.0.1:6379> lrange list_ay_key 0 -1
  6. 1) "xl"
  7. 2) "xy"
  8. 3) "al"
  9. 4) "ay"
  10. #往list尾部添加元素
  11. 127.0.0.1:6379> rpush list_ay_key "together"
  12. (integer) 5
  13. #往list头部添加元素
  14. 127.0.0.1:6379> lpush list_ay_key "first"
  15. (integer) 6
  16. #查询list集合
  17. 127.0.0.1:6379> lrange list_ay_key 0 -1
  18. 1) "first"
  19. 2) "xl"
  20. 3) "xy"
  21. 4) "al"
  22. 5) "ay"
  23. 6) "together"
  24. #更新index为0的值
  25. 127.0.0.1:6379> lset list_ay_key 0 "update_first"
  26. OK
  27. 127.0.0.1:6379> lrange list_ay_key 0 -1
  28. 1) "update_first"
  29. 2) "xl"
  30. 3) "xy"
  31. 4) "al"
  32. 5) "ay"
  33. 6) "together"
  34. #删除index为1上的值
  35. 127.0.0.1:6379> lrem list_ay_key 1 "update_first"
  36. (integer) 1
  37. 127.0.0.1:6379> lrange list_ay_key 0 -1
  38. 1) "xl"
  39. 2) "xy"
  40. 3) "al"
  41. 4) "ay"
  42. 5) "together"

Hash集合(类似Java)的增删改查

  1. 127.0.0.1:6379> flushdb
  2. OK
  3. #生成hash集合,并添加key 为uuid_one value 为"12345"
  4. 127.0.0.1:6379> hset hash_ay_key "uuid_one" "12345"
  5. (integer) 1
  6. 127.0.0.1:6379> hlen hash_ay_key
  7. (integer) 1
  8. #返回集合所有的key
  9. 127.0.0.1:6379> hkeys hash_ay_key
  10. 1) "uuid_one"
  11. #返回集合所有value
  12. 127.0.0.1:6379> hvals hash_ay_key
  13. 1) "12345"
  14. #集合添加值
  15. 127.0.0.1:6379> hset hash_ay_key "uuid_two" "22222"
  16. (integer) 1
  17. #集合添加值
  18. 127.0.0.1:6379> hset hash_ay_key "uuid_three" "33333"
  19. (integer) 1
  20. #获得key为uuid_one的值
  21. 127.0.0.1:6379> hget hash_ay_key uuid_one
  22. "12345"
  23. #删除key为uuid_three的值
  24. 127.0.0.1:6379> hdel hash_ay_key uuid_three
  25. (integer) 1
  26. 127.0.0.1:6379> hkeys hash_ay_key
  27. 1) "uuid_one"
  28. 2) "uuid_two"
  29. #获得所有,包括key和value
  30. 127.0.0.1:6379> hgetall hash_ay_key
  31. 1) "uuid_one"
  32. 2) "12345"
  33. 3) "uuid_two"
  34. 4) "22222"
  35. #更新key为uuid_one的值
  36. 127.0.0.1:6379> hset hash_ay_key uuid_one "11111"
  37. (integer) 0
  38. 127.0.0.1:6379> hset hash_ay_key "uuid_one" "11111"
  39. (integer) 0
  40. 127.0.0.1:6379> hgetall hash_ay_key
  41. 1) "uuid_one"
  42. 2) "11111"
  43. 3) "uuid_two"
  44. 4) "22222"

SortedSet集合的增删改查

  1. #sorted set添加值ay 排序值为 1
  2. 127.0.0.1:6379> zadd zset_ay_key 1 "ay"
  3. (integer) 1
  4. 127.0.0.1:6379> zadd zset_ay_key 2 "al"
  5. (integer) 1
  6. 127.0.0.1:6379> zadd zset_ay_key 3 "xy"
  7. (integer) 1
  8. 127.0.0.1:6379> zadd zset_ay_key 4 "xl"
  9. (integer) 1
  10. #查询所有的值
  11. 127.0.0.1:6379> zrange zset_ay_key 0 -1
  12. 1) "ay"
  13. 2) "al"
  14. 3) "xy"
  15. 4) "xl"
  16. #删除所有的值
  17. 127.0.0.1:6379> zrem zet_ay_key "xl"
  18. (integer) 0
  19. 127.0.0.1:6379> zrange zset_ay_key 0 -1
  20. 1) "ay"
  21. 2) "al"
  22. 3) "xy"
  23. 4) "xl"

redis持久化 – RDB

RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
对于RDB方式,redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
虽然RDB有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么RDB方式就不太适合你,因为即使你每5分钟都持久化一次,当redis故障时,仍然会有近5分钟的数据丢失。所以,redis还提供了另一种持久化方式,那就是AOF。

redis持久化 – AOF

AOF,英文是Append Only File,即只允许追加不允许改写的文件。
如前面介绍的,AOF方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍,就这么简单。
我们通过配置redis.conf中的appendonly yes就可以打开AOF功能。如果有写操作(如SET等),redis就会被追加到AOF文件的末尾。
默认的AOF持久化策略是每秒钟fsync一次(fsync是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。
如果在追加日志时,恰好遇到磁盘空间满、inode满或断电等情况导致日志写入不完整,也没有关系,redis提供了redis-check-aof工具,可以用来进行日志修复。
因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,为此,redis提供了AOF文件重写(rewrite)机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。
在进行AOF重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响AOF文件的可用性,这点大家可以放心。
AOF方式的另一个好处,我们通过一个“场景再现”来说明。某同学在操作redis时,不小心执行了FLUSHALL,导致redis内存中的数据全部被清空了,这是很悲剧的事情。不过这也不是世界末日,只要redis配置了AOF持久化方式,且AOF文件还没有被重写(rewrite),我们就可以用最快的速度暂停redis并编辑AOF文件,将最后一行的FLUSHALL命令删除,然后重启redis,就可以恢复redis的所有数据到FLUSHALL之前的状态了。是不是很神奇,这就是AOF持久化方式的好处之一。但是如果AOF文件已经被重写了,那就无法通过这种方法来恢复数据了。
虽然优点多多,但AOF方式也同样存在缺陷,比如在同样数据规模的情况下,AOF文件要比RDB文件的体积大。而且,AOF方式的恢复速度也要慢于RDB方式。
如果你直接执行BGREWRITEAOF命令,那么redis会生成一个全新的AOF文件,其中便包括了可以恢复现有数据的最少的命令集。
如果运气比较差,AOF文件出现了被写坏的情况,也不必过分担忧,redis并不会贸然加载这个有问题的AOF文件,而是报错退出。这时可以通过以下步骤来修复出错的文件:
1.备份被写坏的AOF文件
2.运行redis-check-aof –fix进行修复
3.用diff -u来看下两个文件的差异,确认问题点
4.重启redis,加载修复后的AOF文件

redis持久化 – AOF重写

AOF重写的内部运行原理,我们有必要了解一下。
在重写即将开始之际,redis会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。
当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中了。

主从 – 用法

像MySQL一样,redis是支持主从同步的,而且也支持一主多从以及多级从结构。
主从结构,一是为了纯粹的冗余备份,二是为了提升读性能,比如很消耗性能的SORT就可以由从服务器来承担。
redis的主从同步是异步进行的,这意味着主从同步不会影响主逻辑,也不会降低redis的处理性能。
主从架构中,可以考虑关闭主服务器的数据持久化功能,只让从服务器进行持久化,这样可以提高主服务器的处理性能。
在主从架构中,从服务器通常被设置为只读模式,这样可以避免从服务器的数据被误修改。但是从服务器仍然可以接受CONFIG等指令,所以还是不应该将从服务器直接暴露到不安全的网络环境中。如果必须如此,那可以考虑给重要指令进行重命名,来避免命令被外人误执行。

主从 – 同步原理

从服务器会向主服务器发出SYNC指令,当主服务器接到此命令后,就会调用BGSAVE指令来创建一个子进程专门进行数据持久化工作,也就是将主服务器的数据写入RDB文件中。在数据持久化期间,主服务器将执行的写指令都缓存在内存中。
在BGSAVE指令执行完成后,主服务器会将持久化好的RDB文件发送给从服务器,从服务器接到此文件后会将其存储到磁盘上,然后再将其读取到内存中。这个动作完成后,主服务器会将这段时间缓存的写指令再以redis协议的格式发送给从服务器。
另外,要说的一点是,即使有多个从服务器同时发来SYNC指令,主服务器也只会执行一次BGSAVE,然后把持久化好的RDB文件发给多个下游。在redis2.8版本之前,如果从服务器与主服务器因某些原因断开连接的话,都会进行一次主从之间的全量的数据同步;而在2.8版本之后,redis支持了效率更高的增量同步策略,这大大降低了连接断开的恢复成本。
主服务器会在内存中维护一个缓冲区,缓冲区中存储着将要发给从服务器的内容。从服务器在与主服务器出现网络瞬断之后,从服务器会尝试再次与主服务器连接,一旦连接成功,从服务器就会把“希望同步的主服务器ID”和“希望请求的数据的偏移位置(replication offset)”发送出去。主服务器接收到这样的同步请求后,首先会验证主服务器ID是否和自己的ID匹配,其次会检查“请求的偏移位置”是否存在于自己的缓冲区中,如果两者都满足的话,主服务器就会向从服务器发送增量内容。

redis配置-通用

  1. //默认情况下,redis并不是以daemon形式来运行的。通过daemonize配置项可以控制redis的运行形式,如果改为yes,那么redis就会以daemon形式运行:
  2. daemonize no
  3. //当以daemon形式运行时,redis会生成一个pid文件,默认会生成在/var/run/redis.pid。当然,你可以通过pidfile来指定pid文件生成的位置,比如:
  4. pidfile /path/to/redis.pid
  5. //默认情况下,redis会响应本机所有可用网卡的连接请求。当然,redis允许你通过bind配置项来指定要绑定的IP
  6. bind 192.168.1.2 10.8.4.2
  7. //redis的默认服务端口是6379,你可以通过port配置项来修改。如果端口设置为0的话,redis便不会监听端口了。
  8. port 6379
  9. //当一个redis-client一直没有请求发向server端,那么server端有权主动关闭这个连接,可以通过timeout来设置“空闲超时时限”,0表示永不关闭。
  10. timeout 0
  11. //redis支持通过loglevel配置项设置日志等级,共分四级,即debug、verbose、notice、warning。
  12. loglevel notice
  13. //redis也支持通过logfile配置项来设置日志文件的生成位置。如果设置为空字符串,则redis会将日志输出到标准输出。假如你在daemon情况下将日志设置为输出到标准输出,则日志会被写到/dev/null中。
  14. logfile ""
  15. //对于redis来说,可以设置其数据库的总数量,假如你希望一个redis包含16个数据库.这16个数据库的编号将是0到15。默认的数据库是编号为0的数据库。用户可以使用select <DBid>来选择相应的数据库。那么设置如下:
  16. databases 16

redis配置 – 快照

  1. save 900 1 //表示每15分钟且至少有1个key改变,就触发一次持久化
  2. save 300 10 //表示每5分钟且至少有10个key改变,就触发一次持久化
  3. save 60 10000 //表示每60秒至少有10000个key改变,就触发一次持久化
  4. //如果你想禁用RDB持久化的策略,只要不设置任何save指令就可以,或者给save传入一个空字符串参数也可以达到相同效果,就像这样:
  5. save ""
  6. //如果用户开启了RDB快照功能,那么在redis持久化数据到磁盘时如果出现失败,默认情况下,redis会停止接受所有的写请求。这样做的好处在于可以让用户很明确的知道内存中的数据和磁盘上的数据已经存在不一致了。如果redis不顾这种不一致,一意孤行的继续接收写请求,就可能会引起一些灾难性的后果。如果下一次RDB持久化成功,redis会自动恢复接受写请求。当然,如果你不在乎这种数据不一致或者有其他的手段发现和控制这种不一致的话,你完全可以关闭这个功能,以便在快照写入失败时,也能确保redis继续接受新的写请求。配置项如下:
  7. stop-writes-on-bgsave-error yes
  8. //对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
  9. rdbcompression yes
  10. //在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果你希望获取到最大的性能提升,可以关闭此功能。
  11. rdbchecksum yes
  12. //我们还可以设置快照文件的名称,默认是这样配置的:
  13. dbfilename dump.rdb
  14. //最后,你还可以设置这个快照文件存放的路径。比如默认设置就是当前文件夹:
  15. dir ./

redis配置 – 复制

  1. //通过slaveof配置项可以控制某一个redis作为另一个redis的从服务器,通过指定IP和端口来定位到主redis的位置。一般情况下,我们会建议用户为从redis设置一个不同频率的快照持久化的周期,或者为从redis配置一个不同的服务端口等等。
  2. slaveof <masterip> <masterport>
  3. //如果主redis设置了验证密码的话(使用requirepass来设置),则在从redis的配置中要使用masterauth来设置校验密码,否则的话,主redis会拒绝从redis的访问请求。
  4. masterauth <master-password>
  5. //你可以控制一个从redis是否可以接受写请求。将数据直接写入从redis,一般只适用于那些生命周期非常短的数据,因为在主从同步时,这些临时数据就会被清理掉。自从redis2.6版本之后,默认从redis为只读。
  6. slave-read-only yes
  7. //当从redis失去了与主redis的连接,或者主从同步正在进行中时,redis该如何处理外部发来的访问请求呢?这里,从redis可以有两种选择:
  8. 第一种选择:如果slave-serve-stale-data设置为yes(默认),则从redis仍会继续响应客户端的读写请求。
  9. 第二种选择:如果slave-serve-stale-data设置为no,则从redis会对客户端的请求返回“SYNC with master in progress”,当然也有例外,当客户端发来INFO请求和SLAVEOF请求,从redis还是会进行处理。
  10. //只读的从redis并不适合直接暴露给不可信的客户端。为了尽量降低风险,可以使用rename-command指令来将一些可能有破坏力的命令重命名,避免外部直接调用。比如:
  11. rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
  12. //从redis会周期性的向主redis发出PING包。你可以通过repl_ping_slave_period指令来控制其周期。默认是10秒。
  13. repl-ping-slave-period 10
  14. 1.以从redis的角度来看,当有大规模IO传输时。
  15. 2.以从redis的角度来看,当数据传输或PING时,主redis超时
  16. 3.以主redis的角度来看,在回复从redisPING时,从redis超时
  17. //用户可以设置上述超时的时限,不过要确保这个时限比repl-ping-slave-period的值要大,否则每次主redis都会认为从redis超时。
  18. repl-timeout 60
  19. //我们可以控制在主从同步时是否禁用TCP_NODELAY。如果开启TCP_NODELAY,那么主redis会使用更少的TCP包和更少的带宽来向从redis传输数据。但是这可能会增加一些同步的延迟,大概会达到40毫秒左右。如果你关闭了TCP_NODELAY,那么数据同步的延迟时间会降低,但是会消耗更多的带宽。
  20. repl-disable-tcp-nodelay no
  21. //我们还可以设置同步队列长度。队列长度(backlog)是主redis中的一个缓冲区,在与从redis断开连接期间,主redis会用这个缓冲区来缓存应该发给从redis的数据。这样的话,当从redis重新连接上之后,就不必重新全量同步数据,只需要同步这部分增量数据即可。
  22. repl-backlog-size 1mb
  23. //我们可以给众多的从redis设置优先级,在主redis持续工作不正常的情况,优先级高的从redis将会升级为主redis。而编号越小,优先级越高。比如一个主redis有三个从redis,优先级编号分别为10、100、25,那么编号为10的从redis将会被首先选中升级为主redis。当优先级被设置为0时,这个从redis将永远也不会被选中。默认的优先级为100。
  24. slave-priority 100
  25. //假如主redis发现有超过M个从redis的连接延时大于N秒,那么主redis就停止接受外来的写请求。这是因为从redis一般会每秒钟都向主redis发出PING,而主redis会记录每一个从redis最近一次发来PING的时间点,所以主redis能够了解每一个从redis的运行情况。假如有大于等于3个从redis的连接延迟大于10秒,那么主redis就不再接受外部的写请求。上述两个配置中有一个被置为0,则这个特性将被关闭。默认情况下min-slaves-to-write为0,而min-slaves-max-lag为10。
  26. min-slaves-to-write 3
  27. min-slaves-max-lag 10

redis配置 – 安全

  1. //由于redis性能非常高,所以每秒钟可以完成多达15万次的密码尝试,所以你最好设置一个足够复杂的密码,否则很容易被黑客破解。
  2. 复制代码 代码如下:
  3. requirepass zhimakaimen
  4. //redis允许我们对redis指令进行更名,比如将一些比较危险的命令改个名字,避免被误执行。比如可以把CONFIG命令改成一个很复杂的名字,这样可以避免外部的调用,同时还可以满足内部调用的需要:
  5. rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c89

redis配置 -限制

  1. //我们可以设置redis同时可以与多少个客户端进行连接。默认情况下为10000个客户端。当你无法设置进程文件句柄限制时,redis会设置为当前的文件句柄限制值减去32,因为redis会为自身内部处理逻辑留一些句柄出来。如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。
  2. maxclients 10000
  3. //我们甚至可以设置redis可以使用的内存量
  4. maxmemory <bytes>
  5. //一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
  6. 1.volatile-lru:使用LRU算法移除过期集合中的key
  7. 2.allkeys-lru:使用LRU算法移除key
  8. 3.volatile-random:在过期集合中移除随机的key
  9. 4.allkeys-random:移除随机的key
  10. 5.volatile-ttl:移除那些TTL值最小的key,即那些最近才过期的key
  11. 6.noeviction:不进行移除。针对写操作,只是返回错误信息。
  12. maxmemory-policy volatile-lru

redis配置 – 追加模式

  1. //追加文件(Append Only File)是一种更好的保持数据一致性的方式。即使当服务器断电时,也仅会有1秒钟的写请求丢失,当redis进程出现问题且操作系统运行正常时,甚至只会丢失一条写请求。建议大家,AOF机制和RDB机制可以同时使用,不会有任何冲突。对于如何保持数据一致性的讨论
  2. appendonly no
  3. //我们还可以设置aof文件的名称:
  4. appendfilename "appendonly.aof"
  5. //设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes
  6. no-appendfsync-on-rewrite yes
  7. //fsync()调用,用来告诉操作系统立即将缓存的指令写入磁盘。一些操作系统会“立即”进行,而另外一些操作系统则会“尽快”进行。
  8. redis支持三种不同的模式:
  9. //1.no:不调用fsync()。而是让操作系统自行决定sync的时间。这种模式下,redis的性能会最快。
  10. //2.always:在每次写请求后都调用fsync()。这种模式下,redis会相对较慢,但数据最安全。
  11. //3.everysec:每秒钟调用一次fsync()。这是性能和安全的折衷。
  12. //默认情况下为everysec。
  13. appendfsync everysec
  14. //redis是这样工作的:redis会记录上次重写时的aof大小。假如redis自启动至今还没有进行过重写,那么启动时aof文件的大小会被作为基准值。这个基准值会和当前的aof大小进行比较。如果当前aof大小超出所设置的增长比例,则会触发重写。另外,你还需要设置一个最小大小,是为了防止在aof很小时就触发重写。
  15. auto-aof-rewrite-percentage 100
  16. auto-aof-rewrite-min-size 64mb
  17. auto-aof-rewrite-percentage0,则会关闭此重写功能。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注