Redis挖掘机(一)基础必备
Redis的特点
- 基于内存的数据库,读取快
- 单线程模型,减少了线程开销(上下文切换、竞争)的性能损耗
- 使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
数据结构
-
字符串String
-
字典Hash
内部结构:数组+链表,相当于HashMap,是无序字典。但是redis只能存储字符串,redis为了高性能,它的扩容方式为渐进式rehash策略,不同于JAVA的HashMap为一次性全部rehash。
渐进式rehash会在rehash的同时,保留新旧两个hash结构。然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的hash结构取而代之。
用途:存储用户信息。hash可以对一个结构体中单个或多个字段进行存储,这样当需要获取用户信息时就可以部分获取。而用字符串存储用户信息的话,需要一次性获取,浪费流量。
注意:当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
缺点:hash结构的存储消耗要大于字符串
主要方法:hset、hget、hgetall、hlen、hmset、hincrBy、hkeys、hvals、hdel、hexists
@Test public void test() { // 将哈希表 key 中的字段 field 的值设为 value 。 hashRedisTemplate.hset("Jerry", "sex", "男"); // 获取在哈希表中指定 key 的所有字段和值 System.out.println(hashRedisTemplate.hgetAll("Jerry")); // 获取存储在哈希表中指定字段的值。 System.out.println(hashRedisTemplate.hget("Jerry", "sex")); //output: {sex=男} //output: 男 Map<String, String> map = Maps.newHashMap(); map.put("age", "24"); map.put("like", "basketball"); map.put("sex", "男"); // 同时将多个键值对设置到哈希表 key 中。 hashRedisTemplate.hmset("Jim", map); // 获取所有给定字段的值 List<String> list = hashRedisTemplate.hmget("Jim", "age", "like", "sex"); System.out.println(list); //output: [24, basketball, 男] // Map<String, String> map1 = hashRedisTemplate.hgetAll("Jim"); System.out.println(map1); //output: {like=basketball, age=24, sex=男} // 为哈希表 key 中的指定字段age的整数值加上增量1 Long newAge = hashRedisTemplate.hincrBy("Jim", "age", 1) System.out.println(newAge); //output: 25 }
由于哈希表是一个二维结构,第一维是数组,第二维是链表,它可能会遇到
- 哈希表中的箱子比较多的情况下扩容会重新rehash,移动数据,性能影响极大
- 哈希函数设计不合理,在极端情况下会变成线性表,性能极低。
Java和Redis对于以上哈希表问题的解决方案?
- Java主要是在哈希函数不合理而导致链表过长时,会使用红黑树来保证插入和查询的效率,但是当遇到哈希表比较大需要扩容时,扩容会导致瞬时效率降低,因为是一次性扩容。而由于它有一个初识容量的设定,所以可以通过设置合适的初识容量,来预防rehash的操作。
- Redis主要是有一个默认的哈希函数可以有效避免链表过长;当遇到哈希表扩容时,会采用渐进式扩容策略的方式,来提高效率。
-
列表List
内部结构是链表,相当于LinkedList,插入和删除的时间复杂度为O(1),查询的时间复杂度为O(n)
用途:当做异步队列使用。将需要延后处理的数据序列化成字符串塞进列表,另一个线程轮询该列表获取数据进行处理
注意:当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
主要方法:rpush、lpush、rpop、lpop、lindex、lrange
获取索引范围内的数据
@Test public void test() { // 批量添加多个元素到列表尾部 listRedisTemplate.rpush("name1", "Jerry", "Tom", "Tony"); // 获取索引范围内的数据 System.out.println(listRedisTemplate.lrange("name1", 1, 2)); // output: [Tom, Tony] }
根据索引获取列表数据
@Test public void test() { // 批量添加多个元素到列表尾部 listRedisTemplate.rpush("name1", "Jerry", "Tom", "Tony"); // 根据索引获取列表数据 System.out.println(listRedisTemplate.lindex("name1", 2)); // output: Tony }
队列:左进右出
@Test public void test() { // 批量添加多个元素到列表头部 listRedisTemplate.lpush("name", "Jerry", "Tom", "Tony"); while (true) { // 移除并返回列表的最后一个元素 String result = listRedisTemplate.rpop("name"); System.out.println(result); if(Strings.isNullOrEmpty(result)) break; // output: Jerry // output: Tom // output: Tony // output: null } }
栈:右进右出
@Test public void test() { // 批量添加多个元素到列表尾部 listRedisTemplate.rpush("name", "Jerry", "Tom", "Tony"); while (true) { // 移除并返回列表的最后一个元素 String result = listRedisTemplate.rpop("name"); System.out.println(result); if(Strings.isNullOrEmpty(result)) break; // output: Tony // output: Tom // output: Jerry // output: null } }
-
集合Set
内部结构:相当于一个特殊字典Hash,value都是null,内部的键值对都是无序且唯一。相当于JAVA的HashSet
用途:可以存储抽奖活动的中奖用户ID
注意:当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
主要方法:sadd、spop、smembers(获取集合内所有元素)、sismember(是否存在)、scard(获取集合元素个数)
@Test public void test() { // 批量添加元素 setRedisTemplate.sadd("nickName", "Jerry", "Tom", "Jim"); // 获取集合内元素个数 System.out.println(setRedisTemplate.scard("nickName")); // 集合内是否存在某个元素 System.out.println(setRedisTemplate.sismember("nickName", "Tony")); // 获取集合内所有元素 System.out.println(setRedisTemplate.smembers("nickName")); // 弹出一个元素 System.out.println(setRedisTemplate.spop("nickName")); // 弹出指定3个元素 System.out.println(setRedisTemplate.spop("nickName", 3)); //output: 3 //output: false //output: [Jim, Tom, Jerry] //output: Jim //output: [Tom, Jerry] (由于已经被推出1个元素,所以仅剩2个元素) }
-
有序集合SortedSet
内部结构:相当于JAVA的SortedSetheHashMap的结合。既能保存元素的唯一性,也能给元素添加一个score,代表这个元素的排序权重。内部实现是基于跳跃列表
用途:存储按时间排序的粉丝列表,即粉丝:粉丝名称+粉丝关注时间。
注意:最后一个 value 被移除后,数据结构自动删除,内存被回收。
主要方法:zcard、zadd、zscore、zrank、zrem、zincrby、zrange、zrevrange、zrangeByScore等
@Test public void test() { // 向有序集合添加一个元素 sortedSetRedisTemplate.zadd("scores", 100.0, "Jerry"); sortedSetRedisTemplate.zadd("scores", 70.0, "Tom"); sortedSetRedisTemplate.zadd("scores", 50.0, "Jim"); // 获取有序集合元素个数 System.out.println(sortedSetRedisTemplate.zcard("scores")); // 根据索引区间按score顺序输出 Set<String> members = sortedSetRedisTemplate.zrange("scores", 0, -1); System.out.println(members); // 根据索引区间按score逆序输出 members = sortedSetRedisTemplate.zrevrange("scores", 0, -1); System.out.println(members); // 根据分数区间按score顺序输出 members = sortedSetRedisTemplate.zrangeByScore("scores", 40.0, 80.0); System.out.println(members); // 根据元素获取索引值,即排名 System.out.println(sortedSetRedisTemplate.zrank("scores", "Jerry")); // 根据元素获取score System.out.println(sortedSetRedisTemplate.zscore("scores", "Jerry")); // 删除元素 sortedSetRedisTemplate.zrem("scores", "Jerry", "Jim"); // 重新获取有序集合元素个数 System.out.println(sortedSetRedisTemplate.zcard("scores")); //output: 3 //output: [Jim, Tom, Jerry] //output: [Jerry, Tom, Jim] //output: [Jim, Tom] //output: 2 //output: 100.0 //output: 1 }
-
HyperLogLog
内部结构:-
特点:
-
提供不精确去重计数方案,标准误差是0.81%
-
每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。相对于用Set集合元素越多,耗费内存也越多,优势明显。
-
计数较小时,存储空间采用稀疏矩阵存储,空间占用很小。当超过一定阈值时,会一次性转变为稠密矩阵,才会占用12k的内存
用途:做基数统计。比如统计一个网站的UV。
如何实现使用较小的内存去实现对大数据的去重计数?
-
使用HashSet或redis的set集合,数据量太大会导致内存占用大,分配不足会导致内存泄漏
-
使用位图法。位图法所需的空间很少,像JAVA的BitSet就是使用了位图法,底层是基于long[]实现,long是8字节存储的,所以BitSet最小是64位。它并不存储每一个数据,而是对每一个数据做了一个标志(1表示存在,0表示不存在),来实现去重。
也可以自己实现一个BitMap,参考
public static int getUniqueNum(int[] array) { if(null == array || array.length == 0) array = new int [] {1, 0, 5, 1, 8, 10, 5, 20}; BitSet bitSet = new BitSet(1); System.out.println(bitSet.size()); //64 for(int i=0;i<array.length;i++) { // 将指定索引处的位设置为 true bitSet.set(array[i], true); } int uv = bitSet.cardinality(); System.out.println("去重结果" + uv); return uv; }
- 使用HyperLogLog,提供了不精确的去重计数方案,误差在0.81%
-
容器型数据结构有哪些?
list/set/hash/sortedSet 这四种。容器型数据结构遵循以下规则:
- create if not exists
- drop if no elements
过期时间设置
以上所有的数据结构均可设置过期时间,只要在set添加后,设置expire时间即可
注意:当设置了过期时间后,继续调用set方法修改了它,会使之前设置的过期时间失效
@Test
public void test() {
stringRedisTemplate.set("name", "Jerry");
System.out.println(stringRedisTemplate.get("name"));
stringRedisTemplate.expire("name", 5);
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stringRedisTemplate.get("name"));
//output: Jerry
//output: null
}
以上是非原子性操作的过期时间设置
来一份面试题: