Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
可以是字符串,整数或者浮点数,对整个字符串或者字符串中的一部分执行操作,对整个整数或者浮点执行自增(increment)或者自减(decrement)操作。
字符串命令:
①get、获取存储在指定键中的值
②set、设置存储在指定键中的值
③del、删除存储在指定键中的值(这个命令可以用于所有的类型)
④incr、加一
⑤decr、减一
一个链表,链表上的每个节点都包含了一个字符串,从链表的两端推入或者弹出元素,根据偏移量对链表进行修剪(trim),读取单个或者多个元素,根据值查找或者移除元素。
列表命令:
①rpush、将给定值推入列表的右端
②lrange、获取列表在指定范围上的所有值
③lindex、获取列表在指定范围上的单个元素
④lpop、从列表的左端弹出一个值,并返回被弹出的值
⑤llen 返回列表的长度
包含字符串的无序收集器(unordered collection)、并且被包含的每个字符串都是独一无二的。添加,获取,移除单个元素,检查一个元素是否存在于集合中,计算交集,并集,差集,从集合里面随机获取元素。
集合命令:
①sadd、将给定元素添加到集合
②smembers、返回集合包含的所有元素
③sismember、检查指定元素是否存在于集合中
④srem、检查指定元素是否存在于集合中,那么移除这个元素
包含键值对无序散列表,添加,获取,移除当键值对,获取所有键值对。
散列命令:
①hset、在散列里面关联起指定的键值对
②hget、获取指定散列键的值
③hgetall、获取散列包含的所有键值对
④hdel、如果给定键存在于散列里面,那么移除这个键
字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定。添加,获取,删除单个元素,根据分值范围(range)或者成员来获取元素。
有序集合命令:
①zadd、将一个带有给定分值的成员添加到有序集合里面
②zrange、根据元素在有序排列中所处的位置,从有序集合里面获取多个元素
③zrangebyscore、获取有序集合在给定分值范围内的所有元素
④zrem、如果指定成员存在于有序集合中,那么移除这个成员
简单的key存储,如 zhangsan 的存储,此时普通的需求可以满足;然而在实际业务中,往往key键的存储会非常的复杂,比如我们现在有一个需求:
++需求:根据基础数据系统中的数据字典类型查询对应的字典集合++
这时,我们需要关注的业务就变得复杂了,就不能使用常规的key键存储方式,上面的需求大致可以拆分为:
1.系统:基础数据系统
2.模块:数据字典
3.方法:根据数据字典类型查询
4.参数:字典类型
为什么要这样拆分呢?为了可读性;也为了抽象出key存储规则;因为业务复杂情况下,我们定义的key键太多时就不便于管理,也不便于查找,以 系统-模块-方法-参数 这样的规则定义,我们可以很清晰的了解redis key存储的值是做了什么事情,而且rdm中也可以以此来分组,后面会讲到。
下面贴上根据此规则定义抽象出的redis工具类:
package com.yclimb.mdm.redis;
/**
* Redis 工具类
*
* @author yclimb
* @date 2018/4/19
*/
public class RedisUtils {
/**
* 主数据系统标识
*/
public static final String KEY_PREFIX = "mdm";
/**
* 分割字符,默认[:],使用:可用于rdm分组查看
*/
private static final String KEY_SPLIT_CHAR = ":";
/**
* redis的key键规则定义
* @param module 模块名称
* @param func 方法名称
* @param args 参数..
* @return key
*/
public static String keyBuilder(String module, String func, String... args) {
return keyBuilder(null, module, func, args);
}
/**
* redis的key键规则定义
* @param module 模块名称
* @param func 方法名称
* @param objStr 对象.toString()
* @return key
*/
public static String keyBuilder(String module, String func, String objStr) {
return keyBuilder(null, module, func, new String[]{objStr});
}
/**
* redis的key键规则定义
* @param prefix 项目前缀
* @param module 模块名称
* @param func 方法名称
* @param objStr 对象.toString()
* @return key
*/
public static String keyBuilder(String prefix, String module, String func, String objStr) {
return keyBuilder(prefix, module, func, new String[]{objStr});
}
/**
* redis的key键规则定义
* @param prefix 项目前缀
* @param module 模块名称
* @param func 方法名称
* @param args 参数..
* @return key
*/
public static String keyBuilder(String prefix, String module, String func, String... args) {
// 项目前缀
if (prefix == null) {
prefix = KEY_PREFIX;
}
StringBuilder key = new StringBuilder(prefix);
// KEY_SPLIT_CHAR 为分割字符
key.append(KEY_SPLIT_CHAR).append(module).append(KEY_SPLIT_CHAR).append(func);
for (String arg : args) {
key.append(KEY_SPLIT_CHAR).append(arg);
}
return key.toString();
}
/**
* redis的key键规则定义
* @param redisEnum 枚举对象
* @param objStr 对象.toString()
* @return key
*/
public static String keyBuilder(RedisEnum redisEnum, String objStr) {
return keyBuilder(redisEnum.getKeyPrefix(), redisEnum.getModule(), redisEnum.getFunc(), objStr);
}
}
上面代码中有此文字描述 分割字符,默认[:],使用:可用于rdm分组查看 ;redis key默认使用冒号分割,好处在于可以在rdm中以文件夹的形式分组查看,如图:
从可靠性的角度来说,redis支持持久化,有快照和AOF两种方式,而memcache是纯的内存存储,不支持持久化的。所以memcache断点后数据会丢失。
从数据结构上来说,redis在kv模式上,支持5中数据结构,String、list、hash、set、zset,并支持很多相关的计算,比如排序、阻塞等,而memcache只支持kv简单存储。所以当你的缓存中不只需要存储kv模型的数据时,redis丰富的数据操作空间,绝对是非常好的选择,另外说一句,利用redis可以高效的实现类似于单集群下的阻塞队列、锁及线程通信等功能。redis的value最大可以达到1GB,而memcache只有1MB
从内存管理方面来说,redis也有自己的内存机制,redis采用申请内存的方式,会把带过期时间的数据存放到一起,redis理论上能够存储比物理内存更多的数据,当数据超量时,会引发swap,把冷数据刷到磁盘上。而memcache把所有的数据存储在物理内存里。memcache使用预分配池管理,会提前把内存分为多个slab,slab又分成多个不等大小的chunk,chunk从最小的开始,根据增长因子增长内存大小。redis更适合做数据存储,memcache更适合做缓存,memcache在存储速度方面也会比redis这种申请内存的方式来的快。
从数据一致性来说,memcache提供了cas命令,可以保证多个并发访问操作同一份数据的一致性问题。 redis是串行操作,所以不用考虑数据一致性的问题。
从IO角度来说,redis选用的I/O多路复用模型,虽然单线程不用考虑锁等问题,但是还要执行kv数据之外的一些排序、聚合功能,复杂度比较高。memcache也选用非阻塞的I/O多路复用模型,速度更快一些。(IO多路复用中有三种方式:select,poll,epoll。需要注意的是,select,poll是线程不安全的,epoll是线程安全的。
redis内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间)
从线程角度来说,memcahce使用多线程,主线程listen,多个worker子线程执行读写,可能会出现锁冲突。redis是单线程的,这样虽然不用考虑锁对插入修改数据造成的时间的影响,但是无法利用多核提高整体的吞吐量,只能选择多开redis来解决。
从集群方面来说,redis天然支持高可用集群,支持主从,而memcache需要自己实现类似一致性hash的负载均衡算法才能解决集群的问题,扩展性比较低。
另外,redis集成了事务、复制、lua脚本等多种功能,功能更全。redis功能这么全,是不是什么情况下都使用redis就行了呢?
非也,redis确实比memcache功能更全,集成更方便,但是memcache相比redis在内存、线程、IO角度来说都有一定的优势,可以利用cpu提高机器性能,在不考虑扩展性和持久性的访问频繁的情况下,只存储kv格式的数据,建议使用memcache,memcache更像是个缓存,而redis更偏向与一个存储数据的系统。
LevelDB是Google开源的持久化KV单机数据库,具有很高的随机写,顺序读/写性能,但是随机读的性能很一般,也就是说,LevelDB很适合应用在查询较少,而写很多的场景。LevelDB应用了LSM (Log Structured Merge) 策略,lsm_tree对索引变更进行延迟及批量处理,并通过一种类似于归并排序的方式高效地将更新迁移到磁盘,降低索引插入开销,关于LSM,本文在后面也会简单提及。
根据Leveldb官方网站的描述,LevelDB的特点和限制如下:
特点:
限制:
内存中有两个表MemTable和Immutable MemTable
MemTable代表当前活跃的表,它主要包含Log, Manifest,以及Current三个硬盘文件,以及内存中的一个跳表SkipList。
Log是用来记录用户的写入或者删除操作,先写入log文件(按操作的顺序),再写入MemTable的SkipList中(根据key有序插入到相应的跳表位置)。
当MemTable的容量达到一定程序后,此Memtable被转换为Immutable MemTable,仍然在内存中,但可读不可写。新的MemTable被创建,并用来服务新的写入请求,至于Immutable MemTable什么时候会被写入到硬盘中,可参见数据的合并中simple compaction操作。
硬盘中有多级Level的数据表,叫做SSTable
它从Level 0 到Level N,每一个级别都可能有多个sst数据表。
Level 1到Level N: 它们的每级内部的数据表之间的key是无交叉的(具体参见数据的合并)。
Level 0: Level 0比较特别,它的多个数据表的key有交叉。Level 0的每一个数据表都是直接来源于Immutable MemTable,当系统进行记录的简单合并操作时候,直接将Immutable MemTable中的跳表转换为一个sst,因此Level 0中的多个sst的key可能有交叉。
数据的合并Compaction
当需要删除数据时候,系统直接插入删除标记即可,那旧的数据什么时候才会被清除呢?这就要用到Compaction操作。有两种合并,一种是simple,一种是major。
simple compaction操作就是Immutable MemTable中的跳表转换为Level 0的一个sst(内存到硬盘)。
major compaction操作指的是Level L与Level L+1的合并操作,是层级之间的合并操作(硬盘到硬盘)。当L >=1的时候,首先选取Level L中的一个数据表,然后寻找Level L+1中的所有与Level L的key有交叉的数据表,进行多路合并。合并的一个原则就是,如果Level L中有一个key在Level L+1中存在,那么将Level L+1中的所有这样的key记录都删除即可。当L = 0,它和Level 1的合并有些特殊,我们需要选取Level 0的多个数据表(由于Level 0的key是有交叉的),与Level 1的多个数据表进行合并。
数据表有多个数据块,存储有Block的索引,以及相应Block的数据。Block的大小为32KB。
有两个级别的缓存,数据表的索引缓存,数据Block的缓存。索引的缓存是默认,Block的缓存可配置。
跳表是在内存中,利用了多级链表的结构,查找和插入效率高,比平衡树减少了很多节点移动的操作,因此插入速度极快;日志的写入虽然是磁盘写入,但由于是顺序写入,因此性能也很好。
首先查找MemTable和Immutable MemTable,没有找到的话进入cache中查找,仍然没有找到,那么只能通过硬盘查找了。
硬盘查找首先查找Level 0,如果没有继续Level 1,直到最后一层Level,还没找到,那么不存在,如果中间任何一个地方找到了,那么直接返回。这个顺序时根据数据的新旧顺序而来的。 对于某一个Level的数据表具体查找如何执行的呢? 首先载入这个数据表的索引到内存中,查看key位于哪一个Block中,然后将相应的Block载入到内存中,逐个查找记录。 至少需要两次硬盘读取操作,很慢,顺序读因为有缓存的缘故,性能相对较好。