阅读更多
1 使用redis有哪些好处
- 速度快:因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和删除的时间复杂度都是O(1)
- 支持丰富数据类型:支持string,list,set,sorted set,hash
- 支持事务:操作都是原子性的,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
2 Memcache与Redis的区别都有哪些
- 存储方式
- Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小
- Redis有部份存在硬盘上,这样能保证数据的持久性
- 数据支持类型
- Memcache对数据类型支持相对简单
- Redis有复杂的数据类型
- 使用底层模型不同
- 它们之间底层实现方式以及与客户端之间通信的应用协议不一样
- Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
3 redis常见性能问题和解决方案
- Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
- 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:
Master <- Slave1 <- Slave2 <- Slave3...
。这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变
4 redis的并发竞争问题如何解决
Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:
- 客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized
- 服务器角度,利用setnx实现锁
- 对于第一种,需要应用程序自己处理资源的同步,可以使用的方法比较通俗,可以使用synchronized也可以使用lock;第二种需要用到Redis的setnx命令,但是需要注意一些问题
5 redis事务的了解CAS
和众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTI/EXEC/DISCARD/WATCH
这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出Redis中事务的实现特征:
- 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事务中的所有命令被原子的执行
- 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行
- 我们可以通过
MULTI
命令开启一个事务,有关系型数据库开发经验的人可以将其理解为BEGIN TRANSACTION
语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD
命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK
语句 - 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行
- 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了
6 WATCH命令和基于CAS的乐观锁
在Redis的事务中,WATCH命令可用于提供CAS(Compare And Swap)功能。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。例如,我们再次假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:
1 | val = GET mykey |
以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景–竞态争用(race condition)。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。为了解决类似的问题,我们需要借助WATCH命令的帮助,见如下代码:
1 | WATCH mykey |
和此前代码不同的是,新代码在获取mykey的值之前先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功
7 redis持久化的几种方式
7.1 快照(snapshots)
缺省情况情况下,Redis把数据快照存放在磁盘上的二进制文件中,文件名为dump.rdb。你可以配置Redis的持久化策略,例如数据集中每N秒钟有超过M次更新,就将数据写入磁盘;或者你可以手工调用命令SAVE
或BGSAVE
工作原理
- Redis forks
- 子进程开始将数据写到临时RDB文件中
- 当子进程完成写RDB文件,用新文件替换老文件
- 这种方式可以使Redis使用copy-on-write技术
copy-on-write参考Linux写时拷贝技术(copy-on-write)
7.2 AOF
快照模式并不十分健壮,当系统停止,或者无意中Redis被kill掉,最后写入Redis的数据就会丢失。这对某些应用也许不是大问题,但对于要求高可靠性的应用来说,Redis就不是一个合适的选择
Append-only文件模式是另一种选择,你可以在配置文件中打开AOF模式
7.3 虚拟内存方式
7.4 diskstore方式
8 redis的缓存失效策略和主键失效机制
作为缓存系统都要定期清理无效数据,就需要一个主键失效和淘汰策略。在Redis当中,有生存期的key被称为volatile。在创建缓存时,要为给定的key设置生存期,当key过期的时候(生存期为0),它可能会被删除
8.1 影响生存时间的一些操作
生存时间可以通过使用DEL
命令来删除整个key
来移除,或者被SET
和GETSET
命令覆盖原来的数据,也就是说,修改key
对应的value
和使用另外相同的key
和value
来覆盖以后,当前数据的生存时间不同
比如说,对一个key
执行INCR
命令,对一个列表进行LPUSH
命令,或者对一个哈希表执行HSET
命令,这类操作都不会修改key
本身的生存时间。另一方面,如果使用RENAME
对一个key
进行改名,那么改名后的key
的生存时间和改名前一样
RENAME
命令的另一种可能是,尝试将一个带生存时间的key
改名成另一个带生存时间的another_key
,这时旧的another_key
(以及它的生存时间)会被删除,然后旧的key
会改名为another_key
,因此,新的another_key
的生存时间也和原本的key
一样。使用PERSIST
命令可以在不删除key
的情况下,移除key
的生存时间,让key
重新成为一个persistent key
8.2 如何更新生存时间
可以对一个已经带有生存时间的key执行EXPIRE命令,新指定的生存时间会取代旧的生存时间。过期时间的精度已经被控制在1ms之内,主键失效的时间复杂度是O(1)
EXPIRE和TTL命令搭配使用,TTL可以查看key的当前生存时间。设置成功返回1;当key不存在或者不能为key设置生存时间时,返回0
8.2.1 最大缓存配置
在redis中,允许用户设置最大使用内存大小。即设置server.maxmemory
参数值
- 默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略
redis提供6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
注意这里的6种机制,volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru(Least Recently Used)、ttl(Time-To-Live)以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略
8.2.2 使用策略规则
- 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
- 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
8.2.3 三种数据淘汰策略
ttl和random比较容易理解,实现也会比较简单。主要是lru最近最少使用淘汰策略,设计上会对key按失效时间排序,然后取最先失效的key进行淘汰
9 缓存穿透、并发和失效的解决方案
我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题:
- 缓存穿透
- 缓存并发
- 缓存失效
9.1 缓存穿透
我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了
处理方法:
- 方法1:在封装的缓存SET和GET部分增加个步骤,如果查询一个KEY不存在,就已这个KEY为前缀设定一个标识KEY;以后再查询该KEY的时候,先查询标识KEY,如果标识KEY存在,就返回一个协定好的非false或者NULL值,然后APP做相应的处理,这样缓存层就不会被穿透。当然这个验证KEY的失效时间不能太长
- 方法2:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,一般只有几分钟
- 方法3:采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉(未必,但是可以挡掉一些),从而避免了对底层存储系统的查询压力
9.2 缓存并发
有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题
我现在的想法是对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询
这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待
9.3 缓存失效
这个问题主要出现在高并发的场景下,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重
那如何解决这些问题呢?
其中的一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
缓存失效时产生的雪崩效应,将所有请求全部放在数据库上,这样很容易就达到数据库的瓶颈,导致服务无法正常提供。尽量避免这种场景的发生