目录
NoSQL数据库
NoSQL,泛指非关系型的数据库,NoSQL即Not-Only SQL,它可以作为关系型数据库的良好补充。
随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。
Redis的发展历史
2008年,意大利的一家创业公司Merzia推出了一款基于MySQL的网站实时统计系统LLOOGG,然而没过多久该公司的创始人 Salvatore Sanfilippo便 对MySQL的性能感到失望,于是他决定亲自为LLOOGG量身定做一个数据库,并于2009年开发完成,这个数据库就是Redis。
不过Salvatore Sanfilippo并不满足只将Redis用于LLOOGG这一款产品,而是希望更多的人使用它,于是在同一年Salvatore Sanfilippo将Redis开源发布,并开始和Redis的另一名主要的代码贡献者Pieter Noordhuis一起继续着Redis的开发,直到今天。
Salvatore Sanfilippo自己也没有想到,短短的几年时间,Redis就拥有了庞大的用户群体。Hacker News在2012年发布了一份数据库的使用情况调查,结果显示有近12%的公司在使用Redis。国内如新浪微博、街旁网、知乎网,国外如GitHub、Stack Overflow、Flickr等都是Redis的用户。
VMware公司从2010年开始赞助Redis的开发, Salvatore Sanfilippo和Pieter Noordhuis也分别在3月和5月加入VMware,全职开发Redis。
Redis概述
Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库。
它通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:
- 字符串类型(string)
- 散列类型(hash)
- 列表类型(list)
- 集合类型(set)
- 有序集合类型(sortset)
Jedis
Redis不仅是使用命令来操作,现在基本上主流的语言都有客户端支持,比如java、C、C#、C++、php、Node.js、Go等。
在官方网站里列一些Java的客户端,有Jedis、Redisson、Jredis、JDBC-Redis、等其中官方推荐使用Jedis和Redisson。 在企业中用的最多的就是Jedis,下面我们就重点学习下Jedis。
Jedis同样也是托管在github上,地址:https://github.com/xetorthio/jedis
依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.0</version>
</dependency>
Jedis的常用API
单实例连接
通过创建单实例jedis对象连接redis服务,如下代码:
// 单实例连接redis
@Test
public void testJedisSingle() {
Jedis jedis = new Jedis("192.168.101.3", 6379);
jedis.set("name", "bar");
String name = jedis.get("name");
System.out.println(name);
jedis.close();
}
使用连接池连接
通过单实例连接redis不能对redis连接进行共享,可以使用连接池对redis连接进行共享,提高资源利用率,使用jedisPool连接redis服务,如下代码:
@Test
public void pool() {
JedisPoolConfig config = new JedisPoolConfig();
//最大连接数
config.setMaxTotal(30);
//最大连接空闲数
config.setMaxIdle(2);
JedisPool pool = new JedisPool(config, "192.168.101.3", 6379);
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.set("name", "lisi");
String name = jedis.get("name");
System.out.println(name);
}catch(Exception ex){
ex.printStackTrace();
}finally{
if(jedis != null){
//关闭连接
jedis.close();
}
}
}
数据类型string
概述
redis中没有使用C语言的字符串表示,而是自定义一个数据结构叫SDS(simple dynamic string)即简单动态字符串。
打开下载的redis源码包,找到src下的sds.h文件查看sds源码:
struct sdshdr {
//字符串长度
unsigned int len;
//buf数组中未使用的字节数量
unsigned int free;
//用于保存字符串
char buf[];
};
c语言对字符串的存储是使用字符数组,遇到’\0’字符则认为字符串结束,redis的字符串可以存储任何类型的数据,因为任何类型数据都可以表示成二进制,sds结构中的char buf[]就是存储了二进制数据。
redis的字符串是二进制安全的,什么是二进制安全?简单理解就是存入什么数据取出的还是什么数据。redis中的sds不像c语言处理字符串那样遇到’\0’字符则认证字符串结束,它不会对存储进去的二进制数据进行处理,存入什么数据取出还是什么数据。
命令行命令
赋值与取值
SET key value
GET key
127.0.0.1:6379> set test 123
OK
127.0.0.1:6379> get test
"123“
当键不存在时返回空结果
向尾部追加值
APPEND key value
APPEND的作用是向键值的末尾追加value。如果键不存在则将该键的值设置为value,即相当于 SET key value。返回值是追加后字符串的总长度。
127.0.0.1:6379> set str hello
OK
127.0.0.1:6379> append str " world!"
(integer) 12
127.0.0.1:6379> get str
"hello world!"
获取字符串长度
STRLEN key
STRLEN命令返回键值的长度,如果键不存在则返回0。
127.0.0.1:6379> strlen str
(integer) 0
127.0.0.1:6379> set str hello
OK
127.0.0.1:6379> strlen str
(integer) 5
同时设置/获取多个键值
MSET key value [key value …]
MGET key [key …]
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> mget k1 k3
1) "v1"
2) "v3"
Jedis的常用操作
取值和赋值
public static void Hello() {
Jedis jedis = RedisUtil.getJedis();
try {
// 向key-->name中放入了value-->minxr
jedis.set("name", "minxr");
String ss = jedis.get("name");
System.out.println(ss);
// 很直观,类似map 将jintao append到已经有的value之后
jedis.append("name", "jintao");
ss = jedis.get("name");
System.out.println(ss);
// 2、直接覆盖原来的数据
jedis.set("name", "jintao");
System.out.println(jedis.get("name"));
// 删除key对应的记录
jedis.del("name");
System.out.println(jedis.get("name"));// 执行结果:null
/**
* mset相当于 jedis.set("name","minxr"); jedis.set("jarorwar","aaa");
*/
jedis.mset("name", "minxr", "jarorwar", "aaa");
System.out.println(jedis.mget("name", "jarorwar"));
} catch (Exception e) {
e.printStackTrace();
} finally {
//RedisUtil.getPool().returnResource(jedis);
RedisUtil.closeJedis(jedis);
}
}
public static void testString() {
System.out.println("==String==");
Jedis jedis = RedisUtil.getJedis();
try {
// String
jedis.set("key", "Hello World!");
String value = jedis.get("key");
System.out.println(value);
System.out.println("=============String==========================");
// 清空数据
System.out.println(jedis.flushDB());
// 存储数据
jedis.set("foo", "bar");
System.out.println(jedis.get("foo"));
// 若key不存在,则存储
jedis.setnx("foo", "foo not exits");
System.out.println(jedis.get("foo"));
// 覆盖数据
jedis.set("foo", "foo update");
System.out.println(jedis.get("foo"));
// 追加数据
jedis.append("foo", " hello, world");
System.out.println(jedis.get("foo"));
// 设置key的有效期,并存储数据
jedis.setex("foo", 2, "foo not exits");
System.out.println(jedis.get("foo"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println(jedis.get("foo"));
// 获取并更改数据
jedis.set("foo", "foo update");
System.out.println(jedis.getSet("foo", "foo modify"));
// 截取value的值
System.out.println(jedis.getrange("foo", 1, 3));
System.out.println(jedis.mset("mset1", "mvalue1", "mset2", "mvalue2",
"mset3", "mvalue3", "mset4", "mvalue4"));
System.out.println(jedis.mget("mset1", "mset2", "mset3", "mset4"));
System.out.println(jedis.del(new String[] { "foo", "foo1", "foo3" }));
} catch (Exception e) {
e.printStackTrace();
} finally {
RedisUtil.getPool().returnResource(jedis);
}
}
判断值的存在
private static void testKey() {
Jedis jedis = RedisUtil.getJedis();
System.out.println("=============key==========================");
// 清空数据
System.out.println(jedis.flushDB());
System.out.println(jedis.echo("foo"));
// 判断key否存在
System.out.println(jedis.exists("foo"));
jedis.set("key", "values");
System.out.println(jedis.exists("key"));
}
数据类型hash
hash叫散列类型,它提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型、集合类型等其它类型。如下:
命令行命令
赋值与取值
HSET key field value 一次只能设置一个字段值
HGET key field 一次只能获取一个字段值
HMSET key field value [field value ...] 一次可以设置多个字段值
HMGET key field [field ...] 一次可以获取多个字段值
HGETALL key
127.0.0.1:6379> hset user username zhangsan
(integer) 1
127.0.0.1:6379> hget user username
"zhangsan“
HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0
127.0.0.1:6379> hmset user age 20 username lisi
OK
127.0.0.1:6379> hmget user age username
1) "20"
2) "lisi"
127.0.0.1:6379> hgetall user
1) "age"
2) "20"
3) "username"
4) "lisi"
判断字段是否存在
HEXISTS key field
127.0.0.1:6379> hexists user age 查看user中是否有age字段
(integer) 1
127.0.0.1:6379> hexists user name 查看user中是否有name字段
(integer) 0
HSETNX key field value
当字段不存在时赋值,类似HSET,区别在于如果字段已经存在,该命令不执行任何操作。
127.0.0.1:6379> hsetnx user age 30 如果user中没有age字段则设置age值为30,否则不做任何操作
(integer) 0
删除字段
可以删除一个或多个字段,返回值是被删除的字段个数
HDEL key field [field ...]
127.0.0.1:6379> hdel user age
(integer) 1
127.0.0.1:6379> hdel user age name
(integer) 0
127.0.0.1:6379> hdel user age username
(integer) 1
只获取字段名或字段值
HKEYS key
HVALS key
127.0.0.1:6379> hmset user age 20 name lisi
OK
127.0.0.1:6379> hkeys user
1) "age"
2) "name"
127.0.0.1:6379> hvals user
1) "20"
2) "lisi"
获取字段数量
HLEN key
127.0.0.1:6379> hlen user
(integer) 2
Jedis应用
public static void testHsh() {
System.out.println("==Hash==");
Jedis jedis = RedisUtil.getJedis();
try {
Map<String, String> pairs = new HashMap<String, String>();
pairs.put("name", "Akshi");
pairs.put("age", "2");
pairs.put("sex", "Female");
jedis.hmset("kid", pairs);
List<String> name = jedis.hmget("kid", "name");// 结果是个泛型的LIST
System.out.println(name);
jedis.hdel("kid","age"); //删除map中的某个键值
System.out.println(jedis.hmget("kid", "pwd")); // 因为删除了,所以返回的是null
System.out.println(jedis.hlen("kid")); // 返回key为user的键中存放的值的个数
System.out.println(jedis.exists("kid"));// 是否存在key为user的记录
System.out.println(jedis.hkeys("kid"));// 返回map对象中的所有key
System.out.println(jedis.hvals("kid"));// 返回map对象中的所有value
Iterator<String> iter = jedis.hkeys("kid").iterator();
while (iter.hasNext()) {
String key = iter.next();
System.out.println(key + ":" + jedis.hmget("kid", key));
}
List<String> values = jedis.lrange("messages", 0, -1);
values = jedis.hmget("kid", new String[] { "name", "age", "sex" });
System.out.println(values);
Set<String> setValues = jedis.zrange("hackers", 0, -1);
setValues = jedis.hkeys("kid");
System.out.println(setValues);
values = jedis.hvals("kid");
System.out.println(values);
pairs = jedis.hgetAll("kid");
System.out.println(pairs);
// 清空数据
System.out.println(jedis.flushDB());
// 添加数据
jedis.hset("hashs", "entryKey", "entryValue");
jedis.hset("hashs", "entryKey1", "entryValue1");
jedis.hset("hashs", "entryKey2", "entryValue2");
// 判断某个值是否存在
System.out.println(jedis.hexists("hashs", "entryKey"));
// 获取指定的值
System.out.println(jedis.hget("hashs", "entryKey")); // 批量获取指定的值
System.out.println(jedis.hmget("hashs", "entryKey", "entryKey1"));
// 删除指定的值
System.out.println(jedis.hdel("hashs", "entryKey"));
// 为key中的域 field 的值加上增量 increment
System.out.println(jedis.hincrBy("hashs", "entryKey", 123l));
// 获取所有的keys
System.out.println(jedis.hkeys("hashs"));
// 获取所有的values
System.out.println(jedis.hvals("hashs"));
} catch (Exception e) {
e.printStackTrace();
} finally {
RedisUtil.getPool().returnResource(jedis);
}
}
数据类型list
列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段。
列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。
命令行命令
向列表两端增加元素
LPUSH key value [value ...]
RPUSH key value [value ...]
向列表左边增加元素
127.0.0.1:6379> lpush list:1 1 2 3
(integer) 3
向列表右边增加元素
127.0.0.1:6379> rpush list:1 4 5 6
(integer) 3
从列表两端弹出元素
LPOP key
RPOP key
LPOP命令从列表左边弹出一个元素,会分两步完成,第一步是将列表左边的元素从列表中移除,第二步是返回被移除的元素值。
127.0.0.1:6379> lpop list:1
"3“
127.0.0.1:6379> rpop list:1
"6“
获取列表中元素的个数
LLEN key
127.0.0.1:6379> llen list:1
(integer) 2
获取列表片段
LRANGE key start stop
LRANGE命令是列表类型最常用的命令之一,获取列表中的某一片段,将返回start、stop之间的所有元素(包含两端的元素),索引从0开始。索引可以是负数,如:“-1”代表最后边的一个元素。
127.0.0.1:6379> lrange list:1 0 2
1) "2"
2) "1"
3) "4"
删除列表中指定的值
LREM key count value
LREM命令会删除列表中前count个值为value的元素,返回实际删除的元素个数。根据count值的不同,该命令的执行方式会有所不同:
- 当count>0时, LREM会从列表左边开始删除。
- 当count<0时, LREM会从列表后边开始删除。
- 当count=0时, LREM删除所有值为value的元素。
获得/设置指定索引的元素值
LINDEX key index
LSET key index value
127.0.0.1:6379> lindex l:list 2
"1"
127.0.0.1:6379> lset l:list 2 2
OK
127.0.0.1:6379> lrange l:list 0 -1
1) "6"
2) "5"
3) "2"
4) "2"
只保留列表指定片段,指定范围和LRANGE一致
LTRIM key start stop
127.0.0.1:6379> lrange l:list 0 -1
1) "6"
2) "5"
3) "0"
4) "2"
127.0.0.1:6379> ltrim l:list 0 2
OK
127.0.0.1:6379> lrange l:list 0 -1
1) "6"
2) "5"
3) "0"
向列表中插入元素
LINSERT key BEFORE|AFTER pivot value
该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面。
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> linsert list after 3 4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
4) "1"
将元素从一个列表转移到另一个列表中
RPOPLPUSH source destination
127.0.0.1:6379> rpoplpush list newlist
"1"
127.0.0.1:6379> lrange newlist 0 -1
1) "1"
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
Jedis应用
public static void testList() {
System.out.println("==List==");
Jedis jedis = RedisUtil.getJedis();
try {
// 开始前,先移除所有的内容
jedis.del("messages");
jedis.rpush("messages", "Hello how are you?");
jedis.rpush("messages", "Fine thanks. I'm having fun with redis.");
jedis.rpush("messages", "I should look into this NOSQL thing ASAP");
// 再取出所有数据jedis.lrange是按范围取出,
// 第一个是key,第二个是起始位置,第三个是结束位置,jedis.llen获取长度 -1表示取得所有
List<String> values = jedis.lrange("messages", 0, -1);
System.out.println(values);
// 清空数据
System.out.println(jedis.flushDB());
// 添加数据
jedis.lpush("lists", "vector");
jedis.lpush("lists", "ArrayList");
jedis.lpush("lists", "LinkedList");
// 数组长度
System.out.println(jedis.llen("lists"));
// 排序
//System.out.println(jedis.sort("lists"));
// 字串
System.out.println(jedis.lrange("lists", 0, 3));
// 修改列表中单个值
jedis.lset("lists", 0, "hello list!");
// 获取列表指定下标的值
System.out.println(jedis.lindex("lists", 1));
// 删除列表指定下标的值
System.out.println(jedis.lrem("lists", 1, "vector"));
// 删除区间以外的数据
System.out.println(jedis.ltrim("lists", 0, 1));
// 列表出栈
System.out.println(jedis.lpop("lists"));
// 整个列表值
System.out.println(jedis.lrange("lists", 0, -1));
} catch (Exception e) {
e.printStackTrace();
} finally {
RedisUtil.getPool().returnResource(jedis);
}
}
数据类型set
在集合中的每个元素都是不同的,且没有顺序。
集合类型和列表类型的对比:
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型的Redis内部是使用值为空的散列表实现,所有这些操作的时间复杂度都为0(1)。
Redis还提供了多个集合之间的交集、并集、差集的运算。
命令行命令
增加/删除元素
SADD key member [member ...]
SREM key member [member ...]
127.0.0.1:6379> sadd set a b c
(integer) 3
127.0.0.1:6379> sadd set a
(integer) 0
127.0.0.1:6379> srem set c d
(integer) 1
获得集合中的所有元素
SMEMBERS key
127.0.0.1:6379> smembers set
1) "b"
2) "a”
判断元素是否在集合中
无论集合中有多少元素都可以极速的返回结果
SISMEMBER key member
127.0.0.1:6379> sismember set a
(integer) 1
127.0.0.1:6379> sismember set h
(integer) 0
数据类型sorted set
在集合类型的基础上有序集合类型为集合中的每个元素都关联一个分数,这使得我们不仅可以完成插入、删除和判断元素是否存在在集合中,还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关的操作。
在某些方面有序集合和列表类型有些相似:
- 二者都是有序的。
- 二者都可以获得某一范围的元素。
但是,二者有着很大区别:
- 列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢。
- 有序集合类型使用散列表实现,所有即使读取位于中间部分的数据也很快。
- 列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现)
- 有序集合要比列表类型更耗内存。
命令行命令
增加元素
向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素个数,不包含之前已经存在的元素。
ZADD key score member [score member ...]
127.0.0.1:6379> zadd scoreboard 80 zhangsan 89 lisi 94 wangwu
(integer) 3
127.0.0.1:6379> zadd scoreboard 97 lisi
(integer) 0
获取元素的分数
ZSCORE key member
127.0.0.1:6379> zscore scoreboard lisi
"97"
获得排名在某个范围的元素列表
从小到大
ZRANGE key start stop [WITHSCORES]
照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
127.0.0.1:6379> zrange scoreboard 0 2
1) "zhangsan"
2) "wangwu"
3) "lisi“
从大到小
ZREVRANGE key start stop [WITHSCORES]
照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
127.0.0.1:6379> zrevrange scoreboard 0 2
1) " lisi "
2) "wangwu"
3) " zhangsan “
如果需要获得元素的分数的可以在命令尾部加上WITHSCORES参数
127.0.0.1:6379> zrange scoreboard 0 1 WITHSCORES
1) "zhangsan"
2) "80"
3) "wangwu"
4) "94"
获得指定分数范围的元素
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
127.0.0.1:6379> ZRANGEBYSCORE scoreboard 90 97 WITHSCORES
1) "wangwu"
2) "94"
3) "lisi"
4) "97"
127.0.0.1:6379> ZRANGEBYSCORE scoreboard 90 (97 WITHSCORES
1) "wangwu"
2) "94“
127.0.0.1:6379> ZRANGEBYSCORE scoreboard 70 100 limit 1 2
1) "wangwu"
2) "lisi"
增加某个元素的分数
ZINCRBY key increment member
增加某个元素的分数,返回值是更改后的分数
给lisi加4分
127.0.0.1:6379> ZINCRBY scoreboard 4 lisi
"101“
获得集合中元素的数量
ZCARD key
127.0.0.1:6379> ZCARD scoreboard
(integer) 3
获得指定分数范围内的元素个数
ZCOUNT key min max
127.0.0.1:6379> ZCOUNT scoreboard 80 90
(integer) 1
按照排名范围删除元素
ZREMRANGEBYRANK key start stop
127.0.0.1:6379> ZREMRANGEBYRANK scoreboard 0 1
(integer) 2
127.0.0.1:6379> ZRANGE scoreboard 0 -1
1) "lisi"
按照分数范围删除元素
ZREMRANGEBYSCORE key min max
127.0.0.1:6379> zadd scoreboard 84 zhangsan
(integer) 1
127.0.0.1:6379> ZREMRANGEBYSCORE scoreboard 80 100
(integer) 1
获取元素的排名
ZRANK key member
ZREVRANK key member
从小到大
127.0.0.1:6379> ZRANK scoreboard lisi
(integer) 0
从大到小
127.0.0.1:6379> ZREVRANK scoreboard zhangsan
(integer) 1
Jedis应用
public static void sortedSet() {
System.out.println("==SoretedSet==");
Jedis jedis = RedisUtil.getJedis();
try {
jedis.zadd("hackers", 1940, "Alan Kay");
jedis.zadd("hackers", 1953, "Richard Stallman");
jedis.zadd("hackers", 1965, "Yukihiro Matsumoto");
jedis.zadd("hackers", 1916, "Claude Shannon");
jedis.zadd("hackers", 1969, "Linus Torvalds");
jedis.zadd("hackers", 1912, "Alan Turing");
Set<String> setValues = jedis.zrange("hackers", 0, -1);
System.out.println(setValues);
Set<String> setValues2 = jedis.zrevrange("hackers", 0, -1);
System.out.println(setValues2);
// 清空数据
System.out.println(jedis.flushDB());
// 添加数据
jedis.zadd("zset", 10.1, "hello");
jedis.zadd("zset", 10.0, ":");
jedis.zadd("zset", 9.0, "zset");
jedis.zadd("zset", 11.0, "zset!");
// 元素个数
System.out.println(jedis.zcard("zset"));
// 元素下标
System.out.println(jedis.zscore("zset", "zset"));
// 集合子集
System.out.println(jedis.zrange("zset", 0, -1));
// 删除元素
System.out.println(jedis.zrem("zset", "zset!"));
System.out.println(jedis.zcount("zset", 9.5, 10.5));
// 整个集合值
System.out.println(jedis.zrange("zset", 0, -1));
} catch (Exception e) {
e.printStackTrace();
} finally {
RedisUtil.getPool().returnResource(jedis);
}
}
Keys命令
设置key的生存时间
Redis在实际使用过程中更多的用作缓存,然而缓存的数据一般都是需要设置生存时间的,即到期后数据销毁。
EXPIRE key seconds 设置key的生存时间(单位:秒)key在多少秒后会自动删除
TTL key 查看key生于的生存时间
PERSIST key 清除生存时间
PEXPIRE key milliseconds 生存时间设置单位为:毫秒
192.168.101.3:7002> set test 1 设置test的值为1
OK
192.168.101.3:7002> get test 获取test的值
"1"
192.168.101.3:7002> EXPIRE test 5 设置test的生存时间为5秒
(integer) 1
192.168.101.3:7002> TTL test 查看test的生于生成时间还有1秒删除
(integer) 1
192.168.101.3:7002> TTL test
(integer) -2
192.168.101.3:7002> get test 获取test的值,已经删除
(nil)
返回满足给定pattern的所有key
redis 127.0.0.1:6379> keys mylist*
1) "mylist"
2) "mylist5"
3) "mylist6"
4) "mylist7"
5) "mylist8"
确认一个key 是否存在
redis 127.0.0.1:6379> exists HongWan
(integer) 0
redis 127.0.0.1:6379> exists age
(integer) 1
redis 127.0.0.1:6379>
从结果来看数据库中不存在HongWan 这个key,但是age这个key 是存在的
删除一个key
redis 127.0.0.1:6379> del age
(integer) 1
redis 127.0.0.1:6379> exists age
(integer) 0
redis 127.0.0.1:6379>
重命名key
redis 127.0.0.1:6379[1]> keys *
1) "age"
redis 127.0.0.1:6379[1]> rename age age_new
OK
redis 127.0.0.1:6379[1]> keys *
1) "age_new"
redis 127.0.0.1:6379[1]>
age成功的被我们改名为age_new
返回值的类型
redis 127.0.0.1:6379> type addr
string
redis 127.0.0.1:6379> type myzset2
zset
redis 127.0.0.1:6379> type mylist
list
redis 127.0.0.1:6379>
Keys相关Jedis应用
public static void testOther() throws InterruptedException {
Jedis jedis = RedisUtil.getJedis();
try {
// keys中传入的可以用通配符
System.out.println(jedis.keys("*")); // 返回当前库中所有的key [sose, sanme,
// name, jarorwar, foo,
// sname, java framework,
// user, braand]
System.out.println(jedis.keys("*name"));// 返回的sname [sname, name]
System.out.println(jedis.del("sanmdde"));// 删除key为sanmdde的对象 删除成功返回1
// 删除失败(或者不存在)返回 0
System.out.println(jedis.ttl("sname"));// 返回给定key的有效时间,如果是-1则表示永远有效
jedis.setex("timekey", 10, "min");// 通过此方法,可以指定key的存活(有效时间) 时间为秒
Thread.sleep(5000);// 睡眠5秒后,剩余时间将为<=5
System.out.println(jedis.ttl("timekey")); // 输出结果为5
jedis.setex("timekey", 8, "min"); // 设为1后,下面再看剩余时间就是1了
System.out.println(jedis.ttl("timekey")); // 输出结果为1
System.out.println(jedis.exists("key"));// 检查key是否存在
System.out.println(jedis.rename("timekey", "time"));
System.out.println(jedis.get("timekey"));// 因为移除,返回为null
System.out.println(jedis.get("time")); // 因为将timekey 重命名为time
// 所以可以取得值 min
// jedis 排序
// 注意,此处的rpush和lpush是List的操作。是一个双向链表(但从表现来看的)
jedis.del("a");// 先清除数据,再加入数据进行测试
jedis.rpush("a", "1");
jedis.lpush("a", "6");
jedis.lpush("a", "3");
jedis.lpush("a", "9");
System.out.println(jedis.lrange("a", 0, -1));// [9, 3, 6, 1]
System.out.println(jedis.sort("a")); // [1, 3, 6, 9] //输入排序后结果
System.out.println(jedis.lrange("a", 0, -1));
} catch (Exception e) {
e.printStackTrace();
} finally {
RedisUtil.getPool().returnResource(jedis);
}
}
Redis服务器命令
ping
测试连接是否存活
redis 127.0.0.1:6379> ping
PONG
//执行下面命令之前,我们停止redis 服务器
redis 127.0.0.1:6379> ping
Could not connect to Redis at 127.0.0.1:6379: Connection refused
//执行下面命令之前,我们启动redis 服务器
not connected> ping
PONG
redis 127.0.0.1:6379>
第一个ping 时,说明此连接正常
第二个ping 之前,我们将redis 服务器停止,那么ping 是失败的
第三个ping 之前,我们将redis 服务器启动,那么ping 是成功的
echo
在命令行打印一些内容
redis 127.0.0.1:6379> echo HongWan
"HongWan"
redis 127.0.0.1:6379>
select
选择数据库,Redis 数据库编号从0~15,我们可以选择任意一个数据库来进行数据的存取。
redis 127.0.0.1:6379> select 1
OK
redis 127.0.0.1:6379[1]> select 16
(error) ERR invalid DB index
redis 127.0.0.1:6379[16]>
当选择16 时,报错,说明没有编号为16 的这个数据库
quit
退出连接
redis 127.0.0.1:6379> quit
dbsize
返回当前数据库中key 的数目
redis 127.0.0.1:6379> dbsize
(integer) 18
redis 127.0.0.1:6379>
结果说明此库中有18 个key
info
获取服务器的信息和统计
redis 127.0.0.1:6379> info
redis_version:2.2.12
redis_git_sha1:00000000
redis_git_dirty:0
arch_bits:32
multiplexing_api:epoll
process_id:28480
uptime_in_seconds:2515
uptime_in_days:0
。。。。
。。。。
flushdb
删除当前选择数据库中的所有key
redis 127.0.0.1:6379> dbsize
(integer) 18
redis 127.0.0.1:6379> flushdb
OK
redis 127.0.0.1:6379> dbsize
(integer) 0
redis 127.0.0.1:6379>
在本例中我们将0号数据库中的key都清除了
flushall
删除所有数据库中的所有key
redis 127.0.0.1:6379[1]> dbsize
(integer) 1
redis 127.0.0.1:6379[1]> select 0
OK
redis 127.0.0.1:6379> flushall
OK
redis 127.0.0.1:6379> select 1
OK
redis 127.0.0.1:6379[1]> dbsize
(integer) 0
redis 127.0.0.1:6379[1]>
在本例中我们先查看了一个1号数据库中有一个key,然后我切换到0号库执行flushall 命令,结果1号库中的key也被清除了,说是此命令工作正常。
redis数据库设计(重点)
在互联网项目中我们需要提高数据的访问速度,关系型数据库就满足不了我们的要求,所有我们需要使用非关系型数据库来提高查询速度。
但是我们该如何同步数据库到redis中呢
我们需要把关系型数据库变成键值对存储
球队的存储
Key的设计:在key中我们可以使用list来存储所有的球队的id
List中的key
team:team_id : [1, 2, 3, 4….]
每一支球队的存储使用hash
team:101 : [{team_id:101},{name : 公牛},{addr:芝加哥}]
team:102 : [{team_id:102},{name : 骑士},{addr:克利夫兰}]
……
每一个雇员使用hash
emp:001 : {emp_id:001, name:张三, birthday:2016-12-12, gender:1, team_id:101}
emp:002 : {emp_id:002, name:李四, birthday:2016-12-13, gender:1, team_id:101}
从球队方向关注:是一对多的关系
team:101:emp : [001, 002]
多对一关系可以直接在员工的hash结构中来获得一的一端。
从关系型数据库同步到Redis中
@Test
public void queryTeam() {
SessionFactory factory = HiberUtils.getSessionFactory();
Session session = factory.openSession();
Jedis jedis = RedisUtils.getJedis();
try {
String hql = "from Team";
Query query = session.createQuery(hql);
List<Team> tList = query.list();
for(Team team : tList){
//加入把team信息加入Redis中
jedis.hset("team:"+team.getTeamId(), "name", team.gettName());
jedis.hset("team:"+team.getTeamId(), "loc", team.getLoc());
jedis.hset("team:"+team.getTeamId(), "team_id", team.getTeamId()+"");
//把每一个team_id存储在key为team:team_id中
jedis.lpush("team:team_id", team.getTeamId()+"");
//获得emp集合
Set<Emp> eList = team.getEmpSet();
for(Emp emp : eList){
//把emp信息加入Redis中
jedis.hset("emp:"+emp.getEmpId(), "emp_id", emp.getEmpId()+"");
jedis.hset("emp:"+emp.getEmpId(), "name", emp.getEname()+"");
jedis.hset("emp:"+emp.getEmpId(), "birthday", emp.getBirthday()+"");
jedis.hset("emp:"+emp.getEmpId(), "gender", emp.getGender()+"");
jedis.hset("emp:"+emp.getEmpId(), "team_id", team.getTeamId()+"");
//存储team和temp的一对多的关系
jedis.lpush("team:"+team.getTeamId()+":emp", emp.getEmpId()+"");
}
}
} catch (Exception e) {
e.printStackTrace();
}finally{
session.close();
RedisUtils.returnJedis(jedis);
}
查询
@Test
public void queryTeam5() {
Jedis jedis = RedisUtils.getJedis();
List<Team> tList = new ArrayList<Team>();
try {
List<String> teamIds = jedis.lrange("team:team_id", 0, -1);
for(String teamId : teamIds){
//获得球队信息
String tName = jedis.hget("team:"+teamId, "name");
String loc = jedis.hget("team:"+teamId, "loc");
String teamIdStr = jedis.hget("team:"+teamId, "team_id");
Team team = new Team();
team.setLoc(loc);
team.settName(tName);
team.setTeamId(new Integer(teamId));
//获得员工的信息
List<String> empList = jedis.lrange("team:"+teamId+":emp", 0, -1);
Set<Emp> eSet = new HashSet<Emp>();
for(String empId : empList){
//获得每一个员工信息
//String empId = jedis.hget("emp:"+empId, "emp_id");
String name = jedis.hget("emp:"+empId, "name");
String birthday = jedis.hget("emp:"+empId, "birthday");
String gender = jedis.hget("emp:"+empId, "gender");
//String teamId = jedis.hget("emp:"+empId, "team_id");
Emp emp = new Emp();
emp.setEmpId(new Integer(empId));
emp.setEname(name);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
emp.setBirthday(sdf.parse(birthday));
emp.setGender(new Integer(gender));
emp.setTeam(team);
eSet.add(emp);
}
team.setEmpSet(eSet);
tList.add(team);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
RedisUtils.returnJedis(jedis);
}
}
Redis的持久化
主从复制
持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,如果通过redis的主从复制机制就可以避免这种单点故障,如下图:
- 主redis中的数据有两个副本(replication)即从redis1和从redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。
- 主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。
- 只有一个主redis,可以有多个从redis。
- 主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求
- 一个redis可以即是主又是从,如下图:
主从配置
主redis无需特殊配置
从redis配置如下
修改从redis服务器上的redis.conf文件,添加slaveof主redisip主redis端口
上边的配置说明当前该从redis服务器所对应的主redis是192.168.101.3,端口是6379
主从复制过程
完整复制
在redis2.8版本之前主从复制过程如下图:
1、slave 服务启动,slave 会建立和master 的连接,发送sync 命令。
2、master启动一个后台进程将数据库快照保存到RDB文件中
注意:此时如果生成RDB文件过程中存在写数据操作会导致RDB文件和当前主redis数据不一致,所以此时master 主进程会开始收集写命令并缓存起来。
3、master 就发送RDB文件给slave
4、slave 将文件保存到磁盘上,然后加载到内存恢复
5、master把缓存的命令转发给slave
注意:后续master 收到的写命令都会通过开始建立的连接发送给slave。
当master 和slave 的连接断开时slave 可以自动重新建立连接。如果master 同时收到多个slave 发来的同步连接命令,只会启动一个进程来写数据库镜像,然后发送给所有slave。
完整复制的问题:
在redis2.8之前从redis每次同步都会从主redis中复制全部的数据,如果从redis是新创建的从主redis中复制全部的数据这是没有问题的,但是,如果当从redis停止运行,再启动时可能只有少部分数据和主redis不同步,此时启动redis仍然会从主redis复制全部数据,这样的性能肯定没有只复制那一小部分不同步的数据高。
部分复制
从机连接主机后,会主动发起 PSYNC 命令,从机会提供 master的runid(机器标识,随机生成的一个串) 和 offset(数据偏移量,如果offset主从不一致则说明数据不同步),主机验证 runid 和 offset 是否有效, runid 相当于主机身份验证码,用来验证从机上一次连接的主机,如果runid验证未通过则,则进行全同步,如果验证通过则说明曾经同步过,根据offset同步部分数据。
Redis集群搭建
redis.conf
Redis 支持很多的参数,但都有默认值
- daemonize
默认情况下,redis 不是在后台运行的,如果需要在后台运行,把该项的值更改为yes - pidfile
当Redis 在后台运行的时候,Redis 默认会把pid 文件放在/var/run/redis.pid,你可以配置到其他地址。当运行多个redis 服务时,需要指定不同的pid 文件和端口 - bind
指定Redis 只接收来自于该IP 地址的请求,如果不进行设置,那么将处理所有请求,在生产环境中最好设置该项 - port
监听端口,默认为6379 - timeout
设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接 - loglevel
log 等级分为4 级,debug, verbose, notice, 和warning。生产环境下一般开启notice - logfile
配置log 文件地址,默认使用标准输出,即打印在命令行终端的窗口上 - databases
设置数据库的个数,可以使用SELECT 命令来切换数据库。默认使用的数据库是0 - save
设置Redis 进行数据库镜像的频率。
if(在60 秒之内有10000 个keys 发生变化时){
进行镜像备份
}else if(在300 秒之内有10 个keys 发生了变化){
进行镜像备份
}else if(在900 秒之内有1 个keys 发生了变化){
进行镜像备份
} - rdbcompression
在进行镜像备份时,是否进行压缩 - dbfilename
镜像备份文件的文件名 - dir
数据库镜像备份的文件放置的路径。这里的路径跟文件名要分开配置是因为Redis 在进行备份时,先会将当前数据库的状态写入到一个临时文件中,等备份完成时,再把该该临时文件替换为上面所指定的文件,而这里的临时文件和上面所配置的备份文件都会放在这个指定的路径当中 - slaveof
设置该数据库为其他数据库的从数据库 - masterauth
当主数据库连接需要密码验证时,在这里指定 - requirepass
设置客户端连接后进行任何其他指定前需要使用的密码。警告:因为redis 速度相当快,所以在一台比较好的服务器下,一个外部的用户可以在一秒钟进行150K 次的密码尝试,这意味着你需要指定非常非常强大的密码来防止暴力破解。 - maxclients
限制同时连接的客户数量。当连接数超过这个值时,redis 将不再接收其他连接请求,客户端尝试连接时将收到error 信息。 - maxmemory
设置redis 能够使用的最大内存。当内存满了的时候,如果还接收到set 命令,redis 将先尝试剔除设置过expire 信息的key,而不管该key 的过期时间还没有到达。在删除时,将按照过期时间进行删除,最早将要被过期的key 将最先被删除。如果带有expire 信息的key 都删光了,那么将返回错误。这样,redis 将不再接收写请求,只接收get 请求。
maxmemory 的设置比较适合于把redis 当作于类似memcached 的缓存来使用。 - appendonly
默认情况下,redis 会在后台异步的把数据库镜像备份到磁盘,但是该备份是非常耗时的,而且备份也不能很频繁,如果发生诸如拉闸限电、拔插头等状况,那么将造成比较大范围的数据丢失。所以redis 提供了另外一种更加高效的数据库备份及灾难恢复方式。
开启append only 模式之后,redis 会把所接收到的每一次写操作请求都追加到
appendonly.aof 文件中,当redis 重新启动时,会从该文件恢复出之前的状态。但是这样会造成appendonly.aof 文件过大,所以redis 还支持了BGREWRITEAOF 指令,对appendonly.aof 进行重新整理。所以我认为推荐生产环境下的做法为关闭镜像,开启appendonly.aof,同时可以选择在访问较少的时间每天对appendonly.aof 进行重写一次。 - appendfsync
设置对appendonly.aof 文件进行同步的频率。always 表示每次有写操作都进行同步,everysec 表示对写操作进行累积,每秒同步一次。这个需要根据实际业务场景进行配置 - vm-enabled
是否开启虚拟内存支持。因为redis 是一个内存数据库,而且当内存满的时候,无法接收新的写请求,所以在redis 2.0 中,提供了虚拟内存的支持。但是需要注意的是,redis中,所有的key 都会放在内存中,在内存不够时,只会把value 值放入交换区。这样保证了虽然使用虚拟内存,但性能基本不受影响,同时,你需要注意的是你要把vm-max-memory 设置到足够来放下你的所有的key - vm-swap-file
设置虚拟内存的交换文件路径 - vm-max-memory
这里设置开启虚拟内存之后,redis 将使用的最大物理内存的大小。默认为0,redis 将把他所有的能放到交换文件的都放到交换文件中,以尽量少的使用物理内存。在生产环境下,需要根据实际情况设置该值,最好不要使用默认的0 - vm-page-size
设置虚拟内存的页大小,如果你的value 值比较大,比如说你要在value 中放置博客、新闻之类的所有文章内容,就设大一点,如果要放置的都是很小的内容,那就设小一点。 - vm-pages
设置交换文件的总的page 数量,需要注意的是,page table 信息会放在物理内存中,每8 个page 就会占据RAM 中的1 个byte。总的虚拟内存大小 = vm-page-size * vm-pages - vm-max-threads
设置VM IO 同时使用的线程数量。因为在进行内存交换时,对数据有编码和解码的过程,所以尽管IO 设备在硬件上本上不能支持很多的并发读写,但是还是如果你所保存的vlaue 值比较大,将该值设大一些,还是能够提升性能的 - glueoutputbuf
把小的输出缓存放在一起,以便能够在一个TCP packet 中为客户端发送多个响应,具体原理和真实效果我不是很清楚。所以根据注释,你不是很确定的时候就设置成yes - hash-max-zipmap-entries
在redis 2.0 中引入了hash 数据结构。当hash 中包含超过指定元素个数并且最大的元素没有超过临界时,hash 将以一种特殊的编码方式(大大减少内存使用)来存储,这里可以设置这两个临界值 - activerehashing
开启之后,redis 将在每100 毫秒时使用1 毫秒的CPU 时间来对redis 的hash 表进行重新hash,可以降低内存的使用。当你的使用场景中,有非常严格的实时性需要,不能够接受Redis 时不时的对请求有2 毫秒的延迟的话,把这项配置为no。如果没有这么严格的实时性要求,可以设置为yes,以便能够尽可能快的释放内存