redis相关
一、redis能做什么?
1、缓存
2、分布式锁
3、延迟队列
二、redis基本数据结构?
字符串string、列表list、字典hash、集合set、有序集合zset
redis中所有的数据结构都是以唯一的key字符作为名称,然后通过这个key来获取相应的value数据,不同数据类型的差异就在于value的结构不同。
1、字符串string
redis中的字符串都是动态字符串,是可修改的,采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串分配的空间(capacity)是大于当前字符串的长度len的,当前字符串的长度小于1MB时,扩容
都是翻倍现有的空间的。如果字符串长度大于1MB时,扩容一次只会扩大1MB,并且字符串的最大长度是512MB。
2、列表list
redis中的列表相当于java中的linkedlist,注意他是链表而不是数组。这也就也为着list的插入和删除操作非常快,时间复杂度为O(1),但是索引定位慢,时间复杂度为O(N)。列表中的每个元素都使用双向指针,
穿起来可以同时支持前向后向遍历,当列表弹出最后一个元素之后,该数据结构就会被自动删除。redis列表常用来做异步队列使用,将需要延后处理的任务结构序列化常字符串,塞进redis列表,另一个线程从这个
列表中轮询数据进行处理。
3、字典hash
redis中的字典相当于java中的hashMap,他是无序字典,内部存储了很多键值对,数据机构也是数据+链表的结构。Java中的rehash是个耗时的操作,需要一次性全部rehash。redis为了性能,采用的是渐进式rehahs策略
4、集合set
redis集合相当于java中的hashSet,他内部的键值对是无序的,唯一的,他内部实现相当于一个特殊的字典,字典中所有的value都是一个值NULL
5、有序列表zset
类似于java的Sortedset和hashmap的结合体,一方面是一个set,保证内部value的唯一性,另一方面他可以给每个value赋予一个score,代表这个value的权重,它内部实现用的是一种叫跳跃列表的数据结构。
高级数据结构:
heperloglog:用来解决统计问题的数据结构(不精确,标准误差在0.81%)
BloomFilter(布隆过滤器):可以理解为不精确的set结构,当使用它的contains方法判断某个对象是否存在是,他可能会存在误判
geohash(地理位置)
三、分布式锁
分布式应用在进行逻辑处理时经常会遇到并发问题。
分布式锁本质上要实现的目标就是在redis中占一个“坑”,当别的进程也要来占时,发现已经被占用时,就只好放弃或者稍后再试。占坑一般使用的setnx(set if not exists)指令,只允许被一个客户占用,先来先用,用完了,
在调用del指令释放坑。
一般会有一个问题,当锁定坑后,处理业务逻辑异常,可能导致del命令没有被调用,这样就会陷入死锁,锁得不到释放,因此,一般在拿到锁后,会给锁加一个过期时间,这样即使出现异常也不会出现锁被永远占用的情况
(上边这个解决方式仍然会有问题,当在设置过期时间时,如果因为网络问题导致expire命令没有执行,这样仍然会出现锁一直得不到释放的情况,解决方式,使用第三方库,或者使用redis2.8之后的版本,2.8之后set指令中加
了扩展参数,可以设置过期时间,这样setnx和expire两个指令就可以一起执行了)
分布式锁超时问题:
redis的分布式锁不能解决超时问题,如果加锁和解锁中间的业务流程耗时过长,大于锁的过期时间时,这样就会出现问题。
四、延迟队列
相对于专业的Rabbitmap或者kafka等消息队列中间节,redis针对只有一组消费者的情况来说可以简化很多操作(中间件各种操作过多,代码繁杂,如果对消息可靠性要求不高的话,可以使用redis来作为消息中间件)。
通过redis中的列表list来实现,list常用来作为异步的消息队列来使用,用rpush和lpush来操作入队,使用rpop和lpop来操作出队。它可以支持多个生产这和多个消费者并发进出消息,每个消费者拿到的消息都是不同的列表元素。
问题:
1、如果list空了怎么处理?
list空了之后,调用pop指令会陷入死循环,不同的pop,并且没有数据,产生空轮询,增大cup消耗,降低redis的QPS
解决:可以使用sleep睡眠以下,但是该方式也不适合,另一种方式就是使用阻塞读blpop/brpop,b代表的就是blocking
2、阻塞读之后,可能会产生空闲链接的问题?
如果在阻塞读时,列表中一直没有数据,线程就会一直阻塞,redis的客户端链接就变成了空闲链接,闲置过久,服务器一般就会主动断开链接,减少闲置资源的占用,这时候brpop/blpop指令就会抛出异常,因此,再使用阻塞读
时,一定要进行try/catch捕获异常,并且还要进行重试。
五、位图
在平时开发过程中,会有一些boolean类型的数据要存储,例如一个用户一年的签到记录,签了是1,没签是0,要记录365天,如果使用普通的key/value,没有用户就要记录365个,当用户上亿时,需要的存储空间就非常大了。
为了解决这个问题,redis提供了位图数据结构,这样每天的签到记录只会占用365个位,也就是46个字节(一个稍长的字符串)就可以完全容纳下,位图最小单位是bit,每个bit的取值只能是0或者1
位图不是一种特殊的数据结构,让的内容其实就是普通的字符串,也就是byte数组,我们可以使用普通的get/set来操作位图,也可以使用getbit/setbit等将位图看成数组来操作。
位图相关指令:
getbit/setbit/bitcount/bitpos
bitcount:统计指定位置范围内容1的个数
bitpos:查找指定范围内出现的第一个0或者1
六、redis的io模型
redis是个单线程程序
问题:
redis是单线程,为什么还那么快?
redis所有的数据都在内存中,所有的运算都是内存级别的运算
为什么能够处理那么多的并发客户端连接?
io的多路复用,select系列的时间轮询api,非阻塞io
KEYS pattern:查找所有符合给定模式pattern的key
keys指令一次返回所有匹配的key
键的数量过大则会造成服务卡顿
*从海量key里查询出某一固定前缀的key: SCAN cursor [MATCH pattern][COUNT count]
基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程,以0作为游标开始一次新的迭代,知道命令返回游标0完成一次遍历。不保证每次执行都返回某个给定数量的元素,支持模糊查询。一次返回的数量不可控,只能大概率符合count参数
第一次scan: scan 0 match k1* count 10 :从0开始迭代,符合k1前缀的字符串,数量为10.
第二次scan: scan 11534336 k1* count 10
多次scan可能获取到重复的数据
分布式锁需要解决的问题:1、互斥性 2、安全性 3、死锁 4、容错
如何通过redis实现分布式锁:
SETNX key value:如果key不存在,则创建并赋值。
时间复杂度:O(1),返回值:设置成功返回1,失败返回0
如何解决SETNX长期有效的问题?
设置过期时间:EXPIER key seconds:设置key的生存时间,当key过期时,会自动删除
以上代码的风险:当设置锁成功后,在执行if判断前,服务宕机(即没有设置过期时间),产生的原因:原子性得不到满足
redis新版本set的时候可以设置过期时间(实现了原子性)
SET key value [EX seconds] [PX milliseconds] [NX|XX] :
EX seconds:设置键的过期时间为seconds秒
PX milliseconds:设置过期时间毫秒值
NX: 只在键不存在时,才对建进行设置操作
XX:只在键已经存在时,才对键进行设置操作
SET:操作成功完成时,返回OK,否则返回nil
set lockid 1234 ex 10 nx
大量的key同时过期时注意的事项:
在设置过期的时候,给每个key减伤随机值
分布式锁的删除:
```java
public static boolean releaseLock(String key, String uniqueId) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
return jedis.eval(
luaScript,
Collections.singletonList(key),
Collections.singletonList(uniqueId)
).equals(1L);
}
```