标签 : Java与NoSQL
Redis事务(<code>transaction</code>)是一组命令的集合,同命令一样也是Redis的最小执行单位, Redis保证一个事务内的命令执行不被其他命令影响.
事务操作
MySQL
Redis
开启
<code>start transaction</code>
<code>MULTI</code>
语句
DML
普通命令
取消
<code>rollback</code>
<code>DISCARD</code>
执行
<code>commit</code>
<code>EXEC</code>
MySQL的<code>rollback</code>与Redis的<code>DISCARD</code>有一定的区别.
假设现在已经成功执行了事务内的前2条语句, 第3条语句出错:
MySQL<code>rollback</code>后,前2条的语句影响消失.
Redis可以分为两种情况:
语法错误: 事务中断, 所有语句均得不到执行;
运行错误: (如语法正确,但适用数据类型不对: 像<code>ZADD</code>操作<code>List</code>), <code>EXEC</code>会执行前2条语句, 并跳过第3条语句.
这样的部分成功会导致数据不一致, 而这一点需要由开发人员负责, 比如提前规划好缓存<code>key</code>的设计.
悲观锁(Pessimistic Lock): 很悲观,每次读写数据都认为别人会修改,所以每次读数据都会上锁,这样如果别人也想读写这条数据就会阻塞, 直到加锁的人把锁释放. 传统的RDBMS中用到了很多这种锁机制, 如行锁、表锁、读锁、写锁等. 乐观锁(Optimistic Lock): 顾名思义非常乐观, 每次读写数据时候都认为别人不会修改,所以不再上锁,但在更新数据时会判断一下在此期间有没有人更新了这条数据, 这个判断过程可以使用<code>版本号</code>等机制实现, 而Redis默认就对乐观锁提供了支持 –<code>WATCH</code>命令.
<code>WATCH</code>命令可以监控一个/多个<code>key</code>, 一旦其中有一个被修改/删除, 则之后的事务就不会执行,如用<code>WATCH</code>命令来模拟抢票场景:
<code>WATCH</code>命令的作用只是当被监控的<code>key</code>值修改后阻止事务执行,并不能阻止其他Client修改. 所以一旦<code>EXEC</code>执行失败, 可以重新执行整个方法或使用<code>UNWATCH</code>命令取消监控.
乐观锁适用于读多写少情景,即冲突真的很少发生,这样可以省去大量锁的开销. 但如果经常产生冲突,上层应用需要不断的retry,反倒是降低了性能,所以这种情况悲观锁比较适用.
Redis可以使用<code>EXPIRE</code>命令设置<code>key</code>的过期时间, 到期后Redis会自动删除它.
命令
作用
<code>EXPIRE key seconds</code>
Set a timeout on key.
<code>TTL key</code>
Get the time to live for a key
<code>PERSIST key</code>
Remove the expiration for a key
除了<code>PERSIST</code>命令之外,<code>SET</code>/<code>GETSET</code>为<code>key</code>赋值的同时也会清除<code>key</code>的过期时间.另外如果<code>WATCH</code>监控了一个拥有过期时间的<code>key</code>,<code>key</code>到期自动删除并不会被<code>WATCH</code>认为该<code>key</code>被修改.
缓存DB数据
当服务器内存有限时,如果大量使用缓存而且过期时间较长会导致Redis占满内存; 另一方面为了防止占用内存过大而设置过期时间过短, 则有可能导致缓存命中率过低而使系统整体性能下降.因此为缓存设计一个合理的过期时间是很纠结的, 在Redis中可以限制能够使用的最大内存,并让Redis按照一定规则的淘汰不再需要的<code>key</code>: 修改<code>maxmemory</code>参数,当超过限制会依据<code>maxmemory-policy</code>参数指定的策略来删除不需要的<code>key</code>:
<code>maxmemory-policy</code>
规则说明
<code>volatile-lru</code>
只对设置了过期时间的key使用LRU算法删除
<code>allkey-lru</code>
使用LRU删除一个key
<code>volatile-random</code>
只对设置了过期时间的key随机删除一个key
<code>allkey-random</code>
随机删除一个key
<code>volatile-ttl</code>
删除过期时间最近的一个key
<code>noevication</code>
不删除key, 只返回错误(默认)
Redis的<code>SORT</code>命令可以对<code>List</code>、<code>Set</code>、<code>Sorted-Set</code>类型排序, 并且可以完成与RDBMS 连接查询 类似的任务:
参数
描述
<code>ALPHA</code>
<code>SORT</code>默认会将所有元素转换成双精度浮点数比较,无法转换则会提示错误,而使用<code>ALPHA</code>参数可实现按字典序比较.
<code>DESC</code>
降序排序(<code>SORT</code>默认升序排序).
<code>LIMIT</code>
指定返回结果范围.
<code>STORE</code>
<code>SORT</code>默认直接返回排序结果, <code>STORE</code>可将排序后结果保存为<code>List</code>.
注: <code>SORT</code>在对<code>Sorted-Set</code>排序时会忽略元素分数,只针对元素自身值排序.
很多情况下<code>key</code>实际存储的是对象ID, 有时单纯对ID自身排序意义不大,这就用到了<code>BY</code>参数, 对ID关联的对象的某个属性进行排序:
<code>pattern</code>可以是字符串类型<code>key</code>或<code>Hash</code>类型<code>key</code>的某个字段(表示为键名 -> 字段名).如果提供了<code>BY</code>参数, <code>SORT</code>将使用ID值替换参考<code>key</code>中的第一个<code>*</code>并获取其值,然后根据该值对元素排序.
注意:
当<code>pattern</code>不包含<code>*</code>时, <code>SORT</code>将不会执行排序操作;
当ID元素的参考<code>key</code>不存在时,默认设置为0;
如果几个ID元素的<code>pattern</code>值相同,则会再比较元素本身值排序.
<code>GET</code>参数不影响排序过程,它的作用是使<code>SORT</code>返回结果不再是元素自身的值,而是<code>GET</code>参数指定的键值:
同<code>BY</code>一样, <code>GET</code>参数也支持<code>String</code>类型和<code>Hash</code>类型, 并使用<code>*</code>作为占位符.
注: <code>GET</code>参数获取自身值需要使用<code>#</code>: <code>GET #</code>
<code>SORT</code>的时间复杂度为<code>O(N+M*log(M))</code>:
所以开发过程中使用<code>SORT</code>需要注意:
尽可能减小待排序<code>key</code>中元素数量(减小<code>N</code>);
使用<code>LIMIT</code>参数限制结果集大小(减小<code>M</code>);
如果待排序数据量较大,尽可能使用<code>STORE</code>将结果缓存.
消息队列就是”传递消息的队列”,与消息队列进行交互的实体有两类, 一是生产者: 将需要处理的消息放入队列; 一是消费者: 不断从消息队列中读出消息并处理.
<dl></dl>
<dt>使用消息队列有如下好处:</dt>
<dd></dd>
松耦合: 生产者和消费者无需知道彼此的实现细节, 只需按照协商好的消息格式读/写, 即可实现不同进程间通信,这就使得生产者和消费者可以由不同的团队使用不同的开发语言编写.
易扩展: 消费者可以有多个,且可以分布在不同的Server中, 降低单台Server负载, 横向扩展业务.
Redis提供了<code>BRPOP</code>/<code>BLPOP</code>命令来实现消息队列:
<code>BRPOP key [key ...] timeout</code>
Remove and get the last element in a list, or block until one is available
<code>BLPOP key [key ...] timeout</code>
Remove and get the first element in a list, or block until one is available
<code>BRPOPLPUSH source destination timeout</code>
Pop a value from a list, push it to another list and return it; or block until one is available
注: 若Redis同时监听多个<code>key</code>, 且每个<code>key</code>均有元素可取,则Redis按照从左到右的顺序去挨个读取<code>key</code>的第一个元素.
前面的<code>BRPOP</code>/<code>BLPOP</code>实现的消息队列有一个限制: 如果一个队列被多个消费者监听, 生产者发布一条消息只会被其中一个消费者获取. 因此Redis还提供了一组命令实现“发布/订阅”模式, 同样可用于进程间通信:
“发布/订阅”模式也包含两种角色: 发布者与订阅者. 订阅者可以订阅一个/多个频道, 而发布者可向指定频道发送消息, 所有订阅此频道的订阅者都会收到此消息.
<code>PUBLISH channel message</code>
Post a message to a channel
<code>SUBSCRIBE channel [channel ...]</code>
Listen for messages published to the given channels
<code>UNSUBSCRIBE [channel [channel ...]]</code>
Stop listening for messages posted to the given channels
<code>PSUBSCRIBE pattern [pattern ...]</code>
Listen for messages published to channels matching the given patterns
<code>PUNSUBSCRIBE [pattern [pattern ...]]</code>
Stop listening for messages posted to channels matching the given patterns
MessagesQueue
注: 发送的消息不会持久化,一个订阅者只能接收到后续发布的消息,之前发送的消息就接收不到了.
Redis支持两种持久化方式: RDB与AOF. RDB: Redis根据指定的规则“定时”将内存数据快照到硬盘; AOF:Redis在每次执行命令后将命令本身记录下来存放到硬盘.两种持久化方式可结合使用.
快照执行过程: Redis使用<code>fork()</code>函数复制一份当前进程副本; 父进程继续接收并处理客户端请求, 而子进程将所有内存数据写入磁盘临时文件; 当子进程将所有数据写完会用该临时文件替换旧的RDB文件, 至此一次快照完成(可以看到自始至终RDB文件都是完整的).
Redis会在以下几种情况下对数据进行快照:
根据配置规则
配置由两个参数构成: 时间窗口<code>M</code>和改动<code>key</code>个数<code>N</code>; 当时间<code>M</code>内被改动的<code>key</code>的个数大于<code>N</code>时, 即符合自动快照条件:
用户执行<code>SAVE</code>/<code>BGSAVE</code>/<code>FLUSHALL</code>命令:
除了让Redis自动快照, 当进行服务重启/手动迁移以及备份时也需要我们手动执行快照.
<code>SAVE</code>
<code>SAVE</code>命令会使Redis同步地执行快照操作(过程中会阻塞所有来自客户端的请求, 因此尽量避免线上使用)
<code>BGSAVE</code>
在后台异步执行快照操作,Redis还可继续响应请求
<code>FLUSHALL</code>
<code>FLUSHALL</code>会清空所有数据,无论是否触发了自动快照条件(只要有配置了),Redis都会执行一次快照
<code>LASTSAVE</code>
获取最近一次成功执行快照时间
执行复制
当设置了主从模式, Redis会在复制初始化时执行快照,即使没有配置自动快照条件.
通过RDB方式实现持久化, Redis在启动后会读取RDB快照文件, 将数据从硬盘导入内存, 但如果在持久化过程中Redis异常退出, 就会丢失最后一次快照以后更改的所有数据.
AOF将Redis执行的每一条命令追加到硬盘文件中.然后在启动Redis时逐条执行AOF文件中的命令将数据载入内存.
Redis默认没有开启AOF, 需要以如下参数启用:
开启AOF后, Redis会将每一条有可能更改数据的命令写入AOF文件,这样就导致AOF文件越来越大,即使有可能内存中实际存储的数据并没多少. 因此Redis每当达到一定条件就自动重写AOF文件,这个条件可以在配置文件中设置:
此外, 我们还可以使用<code>BGREWRITEAOF</code>命令手动执行AOF重写.
执行AOF持久化时, 由于操作系统缓存机制, 数据并没有真正写入磁盘,而是进入了磁盘缓存, 默认情况下系统每30S执行一次同步操作, 将缓存内容真正写入磁盘, 如果在这30S的系统异常退出则会导致磁盘缓存数据丢失, 如果应用无法忍受这样的损失, 可通过<code>appendfsync</code>参数设置同步机制:
复制(replication)中,Redis的角色可以分为两类, Master:可以执行读/写操作,当写操作导致数据修改时会自动将数据同步给Slave; Slave:一般是只读的,并接受Master同步过来的数据(Slave自身也可以作为Master存在, 如图):
replication复制时序
Slave启动后向Master发送<code>SYNC</code>命令;Master收到后在后台保存RDB快照, 并将快照期间接收到的所有命令缓存.
快照执行完, Master将快照文件与所有缓存的命令发送给Slave;
Slave接收并载入快照, 然后执行所有收到的缓存命令,这一过程称为复制初始化.
复制初始化完成后,Master每接收到写命令就同步给Slave,从而保证主从数据一致.
通过Redis的复制功能可以实现以下应用:
读写分离:
通过复制可实现读写分离, 以提高服务器的负载能力, 可以通过复制建立多个Slave节点, Master只进行写操作, 而由Slave负责读操作, 这种一主多从的结构很适合读多写少的场景.
Slave持久化
持久化是一个相对耗时的操作, 为了提高性能, 可以通过复制功能建立一个/多个Slave, 并在Salve中启用持久化, Master禁用持久化. 当Master崩溃后:
在Slave使用<code>SLAVEOF NO ONE</code>命令将Slave提升成Master继续服务;
启用之前崩溃的Master, 然后使用<code>SLAVEOF</code>将其设置为新Master的Slave, 即可将数据同步回来.
注意: 当开启复制且Master关闭持久化时, Master崩溃后一定不能直接重启Master, 这是因为当Master重启后, 因为没有开启持久化, 所以Redis内的所有数据都会被清空, 这时Salve从Master接受数据, 所有的Slave也会被清空, 导致Slave持久化失去意义.
当Master遭遇异常中断服务后, 需要手动选择一个Slave升级为Master, 以使系统能够继续提供服务. 然而整个过程相对麻烦且需要人工介入, 难以实现自动化. 为此Redis提供了哨兵Sentinel.
Sentinel哨兵是Redis高可用性解决方案之一: 由一个/多个Sentinel实例组成的Sentinel系统可以监视任意多个Master以及下属Slave, 并在监控到Master进入下线状态时, 自动将其某个Slave提升为新的Master, 然后由新的Master代替已下线的Master继续处理命令请求.
如图: 若此时Master:server1进入下线状态, 那么Slave: server2,server3,server4对Master的复制将被迫中止,并且Sentinel系统也会察觉到server1已下线, 当下线时长超过用户设定的下线时长时, Sentinel系统就会对server1执行故障转移操作: Sentinel会挑选server1下属的其中一台Slave, 将其提升为新Master; 然后Sentinel向server1下属的所有Slave发送新的复制指令,让他们成为新Master的Salve, 当所有Salve都开始复制新Master时, 故障转移操作完成. 另外, Sentinel还会继续监视已下线的server1, 并在他重新上线时, 将其设置为新Master的Slave.
Cluster是Redis提供的另一高可用性解决方案:Redis集群通过分片(sharding)来进行数据共享, 并提供复制与故障转移功能.
一个 Redis 集群通常由多个节点组成, 最初每个节点都是相互独立的,要组建一个真正可工作的集群, 必须将各个独立的节点连接起来.连接各个节点的工作可以使用<code>CLUSTER MEET</code>命令完成:
向一个节点发送<code>CLUSTER MEET</code>命令,可以使其与<code>ip</code>+<code>port</code>所指定的节点进行握手,当握手成功时, 就会将目标节点添加到当前节点所在的集群中.
案例
假设现在有三个独立的节点 127.0.0.1:7000 、 127.0.0.1:7001 、 127.0.0.1:7002:
通过向节点 7000 发送<code>CLUSTER MEET 127.0.0.1 7001</code>命令,可将节点7001添加到节点7000所在的集群中:
继续向节点7000发送<code>CLUSTER MEET 127.0.0.1 7002</code>命令,同样也可将节点7002也拉进来:
至此, 握手成功的三个节点处于同一个集群:
通过在配置文件中使用<code>requirepass</code>参数可为Redis设置密码:
这样客户端每次连接都需要发送密码,否则Redis拒绝执行客户端命令:
Redis支持在配置文件中将命令重命名, 以保证只有自己的应用可以使用该命令:
如果希望禁用某个命令,可将命令重命名为空字符串.
<code>SLOWLOG</code>
当一条命令执行超过时间限制时,Redis会将其执行时间等信息加入耗时统计日志, 超时时间等可通过以下配置实现:
<code>MONITOR</code> : 监控Redis执行的所有命令
其他常用管理工具
<dt>参考&拓展</dt>