1、Redis相关网址
Redis 官网:https://redis.io/
Redis 命令参考:http://doc.redisfans.com/
2、Redis优缺点
优点
1、性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
2、丰富的数据类型 – Redis支持 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作,以及Bitmaps、HyperLogLogs、GEO(坐标)
3、原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
4、丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
缺点
1、数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
2、Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
3、主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
3、Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,查找和操作的时间复杂度都是O(1);
2、使用多路 I/O 复用模型,非阻塞 IO;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,不会出现死锁而导致的性能消耗;(Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等)
4、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
4、Redis主要数据类型及其应用场景
Redis支持的常用5种数据类型分别为:字符串String、列表List、哈希Hash、集合Set、有序集合Zset
Redis底层的数据结构包括:简单动态数组SDS、链表、字典、跳跃链表、整数集合、压缩列表(为节约内存而开发的经过特殊编码之后的连续内存块顺序型数据结构)、对象。Redis为了平衡空间和时间效率,针对value的具体类型在底层会采用不同的数据结构来实现,其中哈希表和压缩列表是复用比较多的数据结构,如下图展示了对外数据类型和底层数据结构之间的映射关系:
①string
String 是 Redis 最基本的类型,即一个 Key 对应一个 Value。Value 不仅可以是 String,也可以是数字。而且String 类型是二进制安全的,意思是 Redis 的 String 类型可以包含任何数据,比如 jpg 图片或者序列化的对象。String 类型的值最大能存储 512M。
常用命令:get、set、incr、decr、mget等。
使用场景:常规key-value缓存应用。计数器、缓存、分布式序列号、分布式锁等等。
示例:
1、单值缓存
set key value
get key
2、对象缓存
1)set user:1 value(json 格式数据)
2)mset user:1name xxx user:1:balance 1222
mget user:1:name user:1:balance
3、分布式锁
setnx product:10001 true //返回1表示获取锁成功
setnx product:10001 true //返回0表示获取锁失败
del product:10001 //释放锁
set product:10001 true ex 10 nx //防止程序意外终止导致死锁
4、计数器(阅读数实现)
incr article:readcount:{文章id} //自增+1
get article:readcount:{文章id}
5、web集群session共享
Spring session +redis实现session共享
6、分布式系统全局序列号
incrby orderId 1000 //redis 批量生成序列号提升性能
②hash
hash 是一个键值(key value)对集合。是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
常用命令:hget,hset,hgetall 等。
应用场景:存储部分变更数据,如用户信息等,获取/修改用户对象某一属性比较方便。
示例:
1、用户对象缓存
hmset user {userid}:name yoyoshaly {userid}:balance 1111
hmget user {userid}:name {userid}:balance
2、电商购物车
1)以用户id为key
2)商品id为filed
3)商品数量为value
购物车具体操作
1)添加商品 -》hset cart:1001 10088 1
2)增加购物车 -〉hincrby cart:1001 10088 1 //hincrby cart:uid pid x x增加数量
3)商品总数-》hlen cart:1001
4)删除商品-〉hdel cart:1001 10088
5)获取购物车所有商品-》hgetall cart:1001
③list
List 类似是一个双向链表,既可以支持反向查找和遍历,更方便操作。
常用命令:
1)lpush(添加左边元素),rpush,
2)lpop(移除左边第一个元素),rpop,
3)lrange(获取列表片段 LRANGE key start stop,
4)stack(栈实现)LPUSH+LPOP->FILO ,
5)queue(队列实现)LPUSH+RPOP ,
6)blocking mq(阻塞队列)LPUSH+BRPOP . //BRPOP 从key列表的表尾(right)弹出一个元素,若队列中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待。
应用场景:List 应用场景非常多,也是 Redis 最重要的数据结构之一,比如 关注列表,粉丝列表,还可以实现队列。
示例:
取最新N个数据的操作
1)登陆用户放入队列 -》 lpush login yoyo
2) 登陆用户放入队列 -》lpush login lily
3)登陆用户放入队列 -》lpush login shatt
4) 获取前10个登陆用户 -》lrange login 0 10
④set
Set 是 String 类型的无序集合。集合是通过 hashtable 实现的。Set 中的元素是没有顺序的,而且是没有重复的。而且 Set 提供了判断某个成员是否在一个 Set 集合中。
常用命令:sdd、spop、smembers、sunion 等。
应用场景:抽奖小功能、点赞、收藏、共同好友,交集并集、差集等功能。
示例:
1、微信抽奖小程序
1)点击参与抽奖加入集合
sadd key {userid}
2)查看参与抽奖所有用户
smembers key //获取集合key中所有元素
3)抽取count名中奖者
srandmember key [count] //从集合key中选出count个元素,元素不从key中删除
或spop key [count] //从集合key中选出count个元素,元素从key中删除
2、微信微博点赞,收藏,标签
1)点赞
sadd like:{消息id} {用户id}
2)取消点赞
srem like:{消息id} {用户id} //从集合key中删除元素
3)检查用户是否点过赞
sismember like:{消息id} {用户id}
4)获取点赞用户列表
smembers like:{消息id}
5)获取点赞用户数
scard like:{消息id}
3、集合操作实现微信微博关注模型
set1 a,b,c
set2 b,c,d
set3 c,d,e
1)sinter set1 set2 set3 ->{c} 求交集
2)sunion set1 set2 set3 ->{a,b,c,d,e}求并集
3)sdiff 求差集 sdiff set1 set2 set3 ->{a} 以set1为基准 set1 -set2-set3
⑤sorted set
Zset 和 Set 一样是 String 类型元素的集合,且不允许重复的元素。当你需要一个有序的并且不重复的集合列表,那么可以选择 Sorted Set 结构。和 Set 相比,Sorted Set关联了一个 Double 类型权重的参数 Score,使得集合中的元素能够按照 Score 进行有序排列,Redis 正是通过分数来为集合中的成员进行从小到大的排序。Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射。而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 Score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
常用命令:zadd、zrange、zrem、zcard 等。
使用场景:Sorted Set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。
示例:
返回有序成员列表
1)zadd runoob 0 redis
2)zadd runoob 0 mongodb
3)zadd runoob 0 rabitmq
4)zadd runoob 0 rabitmq
5) ZRANGEBYSCORE runoob 0 1000
"mongodb"
"rabitmq"
"redis"
数据类型应用场景总结:
5、Redis线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
6、淘汰策略以及内存回收机制
redis作为内存型数据库,内存不可能源源不断的增加。那么为了安全稳定的运行,内存的使用率一定需要保持在一定合理的阈值范围内。合理的内存回收机制也是很重要的。内存的占用主要是键值对存储的消耗,以及本身的运行消耗。我们的回收主要指的是键值对的回收。键值对可以分为几种:带过期的、不带过期的、热点数据、冷数据。对于带过期的键值是需要删除的,如果删除了所有的过期键值对之后内存仍然不足怎么办?那只能把部分数据给踢掉了。
redis的内存回收主要有:
1、过期键删除
删除方式:
1、定时删除(主动删除):在设置键的过期时间的同时,创建定时器,让定时器在键过期时间到来时,即刻执行键值对的删除;此方式对内存使用率有优势,但是对CPU不友好
2、定期删除(主动删除):每隔特定的时间对数据库进行一次扫描,检测并删除其中的过期键值对;此方式对内存不友好,如果某些键值对一直不被使用,那么会造成一定量的内存浪费
3、 惰性删除(被动删除):键值对过期暂时不进行删除,至于删除的时机与键值对的使用有关,当获取键时先查看其是否过期,过期就删除,否则就保留;此方式是定时删除和惰性删除的折中
Reids采用的是惰性删除和定时删除的结合
2、内存淘汰机制
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰(linkedhashmap)。
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
7、redis事务
7.1) Redis事务的三个阶段
事务开始 MULTI
命令入队
事务执行 EXEC
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队
7.2) Redis事务相关命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
如果在一个事务中出现运行错误,那么正确的命令会被执行。
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
UNWATCH命令可以取消watch对所有key的监控。
8、Redis实现分布式锁
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,我们可以使用setnx+lua,或者使用set key value px milliseconds nx 来实现分布式锁。
使用SETNX完成同步锁的流程及事项如下:
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间。
释放锁,使用DEL命令将锁数据删除。
如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
redisson分布式锁实现原理
redisson有对redlock算法的封装。其大致的工作原理如下
9、Spring Boot 监听 Redis Key 失效事件实现定时任务
原理:通过监听 Redis 提供的过期队列来实现,监听过期队列后,如果 Redis 中某一个 KV 键值对过期了,那么将向监听者发送消息,监听者可以获取到该键值对的 K。因为是获取不到 V 的,所以key的设定必须能够精准定位。
1、开启 Redis key 过期提醒
redis.conf
设置notify-keyspace-events Ex
2、引入依赖
3、相关配置
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
4、定义监听器 RedisKeyExpirationListener,实现KeyExpirationEventMessageListener 接口,
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 针对 redis 数据失效事件,进行数据处理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 获取到失效的 key,进行取消订单业务处理
String expiredKey = message.toString();
System.out.println("redisKey过期"+expiredKey);
}
5、test查看效果
@Test
public void redisTest() throws InterruptedException {
stringRedisTemplate.opsForValue().set("bike", "100", 10, TimeUnit.MILLISECONDS);
Thread.sleep(10000);
}
6、效果