Redis是完全开源的,极高性能的NoSql的数据库;读的速度能达到110000次/s,写的速度能达到81000次/s
数据类型
数据类型 | 可存储值 |
---|---|
STRING | 字符串、整数或者浮点数 |
LIST | 列表 |
SET | 无序集合 |
HASH | 包含键值对的无序散列表 |
ZSET | 有序集合 |
为什么是单线程的
- Redis是基于内存的操作,CPU不是Redis的瓶颈
- 省去了很多上下文切换线程的时间,不用去考虑各种锁的问题
- 多路I/O复用:使用了单线程来轮询描述符,减少了线程切换时上下文的切换和竞争
- 能带来更好的可维护性,方便开发和调试
数据淘汰策略
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰
- allkeys-random:从所有数据集中任意选择数据进行淘汰
- noeviction:禁止驱逐数据
需要先设置最大内存maxmemory,然后如果内存不足,会触发我们选择的过期淘汰策略
持久化
1.RDB
按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb;如果系统发生故障,将会丢失最后一次创建快照之后的数据。
默认开启
# save <seconds> <changes>
save 900 1
save 300 10
save 60 10000
保存流程(BGSAVE)
需要注意的是:
- RDB写入,每次都是全量,在数据量特别大时,服务器负载会比较高
- RDB会在服务器宕机时,丢失几分钟的数据,主要是根据save策略来的
2.AOF
如何开启
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
重写流程
需要注意的是:
- 重写是直接把当前内存的数据生成对应命令,不需要分析老的AOF文件;
- 恢复数据时,会先判断有没有AOF,没有的话,在加载RDB,因为AOF文件相对完整;
3.雪崩、穿透、击穿
雪崩
现象:缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩(由于原有缓存失效,新缓存未到期间);
解决方案:考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上;还有一个解决方案,原有的失效时间基础上增加一个随机值;
穿透
现象:查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义;
解决方案:如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟;
击穿(热点Key)
现象:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮;
解决方案:对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询;
4.分布式锁
public boolean lock(int dbIndex, String key, String requestId, int cacheSeconds) {
long beginTime = System.currentTimeMillis();
boolean isBroken = false;
Jedis jedis = null;
boolean result;
try {
jedis = getJedis();
jedis.select(dbIndex);
//NX: SET IF NOT EXIST,没有SET,有的话不操作
//PX: EXPX,过期时间设置
String value = jedis.set(key, requestId, "NX", "PX", cacheSeconds);
result = LOCK_SUCCESS.equals(value);
} catch (Exception e) {
isBroken = true;
throw new JedisException(e);
} finally {
release(jedis, isBroken);
LoggerBuilder.writeTimeLog("jedis-lock", beginTime);
}
return result;
}
public boolean releaseLock(int dbIndex, String lockKey, String requestId) {
long beginTime = System.currentTimeMillis();
boolean isBroken = false;
Jedis jedis = null;
boolean result;
try {
jedis = getJedis();
jedis.select(dbIndex);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object value = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
result = RELEASE_SUCCESS.equals(value);
} catch (Exception e) {
isBroken = true;
throw new JedisException(e);
} finally {
release(jedis, isBroken);
LoggerBuilder.writeTimeLog("jedis-releaseLock", beginTime);
}
return result;
}
5.Redis 常见性能问题和解决方案
- Master 最好不要做任何持久化工作;
- 某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
- 几个可能导致 Redis 阻塞的原因
1.API或数据结构使用不合理,发现慢查询:slowlog get {n},发现大对象:redis-cli -h{ip} -p{port} bigkeys
2.CPU饱和,redis-cli -h{ip} -p{port} –stat
3.持久化阻塞,fork阻塞,使用info stats命令获取lastest_fork_usec指标,表示redis最近一次fork操作耗时;统计中的aof_delayed_fsync指标
4.内存交换,cat /proc/{process_id}/smaps | grep Swap,如果交换量都是0KB或者个别4KB,是正常现象
5.网络 - 提高缓存命中率
1.keyspace_hits / (keyspace_hits + keyspace_misses) = 命中率
2.缓存时间越长,命中率会越高。时效性要求越低,就越适合缓存
3.缓存的容量有限,则容易引起缓存失效和被淘汰
4.缓存的粒度越小,命中率会越高
6.部署方式
6.1 主从
主从复制原理:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;(从服务器初始化完成)
- 主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)
一般主从配置可以缓解请求压力,做读写分离,写服务器不开启持久化,从服务器开启,从服务器还负责读取的操作,而且从服务器可以是多个,可以有效缓解主服务器的压力;但是坏处在于,如果主服务器宕机,无法自动切换恢复;
6.2 哨兵
主要作用
- 监控主服务器和从服务器是否正常运行
- 主服务器出现故障时自动将从服务器转换为主服务器
- 也具备了主从的全部好处
工作方式
- 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
- 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
- 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)
- 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
- 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
哨兵其实是对主从配置的补充,可以自动选举出新的主,提供服务,但是,如果单机redis数据量过大,这个部署就无法处理了,需要cluster模式进行分片
6.3 集群
- 多个redis节点网络互联,数据共享
- 所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
- 不支持同时处理多个key(如MSET/MGET)
- 支持在线增加、删除节点