redis的基础数据结构
注意:redis中所有数据都是以key-value形式存在的,不同的数据类型在于key的value的结构不同
1、String redis的最基本数据类型
redis的字符串类似java中的ArrayList,是可变的
字符串内部结构:带长度信息的字节数组
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
2、List 列表
相当于java里的LinkedList,可以做为异步队列来使用:从右边添加,从左边拿(做栈也可以,从哪边放就从那边开始拿)
List内部结构:快速链表
元素较少的时候使用压缩列表,当元素增大,压缩列表无法满足的时候,则会将多个压缩列表用双向指针连接,形成一个快速链表
压缩列表结构:
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
3、Hash 相当于java里的HashMap(数组+链表)
不同之处:
(1)redis的hash的value只能存字符串
(2)java中的hashmap在rehash的时候会一次性的完成;而redis的hash采用渐进式rehash,啥叫渐进式呢?就是在rehash的时候会保留新旧两个hash,在查询的时候也是查询这两个新旧hash,随后一点点的迁移数据。
4. Set 相当于java里的HashSet 内部元素是无序且唯一的。相当于一个value为null的hash
5.ZSet 有序集合
相当于java中SortedSet和HashMap的结合,元素唯一,并且可以给每个元素赋值一个得分,元素会根据得分排序
ZSet内部采用跳跃列表的数据结构,为啥不用链表呢?链表不支持快速插入,因为zset里的元素需要按照score排名,插入的时候需要找到一个合适的位置插入,通常我们会采用二分查找来找到这个合适的位置,但二分查找的对象必须是数组,链表不满足。
zset内部探究:
zset内部是一个跳跃列表和hash的符合结构,一方面需要用hash来保存value和score的关系,另一方面需要根据score的进行范围查找。
关于跳跃列表这篇文章写的很牛逼:https://zsr.github.io/2017/07/03/redis-zset内部实现/
redis应用
1.分布式锁
首先知道什么是分布式锁?synchronized是基于线程的锁,而分布式锁是基于进程的锁
具体使用需要根据业务场景,贴一段整理的redis分布式锁代码
@Component
public class DistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
//过期时间单位
private static final String SET_WITH_EXPIRE_TIME = "EX";
private static final Long DEFAULT_EXPIRE_TIME = 60L;
private static final Long RELEASE_SUCCESS = 1L;
private static Jedis jedis = new Jedis("127.0.0.1",6379,60000);
/**
* 尝试获取锁
* @param lockKey 锁
* @param expireTime 超期时间
* @return 是否获取成功
*/
public boolean tryLock(String lockKey,Long expireTime) {
expireTime = expireTime == null ? DEFAULT_EXPIRE_TIME : expireTime;
//设置lockValue防止A删除B的锁
String lockValue = UUID.randomUUID().toString();
try {
String result = jedis.set(lockKey, lockValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
} catch (Exception e){
System.out.println(e);
} finally {
jedis.close();
}
return false;
}
/**
* 释放锁
* @param lockKey 锁
* @param lockValue 请求标识
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String lockValue) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
2.异步消息队列
redis作为消息队列并没有那么多的高级特性,如果业务场景要求不高的情况下可以使用它
原理就是使用redis的list类型
3.位图
比如我们要存每个用户每天的打卡情况信息,1代表打卡,0代表未打卡,那么每个人一年就需要365条数据,如果有上亿用户这个数据量是很惊人的。
redis提供了位图:它就是普通的字符串,也就是内部也用字节数组来存,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
4.Hyper Log Log redis提供的统计解决方案
例如:求某个网站的uv(一个用户访问多少次都算一次这种需要去重的数据)
使用pfadd和pfcount指令
原理暂时先跳过
5.布隆过滤器
场景:新闻推荐,每一次的推荐需要将上一次推荐过的过滤掉,这个时候就排上用场了,为啥不用hashmap呢?简单一句话因为数据量过大时太占用空间
布隆过滤器其实就是一个位(bit)数组,比如我们存"祝君好运"这个字符串的时候,首先需要计算hashcode值,然后将其添加到位数组中。如下图(盗前辈的图?)
比如“百度”字符串存的位置是1、4、7 而“taobao”存的位置是2、5、8
而在我们查询“meituan”的时候,hash函数计算出来的位置是1、5、8 那么布隆过滤器就会认为“meituan”已经存在了(实际上不存在),在查询“meituan”的时候如果1、5、8三个位置上有一个是0那就说明这个值不存在。
问题:
(1)redis单线程为啥还这么快?
redis的数据都是存在内存,所有的操作都是基于内存的操作
(2)redis单线程如何处理那么多的并发客户端请求?
redis使用了多路复用的io模型,这里的单线程是指:redis使用一个线程来处理客户端的请求,并不是说它内部只有一个线程。