Redis的优点
高效
Redis是纯内存数据库,采用了单线程模型和IO多路复用,使用了单个线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
丰富的数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
丰富的指令集合
如可以设置key的过期时间等。
持久化
由于Redis是纯内存数据库,如果没有配置持久化,内存中的数据会丢失,于是需要开启Redis的持久化功能,将数据刷新到磁盘上。当redis重启后,可以从磁盘中恢复数据。Redis提供两种方式进行持久化,RDB持久化(将内存快照dump到磁盘上生成RDB文件),另外一种是AOF(append only file)持久化(将Reids的操作日志以追加的方式写入AOF文件)。
支持多种部署架构
主从架构,哨兵模式,集群架构等。
Redis的应用场景
热点数据缓存
由于Redis访问速度极快(每秒10w),所以很适合用来缓存热点数据,另外结合expire功能,可以设置缓存的失效时间。
限时
利用Redis的expire命令可以设置key的失效时间,从而可以实现限时优惠活动,手机验证码等业务场景。
分布式计数器
利用Redis的incrby命令可以实现原子性的递增,从而可以实现高并发的秒杀活动的计数,分布式ID的生成等。
排行榜问题
利用Redis的有序集合(zset)可以实现热点数据的排行榜。
分布式锁
利用Redis的setnx(set if not exist)命令结合超时功能,可以实现分布式锁。
延时操作
在订单产生后就占用了库存,10分钟后去检验用户是否真的付款购买,如果没有购买,则还原库存。
阻塞队列
Redis的队列可以设置为阻塞队列(brpush, brpop),从而可以实现简单的消息队列中间件。
Redis的数据类型
Redis五种数据类型的底层实现方式: blog.csdn.net/caishenfans…
String
Redis最基本的数据类型,每个value最大能存储512mb,常用命令为get/set key value,用作计数器时,value的值必须能转换成整数或浮点数,否则在进行加减操作时会报错。
Hash
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。实例:
127.0.0.1:6379> HMSET book name "redis tutorial" description "redis basic commands for caching" likes 20 visitors 23000
OK
127.0.0.1:6379> HGETALL book
1) "name"
2) "redis tutorial"
3) "description"
4) "redis basic commands for caching"
5) "likes"
6) "20"
7) "visitors"
8) "23000"
List
String类型的列表,按照插入顺序排序,可以从列表头部或尾部添加/删除元素,可以存储 232 - 1 键值对(40多亿),支持一次性添加多个key。
Set
String类型的无序集合,集合成员是唯一的,添加,删除和查找的时间复杂度都是O(1),支持一次性添加多个成员。
Zset
有序集合,每个元素都会关联一个double类型的分数(score),可以通过score为集合中的成员从小到大排序,底层实现包括跳跃表或ziplist两种方式。
Bitmaps
可以理解为只存储0/1的字符串,可以用来实现布隆过滤器解决缓存穿透的问题。
Geo
用于存储地理位置信息(经纬度),并对存储的信息进行计算操作,如计算两个点之间的距离,计算当前点附近有哪些点等。
持久化
RDB
在RDB持久化方式下,有两种选择,一种是手动执行持久化数据命令来让redis生成内存数据快照,另一种则是根据配置文件的策略,在满足条件时自动持久化数据。手动执行持久化有两个命令,save命令和bgsave命令。
save操作在Redis主线程中工作,因此会阻塞其他请求操作,应该避免使用。
bgsave则是fork出子进程,父进程继续处理请求。而子进程则将数据写入临时文件,并在写完后,替换原有的.rdb文件。fork发生时,父子进程内存共享,所以为了不影响子进程做数据快照,在这期间修改的数据,将会被复制一份,而不进行共享内存。所以说,RDB所持久化的数据,是fork发生时刻的数据。配置文件的策略:
save 900 1
save 300 10
save 60 10000
这是配置文件默认策略,三个策略是或的关系,即:
每隔900秒,在这期间变化了至少1个键值,做快照;
或每隔300秒,变化了10键值,做快照;
或每隔60秒,变化了至少10000个键值,做快照。
AOF
配置文件中的appendonly设置为yes。开启AOF持久化后,你所执行的每一条指令,都会以日志形式记录到appendonly.aof文件中。但事实上,aof文件并不会立即将命令写入到硬盘文件中,而是写入到硬盘缓存,可以在配置文件中配置多久把硬盘缓存写入到硬盘文件。所以在一定程度一定条件下,还是会有数据丢失,不过RDB方式相比可以大大减少数据损失。
redis默认使用everysec刷新磁盘缓存,就是说每秒持久化一次,而always则是每次操作都会立即写入aof文件中。而no则是不主动进行同步操作,是默认30s一次。当然always一定是效率最低的,个人认为everysec就够用了,数据安全性能又高。
AOF还支持日志重写,用来解决本地日志量过大的问题,如先后执行了set a a, set a b命令,重写之后只会记录set a b这一个操作,而del a操作就不再记录到日志等。
Redis也允许我们同时使用两种方式,重启redis后会优先从aof中恢复数据,毕竟aof比rdb损失的数据少,如果同时配置了RDB和AOF,启动只加载AOF文件恢复数据。如果只配置了RDB,启动时加载dump.rdb文件恢复数据。
两种持久化方式的对比
| 持久化方式 | RDB | AOF |
|---|---|---|
| 占用内存空间 | 小 | 大 |
| 存储速度 | 慢 | 快 |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 会丢失数据 | 依据策略决定 |
| 资源消耗 | 高,重量级 | 低,轻量级 |
| 启动优先级 | 低 | 高 |
Redis实现分布式锁
如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。大家如果有兴趣,可以去看看Redisson的官网,看看如何在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。 下面给大家看一段简单的使用代码片段,先直观的感受一下:
Rlock lock = redisson.getLock("lock");
lock.lock();
lock.unlock();
此外,redisson还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。
Redisson实现Redis分布式锁的底层原理
好的,接下来就通过一张手绘图,给大家说说Redisson这个开源框架对Redis分布式锁的实现原理。
加锁机制
咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。这里注意,仅仅只是选择一台机器!这点很关键!紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:
为啥要用lua脚本呢?因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。那么,这段lua脚本是什么意思呢?
KEYS[1]代表的是你加锁的那个key,比如说:
RLock lock = redisson.getLock("myLock");
这里你自己设置了加锁的那个锁key就是“myLock”。
ARGV[1]代表的就是锁key的默认生存时间,默认30秒。
ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:
8743c9c0-0795-4907-87fd-6c719a6b4586:1
给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。 如何加锁呢?很简单,用下面的命令:
hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
myLock:
{
"8743c9c0-0795-4907-87fd-6c719a6b4586:1" 1
}
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。 接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。好了,到此为止,ok,加锁完成了。
锁互斥机制
那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。
watch dog自动延期机制
客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
可重入加锁机制
那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢? 比如下面这种代码:
RLock lock = redisson.getLock("lock");
lock.lock();
// biz
lock.lock();
// biz
lock.unlock();
这时我们来分析一下上面那段lua脚本。 第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。 第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1” 此时就会执行可重入加锁的逻辑,他会用:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
通过这个命令,对客户端1的加锁次数,累加1。此时myLock数据结构变为下面这样:
myLock:
{
"8743c9c0-0795-4907-87fd-6c719a6b4586:1" 2
}
大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数
释放锁机制
如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从redis里删除这个key。然后呢,另外的客户端2就可以尝试完成加锁了。这就是所谓的分布式锁的开源Redisson框架的实现机制。一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。
上述Redis分布式锁的缺点
其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。