1)都是基于内存的数据库,一般都用来当作缓存使用,两者的性能都较高
2)两者都有过期时间
1)Redis支持更丰富的数据类型(支持更复杂的应用场景),提供map、list、hash、set、zset等数据结构的存储。Memcached 只支持最简单的key-value数据类型的存储。
2)Redis支持数据的持久化(AOF和RDB方式),可以将内存中的数据写入磁盘,重启的时候,可以加载进行使用,故有在灾难恢复机制。而Memcached需要将数据全部保存在内存中。
3)Redis在服务器内存使用完之后,可以将不用的数据放到磁盘中。而Memcached 在服务器内存使用完后,会直接报异常。
4)Redis支持原生的集群模式(cluster),Memcached 没有原生的集群模式,需要依赖客户端往集群中分片写入数据。
5)Redis使用单线程的多路IO复用模型(Redis 6.0使用了多线程IO)。Memcached 是多线程,非阻塞IO复用的网络模型。
6)Redis支持发布订阅模型、事务、Lua脚本等功能。而Memcached 不支持,且Redis支持更多的编程语言。
7)Redis过期数据的删除策略同时使用到了惰性删除与定期删除,而Memcached 是支持惰性删除。
基于上述Redis的优点,当前项目中基本都使用Redis来做项目的分布式缓存。
1)Redis通过IO多路复用程序 监听客户端的大量连接,将需要的事件和类型(读、写)注册到内核中并监听每个事件是否发生。如此,Redis不需要创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
2)Redis服务器是一个事件驱动程序,服务器需要处理 文件事件和时间事件。
时间事件不需要多花时间了解,我们接触最多的还是 文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。
3)文件事件处理器(file event handler)主要是包含 4 个部分:
- 多个Socket(客户端连接)
- IO多路复用程序(支持多个客户端连接的关键)
- 文件事件分派器(将socket关联到相应的时间处理器)
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
1)纯内存操作
2)核心是基于非阻塞的IO多路复机制来处理客户端的Socket
3)单线程避免了多线程频繁的上下文切换带来的性能问题
1)单线程编程较简单且更容易维护。
2)Redis的性能瓶颈不再是CPU,而是内存和网络。
3)多线程会有死锁,线程上下文切换等问题,甚至会影响性能。
redis.conf
:io-threads-do-reads yes
开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf
:
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
127.0.0.1:6379> subscribe channel1 channel2 channel3 # 订阅频道channel1、channel2、channel3
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "subscribe"
2) "channel2"
3) (integer) 2
1) "subscribe"
2) "channel3"
3) (integer) 3
当给这个频道发布消息后,消息就会发送给订阅的客户端:
再另外打开一个频道,发布消息:
127.0.0.1:6379> publish channel1 "This is a message of channel1"
(integer) 1 # 返回的1是订阅者数量
其后,订阅channel1、channel2、channel3的一端就会接收到消息发布端的发送的消息:
1) "message"
2) "channel1"
3) "This is a message of channel1"
127.0.0.1:6379> publish channel2 "hello,redis 222"
(integer) 1
127.0.0.1:6379> publish channel3 "welcome to study redis 3333"
(integer) 1
String 类型数据是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据,还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
常用命令: set,get,strlen,exists,dect,incr,setex
等等。
应用场景 :一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
普通字符串的基本操作:
127.0.0.1:6379> set key1 var1 #设置 key-value 类型的值
OK
127.0.0.1:6379> get key1 # 根据 key 获得对应的 value
"var1"
127.0.0.1:6379> exists key # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。
(integer) 4
127.0.0.1:6379> del key # 删除某个 key 对应的值
(integer) 1
127.0.0.1:6379> get key
(nil)
127.0.0.1:6379> mset user1 zhangsan user2 lisi user3 maliu # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget user1 user2 user3 # 批量获取多个 key 对应的 value
1) "zhangsan"
2) "lisi"
3) "maliu"
127.0.0.1:6379> set number 10
OK
127.0.0.1:6379> incr number # 将key中储存的数字值增一,相当于number++
(integer) 11
127.0.0.1:6379> get number
"11"
127.0.0.1:6379> incrby num 20 # 相当于num = num + 20;
(integer) 31
127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
(integer) 30
127.0.0.1:6379> get number
"30"
127.0.0.1:6379> expire dtime 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex dtime 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期,单位为秒
(integer) 56
rpush,lpop,lpush,rpop,lrange、llen
等。rpush/lpop
实现队列:127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素
(integer) 3
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value1"
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value2"
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value3"
rpush/rpop
实现栈:127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"
127.0.0.1:6379> rpop myList2
"value2"
127.0.0.1:6379> rpop myList2
"value1"
lrange
查看对应下标范围的列表元素:127.0.0.1:6379> rpush myList3 value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList3 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value1"
2) "value2"
127.0.0.1:6379> lrange myList3 0 -1 # 查看列表中的所有元素,-1表示倒数第一
1) "value1"
2) "value2"
3) "value3"
127.0.0.1:6379> llen myList3 # 查看List链表的长度
(integer) 3
hset,hmset,hexists,hget,hgetall,hkeys,hvals
等。127.0.0.1:6379> hmset emp ename lihua gender man age 26 email 123@qq.com
OK
127.0.0.1:6379> hkeys emp # 获取emp对象的所有key
1) "ename"
2) "gender"
3) "age"
4) "email"
127.0.0.1:6379> hvals emp # 获取emp对象的所有value
1) "lihua"
2) "man"
3) "26"
4) "[email protected]"
127.0.0.1:6379> hgetall emp # 获取在哈希表中指定 key 的所有键和值
1) "ename"
2) "lihua"
3) "gender"
4) "man"
5) "age"
6) "26"
7) "email"
8) "[email protected]"
127.0.0.1:6379> hexists emp ename # 查看 key 对应的 value中指定的字段是否存在。
(integer) 1
127.0.0.1:6379> hget emp ename # 获取存储在哈希表中指定字段的值。
"lihua"
127.0.0.1:6379> hget emp email
"[email protected]"
127.0.0.1:6379> hset emp ename zhangsan # 如果ename为空,则将ename设置为后面的值,如果ename不为空,则将ename修改为后面的值
(integer) 0
127.0.0.1:6379> hget emp ename
"zhangsan"
HashSet
。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。sadd,spop,smembers,sismember,scard,sinterstore,sunion
等。127.0.0.1:6379> sadd myset1 v1 v2 v3 # 向set集合中添加元素
(integer) 3
127.0.0.1:6379> sadd myset1 v1 v2 # 不能向set集合中添加重复元素
(integer) 0
127.0.0.1:6379> smembers myset1 # 查看myset1中的所有元素
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> sadd myset1 v4
(integer) 1
127.0.0.1:6379> smembers myset1
1) "v3"
2) "v2"
3) "v4"
4) "v1"
127.0.0.1:6379> scard myset1 # 查看myset1集合的长度
(integer) 4
127.0.0.1:6379> sismember myset1 v1 # 检查某个元素是否存在set 中,只能接收单个元素
(integer) 1
127.0.0.1:6379> sismember myset1 val1
(integer) 0
127.0.0.1:6379> sadd myset2 var1 var2 v1 v2 var3
(integer) 5
127.0.0.1:6379> sinterstore myset3 myset1 myset2 # 获取 mySet1 和 mySet2 的交集并存放在 mySet3 中
(integer) 2
127.0.0.1:6379> smembers myset3
1) "v2"
2) "v1"
127.0.0.1:6379> sunion myset4 myset1 myset2 # 获取 mySet1 和 mySet2 的并集并存放在 mySet4 中
1) "v3"
2) "v4"
3) "v1"
4) "var2"
5) "v2"
6) "var1"
7) "var3"
zadd,zcard,zscore,zrange,zrevrange,zrem
等。127.0.0.1:6379> zadd score 110 Math 120 English 105 Chinese 130 History # 向zset集合(score)中添加元素
(integer) 4
127.0.0.1:6379> zcard score # 查看score集合中的元素个数
(integer) 4
127.0.0.1:6379> zscore score Math # 查看键为Math的权重(分数/权重)
110.0
127.0.0.1:6379> zrange score 0 -1 withscores # 将课程成绩从低到高正序输出,且带权重,如果不需要带权重,即不用后面添加withscores
1) "Chinese"
2) 105.0
3) "Math"
4) 110.0
5) "English"
6) 120.0
7) "History "
8) 130.0
127.0.0.1:6379> zrank score Math # 返回该值在集合中的排名,排名从0开始
(integer) 1
127.0.0.1:6379> zrank score Chinese
(integer) 0
127.0.0.1:6379> zcount score 115 150 # 统计该集合,分数区间内(115到150)的元素个数
127.0.0.1:6379> zrem score History # 删除score集合下,指定值的元素
(integer) 1
127.0.0.1:6379> zadd topn 1600 a1 2000 a2 800 a3 1200 a4 600 a5
(integer) 4
127.0.0.1:6379> zrevrange topn 0 -1 withscores # 输出所有文章访问量的排行榜
# 逆序输出某个范围区间的元素,0 为排序的起始下标,后面的值表示排序的最后下标,-1表示下标倒数第一
1) "a2"
2) 2000.0
3) "a1"
4) 1600.0
5) "a4"
6) 1200.0
7) "a3"
8) 800.0
9) "a5"
10) 600.0
127.0.0.1:6379> zrevrange topn 0 2 withscores # 输出访问量前三名的文章(a2、a1、a4)
1) "a2"
2) 2000.0
3) "a1"
4) 1600.0
5) "a4"
6) 1200.0
setbit<key><offset><value> # 设置Bitmaps中某个偏移量的值(0或1),偏移量从0开始
getbit<key><offset> # 获取Bitmaps中某个偏移量的值
bitcount<key>[start end] # 统计字符串从start字节到end字节比特值为1的数量
bitop and(or/not/xor) <destkey> [key…] # bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
可以将每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图:
用users:20210816 代表2021-08-16这天的独立访问用户的Bitmaps:
127.0.0.1:6379> setbit users:20210816 1 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 6 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 11 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 15 1
(integer) 0
127.0.0.1:6379> setbit users:20210816 19 1
(integer) 0
获取id=6和id=12的用户是否在2021-08-16这天访问过, 返回0说明没有访问过,返回1则访问过:
127.0.0.1:6379> getbit users:20210816 6
(integer) 1
127.0.0.1:6379> getbit users:20210816 12
(integer) 0
- 这里需要注意的是:
bitcount <key> [start end] 统计字符串从start字节到end字节比特值为1的数量(当start和end不指定时,即该命令求的是key中所有1的数量,当指定start和end时,start和end代表起始和结束字节数,字节计数从0开始)
计算2021-08-16这天独立访问的总用户数量
> 127.0.0.1:6379> bitcount users:20210816
(integer) 5
用户id在第1个字节到第2个字节(9到24位)之间的独立访问用户数, 对应的用户id是11, 15, 19,在第0个字节到第1个字节(0到16位)之间的独立访问用户数,对应的用户id是1,6,11,15
users:20210816 -->【01000001 01000000 00000000 00100001】,对应的字节为【0,1,2,3】
127.0.0.1:6379> bitcount users:20210816 1 2
(integer) 3
127.0.0.1:6379> bitcount users:20210816 0 1
(integer) 4
# 2021-08-16 日访问网站的userid=1,6,11,15,19。
setbit users:20210816 1 1
setbit users:20210816 6 1
setbit users:20210816 11 1
setbit users:20210816 15 1
setbit users:20210816 19 1
# 2021-08-17 日访问网站的userid=1,6,11,15,19。
setbit users:20210817 6 1
setbit users:20210817 8 1
setbit users:20210817 10 1
setbit users:20210817 11 1
setbit users:20210817 20 1
setbit users:20210817 30 1
- 127.0.0.1:6379> bitop and anskey users:20210816 users:20210817
(integer) 4
127.0.0.1:6379> bitcount anskey # 结果为2,userid为6和11的用户在这两天都访问过
(integer) 2
127.0.0.1:6379> bitop or anskey users:20210816 users:20210817
(integer) 4
127.0.0.1:6379> bitcount anskey
# 结果为9,userid为1,6,8, 10,11,15, 19,20,30的用户在这两天中的任意一天访问过
(integer) 9
pfadd <key>< element> [element ...] 添加指定元素到 HyperLogLog 中
pfcount<key> [key ...] 计算HLL的近似基数,可以计算多个HLL
pfmerge<destkey><sourcekey> [sourcekey ...] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
127.0.0.1:6379> pfadd language1 java php python C++
(integer) 1
127.0.0.1:6379> pfcount language1 # language1的元素基数为4个
(integer) 4
127.0.0.1:6379> pfadd language1 java go python # HyperLogLog经常被用来统计集合中的元素基数,重复元素不会重复计数
(integer) 1
127.0.0.1:6379> pfcount language1 # language1的元素只多了go,基数变为5
(integer) 5
127.0.0.1:6379> pfadd language2 perl ruby go java
(integer) 1
127.0.0.1:6379> pfcount language1 language2 # 统计language1和language2两者中元素的基数(基数:集合中不重复元素个数)
(integer) 7
127.0.0.1:6379> pfmerge resKey language1 language2 #language1和language2中的元素合并到resKey,(即resKey 是language1和language2两者的并集)
OK
127.0.0.1:6379> pfcount resKey
(integer) 7
geoadd<key>< longitude><latitude><member> [longitude latitude member...] # 添加地理位置(经度,纬度,名称)
geopos <key><member> [member...] # 获得指定地区的坐标值
geodist<key><member1><member2> [m|km|ft|mi ]
# 获取两个位置之间的直线距离,单位:
# m 表示单位为米[默认值]。
# km 表示单位为千米。
# mi 表示单位为英里。
# ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
georadius<key>< longitude><latitude>radius m|km|ft|mi #以给定的经纬度为中心,找出某一半径内的元素
127.0.0.1:6379> geoadd china:city 121 31 shanghai 114 22 shenzhen 116 39 beijing # 向集合china:city中添加城市信息(经度 纬度 城市名)
(integer) 3
127.0.0.1:6379> geopos china:city beijing shenzhen # 获取beijing和shenzhen的经度和纬度
1) 1) "116.00000113248825073"
2) "38.99999918434559731"
2) 1) "114.00000125169754028"
2) "21.99999950739083232"
127.0.0.1:6379> geodist china:city shanghai shenzhen km
"1218.8985"
127.0.0.1:6379> geodist china:city beijing shanghai km # 获取beijing和shanghai两个位置之间的直线距离
"999.2077"
127.0.0.1:6379> georadius china:city 110 30 1000 km # 以给定的经纬度(110,30)为中心,找出某一半径(1000km)内的城市
1) "shenzhen"
1)如果用户请求的数据在缓存中,就直接返回对应的数据。
2)请求数据不再缓存中,就去数据库中查找。
3)如果数据库能找到需要的数据,就将数据更新到缓存中,并返回对应的结果。
4)如果数据库不能找到数据,就返回空数据。
高性能 :
对照上面的图。我们设想这样的场景:
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢?那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接Out of memory。
Redis 自带了给缓存数据设置过期时间的功能,比如:
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
setex
外,其他方法都需要依靠 expire
命令来设置过期时间 。另外, persist
命令可以移除一个键的过期时间:过期时间除了有助于缓解内存的消耗,还有什么其他用么?
Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的key(键)指向Redis数据库中的某个key(键),过期字典的value(值)是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。
typedef struct redisDb {
...
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
1) 惰性删除 :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
2)定期删除 : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供 6 种数据淘汰策略:
1)volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3) volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
4.)allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
5) allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
6) no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种:
7) volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
8) allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
拓展:Redis 4.0 对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble
开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
补充内容:AOF 重写
AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> mset k1 100 k2 200 # 将命令放入命令队列中,直到输入命令 exec才会依次执行命令队列中的命令
QUEUED
127.0.0.1:6379(TX)> mset k3 300 k4 400
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> mget k2 k3
QUEUED
127.0.0.1:6379(TX)> exec # 执行队列中的命令
1) OK
2) OK
3) "100"
4) 1) "200"
2) "300"
组队中某个命令出现了错误,执行时整个队列所有命令都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
127.0.0.1:6379> mset eid 101 age 26 ename lisi gender man
OK
127.0.0.1:6379> watch eid age
OK
127.0.0.1:6379> mset eid 301 age 40
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby age 5
QUEUED
127.0.0.1:6379(TX)> set gender woman # 如果事务在后面被执行,age == 45,woman=="woman"
QUEUED
127.0.0.1:6379(TX)> exec # 事务被打断
(nil)
127.0.0.1:6379> mget age gender
1) "40"
2) "man"
1)原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2) 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
3) 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
4)一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
(1)对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2)设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3)采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
(4)进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决问题:
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:
(1)就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
(2)先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
(3)当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
(4)当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
(1)构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
(2)使用锁或队列:
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3)设置过期标志更新缓存:
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4)将缓存失效时间分散开:
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
实现方式可以有很多种,下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。
Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
1)缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
2) 增加cache更新重试机制(常用): 如果 cache 服务当前不可用,导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。
参考:
1、Redis 命令参考
2、《Redis 设计与实现》
3、关于 Redis 事务不满足原子性的问题
4、尚硅谷Redis教程