分布式缓存 - Redis

简介

Redis 是开源的、高性能的 key-value 数据库。Redis 与其他 key-value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启时可以再次加载进行使用。
  • Redis不仅仅支持简单的 key-value 类型的数据,同时还提供 list(列表)、hash(哈希)、set(集合)、zset(有序集合) 数据结构的存储。
  • Redis支持数据的备份,即 master-slave 模式的数据备份。

优势

  • 性能极高:Redis速度非常快,读的速度是110000次/s,写的速度是81000/s。
  • 支持丰富的数据类型:Redis支持 string、hash、list、set、zset 五种数据类型。
  • 原子性:Redis的所有操作都是原子性的,这可确保在多个客户端同时访问时的线程安全问题(单线程),单个操作是原子性的,多个操作也支持事务(即原子性),通过MULTI和EXEC指令包起来。
  • 丰富的特性:Redis可用于许多应用场景,如缓存(注意缓存的添加不能影响正常的业务逻辑)、消息队列(Redis本身支持发布/订阅)、应用程序中的任何短期数据(如Web应用程序会话)、网页命中数等。

数据存储类型

Redis支持 string、hash、list、set、zset 五种数据类型。

Data Type Java Data Type
string Map<String, String>
hash Map<String, Map<String, String>>
list Map<String, List>
set Map<String, Set>
zset Map<String, SortedSet>

string

字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等,在Redis中字符串类型的value最多可以容纳的数据长度是512M。

Command Description
SET key value This command sets the value at the specified key.
GET key Gets the value of a key.
GETRANGE key start end Gets a substring of the string stored at a key.
GETSET key value Sets the string value of a key and return its old value.
GETBIT key offset Returns the bit value at the offset in the string value stored at the key.
MGET key1 [key2..] Gets the values of all the given keys.
SETBIT key offset value Sets or clears the bit at the offset in the string value stored at the key.
SETEX key seconds value Sets the value with the expiry of a key.
SETNX key value Sets the value of a key, only if the key does not exist.
SETRANGE key offset value Overwrites the part of a string at the key starting at the specified offset.
STRLEN key Gets the length of the value stored in a key.
MSET key value [key value …] Sets multiple keys to multiple values.
MSETNX key value [key value …] Sets multiple keys to multiple values, only if none of the keys exist.
PSETEX key milliseconds value Sets the value and expiration in milliseconds of a key.
INCR key Increments the integer value of a key by one.
INCRBY key increment Increments the integer value of a key by the given amount.
INCRBYFLOAT key increment Increments the float value of a key by the given amount.
DECR key Decrements the integer value of a key by one.
DECRBY key decrement Decrements the integer value of a key by the given number.
APPEND key value Appends a value to a key.

hash

Redis中的hash类型是具有string field和string value的map容器,所以它是表示对象的完美数据类型。在Redis中,每个hash可以存储多达40亿个字段值对。

Command Description
HDEL key field2 [field2] Deletes one or more hash fields.
HEXISTS key field Determines whether a hash field exists or not.
HGET key field Gets the value of a hash field stored at the specified key.
HGETALL key Gets all the fields and values stored in a hash at the specified key.
HINCRBY key field increment Increments the integer value of a hash field by the given number.
HINCRBYFLOAT key field increment Increments the float value of a hash field by the given amount.
HKEYS key Gets all the fields in a hash.
HLEN key Gets the number of fields in a hash.
HMGET key field1 [field2] Gets the values of all the given hash fields.
HMSET key field1 value1 [field2 value2 ] Sets multiple hash fields to multiple values.
HSET key field value Sets the string value of a hash field.
HSETNX key field value Sets the value of a hash field, only if the field does not exist.
HVALS key Gets all the values in a hash.
HSCAN key cursor [MATCH pattern] [COUNT count] Incrementally iterates hash fields and associated values.

list

在Redis中,list类型是按照插入顺序排序的字符串链表,和数据结构中的普通链表一样,我们可以在其头部和尾部添加新的元素。List中可以包含的最大元素数量是4294967295(2^32-1)。
在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。
从元素插入和删除的效率来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成;然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的(因为需要进行移动操作)。

Command Description
BLPOP key1 [key2 ] timeout Removes and gets the first element in a list, or blocks until one is available.
BRPOP key1 [key2 ] timeout Removes and gets the last element in a list, or blocks until one is available.
BRPOPLPUSH source destination timeout Pops a value from a list, pushes it to another list and returns it; or blocks until one is available.
LINDEX key index Gets an element from a list by its index.
LINSERT key BEFORE(AFTER) pivot value Inserts an element before or after another element in a list.
LLEN key Gets the length of a list.
LPOP key Removes and gets the first element in a list.
LPUSH key value1 [value2] Prepends one or multiple values to a list.
LPUSHX key value Prepends a value to a list, only if the list exists.
LRANGE key start stop Gets a range of elements from a list.
LREM key count value Removes elements from a list.
LSET key index value Sets the value of an element in a list by its index.
LTRIM key start stop Trims a list to the specified range.
RPOP key Removes and gets the last element in a list.
RPOPLPUSH source destination Removes the last element in a list, appends it to another list and returns it.
RPUSH key value1 [value2] Appends one or multiple values to a list.
RPUSHX key value Appends a value to a list, only if the list exists.

set

在Redis中,set类型是没有排序的字符串集合。set可包含的最大元素数量是4294967295(2^32-1)。
和list类型一样,我们也可以在该类型的数据值上执行添加、删除或判断某一元素是否存在等操作。需要说明的是,这些操作的时间复杂度为O(1)(无论set集合中包含的元素数量是多少都是常量时间)。
和list类型不同的是,set集合中不允许出现重复的元素,换句话说,如果多次添加相同元素,set中将仅保留该元素的一份拷贝。
和list类型相比,set类型在功能上还存在着一个非常重要的特性,即在服务器端完成多个sets之间的聚合计算操作,如unions、intersections和differences操作,由于这些操作均在服务端完成,因此效率极高,并且也节省了大量的网络IO开销。

Command Description
SADD key member1 [member2] Adds one or more members to a set.
SCARD key Gets the number of members in a set.
SDIFF key1 [key2] Subtracts multiple sets.
SDIFFSTORE destination key1 [key2] Subtracts multiple sets and stores the resulting set in a key.
SINTER key1 [key2] Intersects multiple sets.
SINTERSTORE destination key1 [key2] Intersects multiple sets and stores the resulting set in a key.
SISMEMBER key member Determines if a given value is a member of a set.
SMEMBERS key Gets all the members in a set.
SMOVE source destination member Moves a member from one set to another.
SPOP key Removes and returns a random member from a set.
SRANDMEMBER key [count] Gets one or multiple random members from a set.
SREM key member1 [member2] Removes one or more members from a set.
SUNION key1 [key2] Adds multiple sets.
SUNIONSTORE destination key1 [key2] Adds multiple sets and stores the resulting set in a key.
SSCAN key cursor [MATCH pattern] [COUNT count] Incrementally iterates set elements.

zset

和set类型极为相似,它们都是字符串的集合,都不允许重复的成员出现在一个set中。它们之间的主要差别是zset中的每一个成员都会有一个分数(score)与之关联,Redis正是通过score来为集合中的成员进行从小到大的排序。然而需要说明的是,尽管zset中的成员必须是唯一的,但是score却是可以重复的。
在zset中,添加、删除或测试成员是否存在都是非常快速的操作,其时间复杂度为O(1)(无论set集合中包含的元素数量是多少都是常量时间)。
由于zset中的成员在集合中的位置是有序的,因此,即便是访问位于集合中部的成员也仍然是非常高效的。事实上,Redis所具有的这一特征在很多其它类型的数据库中是很难实现的,换句话说,在这点上要想达到和Redis同样的高效,在其它数据库中进行建模是非常困难的。

Command Description
ZADD key score1 member1 [score2 member2] Adds one or more members to a sorted set, or updates its score, if it already exists.
ZCARD key Gets the number of members in a sorted set.
ZCOUNT key min max Counts the members in a sorted set with scores within the given values.
ZINCRBY key increment member Increments the score of a member in a sorted set.
ZINTERSTORE destination numkeys key [key …] Intersects multiple sorted sets and stores the resulting sorted set in a new key.
ZLEXCOUNT key min max Counts the number of members in a sorted set between a given lexicographical range.
ZRANGE key start stop [WITHSCORES] Returns a range of members in a sorted set, by index.
ZRANGEBYLEX key min max [LIMIT offset count] Returns a range of members in a sorted set, by lexicographical range.
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] Returns a range of members in a sorted set, by score.
ZRANK key member Determines the index of a member in a sorted set.
ZREM key member [member …] Removes one or more members from a sorted set.
ZREMRANGEBYLEX key min max Removes all members in a sorted set between the given lexicographical range.
ZREMRANGEBYRANK key start stop Removes all members in a sorted set within the given indexes.
ZREMRANGEBYSCORE key min max Removes all members in a sorted set within the given scores.
ZREVRANGE key start stop [WITHSCORES] Returns a range of members in a sorted set, by index, with scores ordered from high to low.
ZREVRANGEBYSCORE key max min [WITHSCORES] Returns a range of members in a sorted set, by score, with scores ordered from high to low.
ZREVRANK key member Determines the index of a member in a sorted set, with scores ordered from high to low.
ZSCORE key member Gets the score associated with the given member in a sorted set.
ZUNIONSTORE destination numkeys key [key …] Adds multiple sorted sets and stores the resulting sorted set in a new key.
ZSCAN key cursor [MATCH pattern] [COUNT count] Incrementally iterates sorted sets elements and associated scores.

key操作

Command Description
DEL key This command deletes the key, if it exists.
DUMP key This command returns a serialized version of the value stored at the specified key.
EXISTS key This command checks whether the key exists or not.
EXPIRE key seconds Sets the expiry of the key after the specified time.
EXPIREAT key timestamp Sets the expiry of the key after the specified time. Here time is in Unix timestamp format.
PEXPIRE key milliseconds Set the expiry of key in milliseconds.
PEXPIREAT key milliseconds-timestamp Sets the expiry of the key in Unix timestamp specified as milliseconds.
KEYS pattern Finds all keys matching the specified pattern.
MOVE key db Moves a key to another database.
PERSIST key Removes the expiration from the key.
PTTL key Gets the remaining time in keys expiry in milliseconds.
TTL key Gets the remaining time in keys expiry.
RANDOMKEY Returns a random key from Redis.
RENAME key newkey Changes the key name.
RENAMENX key newkey Renames the key, if a new key doesn’t exist.
TYPE key Returns the data type of the value stored in the key.

事务

在事务中的所有命令都将会被串行化顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事务的原子性。

Command Description
DISCARD Discards all commands issued after MULTI.
EXEC Executes all commands issued after MULTI.
MULTI Marks the start of a transaction block.
UNWATCH Forgets about all watched keys.
WATCH key [key …] Watches the given keys to determine the execution of the MULTI/EXEC block.

脚本

从Redis 2.6版本开始通过内嵌支持 Lua 环境,用于执行脚本的常用命令是EVAL。

Command Description
EVAL script numkeys key [key …] arg [arg …] Executes a Lua script.
EVALSHA sha1 numkeys key [key …] arg [arg …] Executes a Lua script.
SCRIPT EXISTS script [script …] Checks the existence of scripts in the script cache.
SCRIPT FLUSH Removes all the scripts from the script cache.
SCRIPT KILL Kills the script currently in execution.
SCRIPT LOAD script Loads the specified Lua script into the script cache.

持久化

  • 无持久化:我们可以通过配置的方式禁用Redis的持久化功能,这样我们就可以将Redis视为一个功能加强版的memcached了。
  • RDB(Relational Data Base)持久化:该机制是指在指定的时间间隔内将内存中的数据集快照写入磁盘。
  • AOF(Append Only File)持久化:该机制将以日志的形式记录服务器所处理的每一个写操作,在Redis服务器启动之初会读取该文件来重新构建数据库,以保证启动后数据库中的数据是完整的。
  • 同时应用AOF和RDB:AOF和RDB两种持久化方式是可以同时存在的,但是当Redis重启时,AOF文件会被优先用于重建数据。

集群

Redis提供了几种集群的方式:主从(Master-Slave)复制、哨兵(Sentinel)机制、Redis-Cluster集群模式

Redis集群几种模式

主从复制

工作机制

当Slave启动后,主动向Master发送SYNC命令。Master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给Slave。Slave接收到快照文件和命令后加载快照文件和缓存的执行命令。

复制初始化后,Master每次接收到的写命令都会同步发送给Slave,保证主从数据一致性。

特点

  • Master可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给Slave
  • Slave一般都是只读的,并且接收Master同步过来的数据
  • 一个Master可以拥有多个Slave,但是一个Slave只能对应一个Master
1
2
3
4
5
6
7
#bind 127.0.0.1

daemonize yes

protected-mode no

slaveof 127.0.0.1 6379

哨兵机制

Redis主从复制的缺点:没有办法对 Master 进行动态选举,需要使用 Sentinel 机制完成动态选举。

特点

  • 监控Master和Slave是否正常运行
  • Master出现故障时,自动将Slave转化为Master
  • 多哨兵配置的时候,哨兵之间也会自动监控
  • 多个哨兵可以监控同一个redis

无论是主从模式还是哨兵模式,这两个模式都有一个问题,不能水平扩容,并且这两个模式的高可用特性都会受到Master主节点内存的限制。

Cluster集群模式

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0上加入了 Cluster 集群模式,Redis Cluster是一种服务器 Sharding 技术,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容

  • Redis集群的键空间被分割为16384个槽(slot),集群的最大节点数量也是16384个。
  • Redis集群把所有的物理节点映射到[0-16383]槽上,Cluster负责维护 node <-> slot <-> value。
  • Redis集群中内置了16384个哈希槽,当需要在Redis集群中放置一个 key-value 时,Redis通过 CRC16(key) mod 16384 算法将键映射到指定的槽上。

哈希槽机制一个很明显的优势就是在处理并发的场景,因为它将数据集进行了分割,实际上减小了锁的粒度,从而扩大了并发度。Java中的ConcurrentHashMap容器就是应用这种机制来实现并发的典型例子。

搭建

  1. 修改配置文件 redis.conf 中的端口号并将cluster-enabled设置为yes来启用集群
  2. 启动所有redis实例
  3. 将ruby脚本 redis-trib.rb 拷贝到redis-cluster目录下
  4. 执行ruby脚本创建集群
1
./redis-trib.rb create --replicas 1 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006

容错

  1. 节点检测(投票机制)
    每一个节点都存有这个集群所有主节点以及从节点的信息,它们之间通过互相的PING-PONG判断是否节点可以连接上,如果有一半以上的节点去PING一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点(集群中一个节点的Master挂掉,从节点会提升为主节点)。
  2. 集群进入fail状态的必要条件
    • 如果集群任意Master挂掉且当前Master没有Slave,集群进入fail状态(即集群的slot映射[0-16383]不完成时进入fail状态)。
    • 如果集群有超过半数以上Master挂掉,无论是否有Slave,集群进入fail状态。

使用Redis需要注意的问题

缓存使用原则

缓存的添加不应该影响原有业务。

缓存更新

如果先让缓存失效再更新数据库,可能在让缓存失效后的瞬间,其他线程刚好去访问缓存,这时发现数据不存在就会去查询DB(脏数据)后再回设到缓存中,这样就会导致脏数据的出现。

解决方案:先更新数据库,再让缓存失效。当然,在更新完数据库,未来得及让缓存失效的瞬间,其他线程访问到的缓存中的数据依然是脏数据,不过概率相对较小。

缓存击穿/缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value就会去DB查找。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对DB造成很大的压力,这就叫做缓存穿透。

解决方案:业界比较常用的做法是使用使用互斥锁(mutex)。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存,否则,就重试整个get缓存的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String get(key) {
String value = redis.get(key);
// 代表缓存值未过期
if(value != null) {
return value;
}
// 设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
// SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { // 代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex); // 避免极端情况下,锁设置失效时间失败,导致了死锁发生,如果锁太久没被释放就主动删除锁
return value;
} else { // 这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
value = get(key); // 重试
}
return value;
}

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案:将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值。

常见问题

为什么Redis是单线程的?

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(单线程下已经很快了,没有必要再使用多线程了,并且使用单线程还省去了多线程之间的上下文切换导致的开销);但是,使用单线程的方式就无法发挥多核CPU性能,不过我们可以通过在单机开多个Redis实例来完善。

单线程的Redis为什么快?

Redis使用的是非阻塞IO,IO多路复用技术,使用了单线程来轮询事件,将IO的读、写、连接都转换成了事件,这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,还避免了多线程之间的上下文切换导致的开销。