1.基础底层数据结构
1.1.简单动态字符串SDS
定义:
struct sdshdr{
int len;
int free;
char buf[];
}
优势:
为了重用部分C语言函数库功能,在buf里存储了空字符‘\0’,但是不同于C char[]的是:
- 取sds长度时,直接从len中获取,不是像C中遍历buf,直到遇到空字符结束。
- 在改变sds内容时,如果当前的buf能容纳得下新内容,则不重新分配内存,否则进行内存重分配,分配的原则为:当内容小于1M时,分配两倍实际大小+1(存空字符)的大小,当长度大于1M时,只多分配1M。
- 针对sds的操作是安全的。在操作时,会首先检测buf的大小是否能完成此次操作,如果不行,则进行内存扩展。
惰性内存回收:
当sds内存缩减时,并不立即回收buf中多余的空间,而是只是修改free,len的值。
1.2.链表
同数据结构中的链表
1.3.字典(hash表)
定义:同数据结构中的hash表
解决冲突的方法:拉链法
结构:
struct dict{
dicType *type;
void *private;
dictht ht[2];//rehash时,有一个做临时的存储
int trehashidx;//在rehash时,已经保存到的index号
}dic;//字典结构,主要使用了dictht哈希表实现
哈希表结构: struct dicht{
dictEntry **table;
unsigned long size;
unsigened long sizemask;
unsigned long used;
}dictht;
哈希表节点: struct dictEntry{
void *key;
union{
void *val;
int64 ——tu64;;
int64_ts64;
}v;
struct dictEntry *next;
}dictEntry;
rehash
保证loadfactor=1/2
实现:通过 dictht ht[2];和trehashidx;这两个变量,首先为ht[1]分配新的内存空间,然后将ht[0]中的值拷贝到ht[1]中,然后将ht[1]和ht[0]交换,释放掉ht[1],为ht[1]分配一个新的空的哈希表
在redis中,rehash是渐进的(因为如果字典中保存的键值对比较多,则rehash时,可能需要非常多的时间,所以,在每次拷贝一点后,将当前拷贝的索引号保存在trehashidx中,并且以后插入的话,直接插入到ht[1]中。在rehash过程中,每次执行一个添加,删除,查找或者更新操作后,拷贝一个trehashidx链,然后更新trehashidx,直到rehash结束,将trehashidx置为-1)
1.4.跳跃表
为了支持有序集合而实现。同数据结构中的跳跃表
优势:简单,比起红黑树,二叉平衡树来说
总结:是有序集合的底层实现
1.5.整数集合*
用来保存整数值集合的抽象数据结构,是整数集合的底层实现方式之一
定义:
struct intset{
unit32_t encoding;
uint_t length;
int8_t contends[]
}intset;
1.6.压缩列表(遍历时,是反向遍历的)
定义:构成
zlbytes:记录整个压缩列表占用的内存字节数
zltail:记录压缩列表表尾节点距离压缩列表的起始地址多少字节。通过这个,可以方便得到表尾节点的地址
zllen:节点数量
entryx:具体的节点
zlen:特殊字符,表示压缩类别结束
Entry 节点构成:
优势:节约内存
总结:用作list 和哈希表的底层实现之一
1.7.对象
定义:Redis并没有直接使用上述的数据结构,而是基于这些数据结构创建了一个对象系统,包括字符串对象、列表对象、哈希对象、集合对象、有序集合对象这五中类型。
Redis是一个键-值对 数据库,每次在Redis数据库中创建一个新的键值对时,Redis至少会创建两个对象,一个是对象的key对象,另一个是value对象。
对于key来说,就是一个字符串对象,value可以为上面说的5中对象之一。
结构:
struct redisObject{
unsigned type:4;// 表面的类型
unsigned encoding:4;//底层实现时,使用的类型
void *ptr;//指向底层额数据结构指针
使用type 命令可以查看对象的类型(看的是value的类型)
type和encoding:
关于Redis_Encoding_raw 和 Redis_Encoding_embstr: 都是sds对象,只是embstr用来存储字符串长度小于等于32字节的字符串值。但是,在使用embstr类型保存对象时,只调用一次内存分配函数,直接分配redisobject 和sdshdr内存,而使用raw时,需要两次内存分配,第一次为redisobject分配内存,另一次为sdshdr分配内存。embstr是用来优化小字符串的。
不同value类型支持的操作:
string:
set:
get:
setrange:
getrange:
append:
incrbyfloat:只支持能转化成数值型的value
incrby::只支持能转化成数值型的value
decrby::只支持能转化成数值型的value
strlen:
list:
lpush:
rpush:
lpop:
rpop:
lindex:
llen:
linsert:
ltrim::
lset:
hash:
hset:
hget:
hexists:
hdel:
hlen:
hgetall:
set:
sadd:
scard:返回键数量
sismember:
smembers:遍历整个集合,返回集合元素
srandmember:随机返回一个元素
spop:
srem:
:
:
zset:
zadd:
zcard:
zcount:
zrange:从表头到表尾遍历整个列表,返回给定索引范围内的所有元素。
zrevrange:
zrank:
zrevrank:
zrem:
zscore:
2.windows下环境搭建
2.1 download and install
下载地址github.com/dmajkic/redis/downloads
- 在列表中搜索,下载到的Redis支持32bit和64bit。
- 根据自己实际情况选择,我选择32bit。把32bit文件内容拷贝到需要安装的目录下,比如:D:\dev\redis-2.4.5。
- 打开一个cmd窗口,使用cd命令切换到指定目录(D:\dev\redis-2.4.5)运行 redis-server.exe redis.conf 。
- 重新打开一个cmd窗口,使用cd命令切换到指定目录(D:\dev\redis-2.4.5)运行 redis-cli.exe -h 127.0.0.1 -p 6379,其中 127.0.0.1是本地ip,6379是redis服务端的默认端口。这样,Redis windows环境下搭建已经完成。
(其实本机测试的话,直接运行redis-cli.exe就可以了,会默认连接到本机的server上去)
3.java里连接redis数据库
3.1关于Jedis
需要导入jedis jar包
关于Jedis: Redis的java版客户端实现
3.2实例
//连接redis服务器,192.168.0.100:6379
jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("password");
jedis.set("name","xinxin");//向key-->name中放入了value-->xinxin
System.out.println(jedis.get("name"));//执行结果:xinxin
jedis.append("name", " is my lover"); //拼接
System.out.println(jedis.get("name"));
jedis.del("name"); //删除某个键
System.out.println(jedis.get("name"));
//设置多个键值对
jedis.mset("name","liuling","age","23","qq","476777XXX");
jedis.incr("age"); //进行加1操作
System.out.println(jedis.get("name") + "-" + jedis.get("age") + "-" + jedis.get("qq"));
3.3.使用jedis进行事务操作
//连接redis服务器,192.168.0.100:6379
jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("password");
Transaction t = jedis.multi();
// 开启事务,当server端收到multi指令
// 会将该client的命令放入一个队列,然后依次执行,知道收到exec指令
String key="a";
String value="hello world";
t.set(key, value) ;
t.set(key+key, value) ;
t.set(key+key+key, value) ;
t.set(key+key+key, value) ;
t.set(key+key+key, value) ;
String ret = (String) t.exec().get(0);
if(ret!=null)
System.out.println(ret);
关于Redis的认证问题
如果要使用认证功能的话,则redis启动时,要开启认证功能
4.关于认证
redis启动时,如果不指定服务器配置文件,则是不需要认证的。
需要认证时,需要在配置文件中配置,加入一行 :requirepass yourpassword
然后以该配置文件启动redis.server.exe,如:
redis-server.exe redis.conf
客户端连接时,在启动 redis-cli.exe后,首先需要执行auth命令,不然,其他命令都不会执行,见:
这里输入的密码为你在配置文件中配置的密码(由此可见,所有的客户应该都是共用一个密码的)
5.redis高级功能总结
5.1 1.Redis数据库
Redis服务器中的所有数据库都保存在服务器状态redisServer数据结构中
struct redisServer{
int dbnum;
redisDb *db;
...}
在初始化时,服务器根据服务器状态dbnum属性来决定创建多少个数据库,默认为16个。
客户端使用select dbnum来切换数据库,如select 2
另外,当前redis数据库版本没有办法知道当前处于那个数据库中,因此如果应用需要经常切换数据库,为了保证操作的数据库正确,应该多执行select dbnum指令,显示表明自己操作的数据库。
在Redis中,其实每个数据库里面的内容都是使用一个字典来保存的,见redisdb数据结构
struct redisDb{
dict *dict;//保存数据库键空间
dict *expires;//过期字典
dict *watched_keys;//正在被监视的键
};
5.2.Redis数据库的键的过期时间
可以通过 expire 或是 pexpire命令设置键的失效,如:
set key value
expire key 5
//5秒之后
get key
//输出 nil
redisDb中的expires结构保存了数据库中所有键的过期时间
可以通过persist命令移除一个键的过期时间(即不加过期约束),如:
persist key
Redis过期键删除策略
同时使用惰性删除策略(使用时,检测键是否过期,过期,则删除,同时返回nil给客户端),定期删除测试(定期运行程序来删除过期键)
惰性删除:所有读写数据库的redis命令执行之前都会检测键是否过期。
定期删除:在周期性执行的函数serverCron函数中,都会调用键过期检测函数(activeExpireCycle)。
关于函数activeExpireCycle:并不检查所有过期字典中的键,而是随机的取出一定数量的键检测,并删除其中的过期键。全局变量current_db会记录当前函数检测的进度,下一次调用该函数时,会接着检测。
5. 3.Redis数据库的持久化
Redis可设置定期保存任务,服务器维持一个dirty计数和lastsave属性。如在配置文件中配置:
save 900 1
save 300 1
save 60 10000 //在60s内进行了10000次修改就保存一次
同时Redis可通过命令方式,主动发起保存,命令有:save,bgsave。其中bgsave是通过子进程方式来执行保存任务,save是通过主进程来执行保存任务,会阻塞当前操作。
保存的方式:
有两种方式:AOP,RDB
RDB:直接导出存储数据库的当前状态
AOP:保存的是数据库中执行的命令。以AOP方式进行持久化时,可以执行重写优化(本来aop会保存所有执行过的命令,但是这样太多。因此在重写时,会根据当前状态,生成相应的指令,保存这些指令,这些指令会保存在一个新文件中,在保存完后,替换原来的文件)
5.4.Redis数据库的事件
分为两种:文件事件,时间事件
文件事件:是对套接字的操作的抽象,分为读事件,写事件。
时间事件:服务器将所有的时间事件放在一个无序链表中,每当时间事件执行器运行时,就遍历这个链表,指向达到的事件。典型的时间事件为serverCron
5. 5.Redis服务器的程序的伪代码
main()
{
init_server();
while server_is_not_shutdown(): aeprecessEvents();
clean_server();
}
aeprecessEvents()
{
time_event=aesearchNearestTimer();获得离当前时间最近的一个时间事件
//获取这个事件离当前时间的距离
remaid_ms=time_event.when -unix_ts_now();
if(remaid_ms<0)//如果已经达到
remaid_ms=0;
//创建时间结构体
timeval=create_timeval_with_ms(remaind_ms)
//阻塞并等待文件事件的发生,最大阻塞时间由传入的timeval结构觉得,如果remaind_ms的值为0,那么aeAPIpoll调用之后马上返回,不阻塞
aeAPIpoll(timeval);
//处理文件事件
processfileEvents();
//处理时间事件
processTimeEvents();
}
总结:
文件事件和时间事件是同步,有序,原子的执行的,不会中途中断处理事件,也没有抢占机制。
服务器在一般情况下,只会执行一个时间时间
时间事件的实际处理通常会比设定的达到时间晚一些。
5. 6.Redis数据库的事务
Redis数据库通过multi,exec,watch命令来实现事务功能,将多个命令请求打包,然后一次性的、按顺序的执行多个命令。
例子:
一个事务会经历三个阶段:(redis的每个客户端都会有自己的事务状态,保存事务的队列信息)
- a.开始事务
- b.事务入队
- c.事务执行
命令的处理流程:
使用watch命令监视数据库键:
当你开启了事务,且还没有提交事务时,使用watch命令监视你要操作的键。如果事务提交之前,监视的键被别的客户端修改了,则你的时候就会失效。
判断事务的安全性:
redis数据库redisDb数据结构中的 dict *watched_keys字典保存该数据库被监视的键,通过这个字典,能知道那个键被那个客户端监视,当这些键被修改后,将对应的客户端的redis_dirty_cas标志置位。但该客户端提交事务时,会检查这个标志。
Redis事务的特性
Redis事务不具有持久性。且不支持回滚。
当一个事务的命令在入队时,出现了错误,如命令不存在或是参数不对,则Redis拒绝执行这个事务(还是可以提交这个事务,但是你执行exec时,会报错)
当事务在执行时,如果某个命令出错,不会中断该事务其他命令的执行,也不会回滚该事务的错误操作,且出错的命令不会影响后面命令的执行。