天天看点

分布式缓存Redis之数据类型

写在前面

  本学习教程所有示例代码见GitHub:https://github.com/selfconzrr/Redis_Learning

  Redis 数据类型官方文档:http://www.redis.net.cn/tutorial/3505.html

  Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)

使用场景及简介

String:

  string是最简单的类型,你可以理解成与Memcached是一模一样的类型,一个key对应一个value,其上支持的操作与Memcached的操作类似。但它的功能更丰富。

  string类型是二进制安全的。意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。从内部实现来看其实string可以看作byte数组,最大上限是1G字节,下面是string类型的定义:

struct sdshdr {
  long len;
  long free;
  char buf[];
  };
           

  其中:

  len是buf数组的长度。

  free是数组中剩余可用字节数,由此可以理解为什么string类型是二进制安全的了,因为它本质上就是个byte数组,当然可以包含任何数据了。

  buf是个char数组用于存贮实际的字符串内容,其实char和c#中的byte是等价的,都是一个字节。

  另外string类型可以被部分命令按int处理。比如incr等命令,如果只用string类型,redis就可以被看作加上持久化特性的memcached。当然redis对string类型的操作比memcached还是多很多的。

  学习的时候,就有个疑问了:什么是二进制安全?为什么string是二进制安全的?

  见博文:http://blog.csdn.net/u011489043/article/details/78738374

LIST:

  Redis的list类型其实就是一个每个子元素都是string类型的双向链表。链表的最大长度是(2的32次方)。我们可以通过push,pop操作从链表的头部或者尾部添加删除元素。这使得list既可以用作栈,也可以用作队列。

  list的pop操作还有阻塞版本的,当我们[l/r]pop一个list对象时,如果list是空,或者不存在,会立即返回nil。但是阻塞版本的b[l/r]pop则可以阻塞,当然可以加超时时间,超时后也会返回nil。

  为什么要阻塞版本的pop呢,主要是为了避免轮询。举个简单的例子如果我们用list来实现一个工作队列,执行任务的thread可以调用阻塞版本的pop去获取任务。这样就可以避免轮询去检查是否有任务存在,当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。

  

  应用: 可以作为消息队列,LPUSH链表头作为生产者插入消息,RPOP作为消费者取得消息。

Hash:

  Redis hash是一个string类型的field和value的映射表。它的添加、删除操作都是O(1)(平均)。hash特别适合用于存储对象。相较于将对象的每个字段存成单个string类型。将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。

  省内存的原因是新建一个hash对象时,开始是用zipmap(又称为small hash)来存储的。这个zipmap其实并不是hash table,但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销。尽管zipmap的添加,删除,查找都是O(n),但是由于一般对象的field数量都不太多。所以使用zipmap也是很快的,也就是说添加删除平均还是O(1)。如果field或者value的大小超出一定限制后,Redis会在内部自动将zipmap替换成正常的hash实现。这个限制可以在配置文件中指定。

hash-max-zipmap-entries 64 #配置字段最多64个。
hash-max-zipmap-value 512 #配置value最大为512字节。
           

  应用: AIOP中用户标签信息使用HASHMAP存储,存储结构为user:phone tag1 val1 tag2 val2 ,AIOP传递客户手机号,即可通过HGET user:phone tag1 tag2 取出标签值。

Set:

  Redis的set是string类型的无序集合。set元素最大可以包含(2的32次方)个元素。

  set的是通过hash table实现的,所以添加、删除和查找的复杂度都是O(1)。hash table会随着添加或者删除自动的调整大小。需要注意的是调整hash table大小时候需要同步(获取写锁)会阻塞其他读写操作,可能不久后就会改用跳表(skip list)来实现,跳表已经在sorted set中使用了。关于set集合类型除了基本的添加删除操作,其他有用的操作还包含集合的取并集(union),交集(intersection),差集(difference)。通过这些操作可以很容易的实现sns中的好友推荐和blog的tag功能。

  应用: COC中将符合标签的用户进行聚类,存放到Set中,比如set1 存放学生标签用户、set2存放低消费用户、set3存放非合约用户,那么取出可能购买小米的低消非合约学生用户群:

SINTERSTORE  aset set1 set2 // 将set1与set2的交集赋给aset
SDIFF aset set3   //取aset与set3的差集
           

Sorted Set:

  和set一样sorted set也是string类型元素的集合,不同的是每个元素都会关联一个double类型的score。sorted set的实现是skip list和hash table的混合体。

  当元素被添加到集合中时,一个元素到score的映射被添加到hash table中,所以给定一个元素获取score的开销是O(1),另一个score到元素的映射被添加到skip list,并按照score排序,所以就可以有序的获取集合中的元素。**添加,删除操作开销都是O(log(N))**和skip list的开销一致。

  redis的skip list实现用的是双向链表,这样就可以逆序从尾部取元素。sorted set最经常的使用方式应该是作为索引来使用。我们可以把要排序的字段作为score存储,对象的id当元素存储。

  

  **应用:**可以用于一个大型在线游戏的积分排行榜。每当玩家的分数发生变化时,可以执行ZADD命令更新玩家的分数,此后再通过ZRANGE命令获取积分TOP TEN的用户信息。当然我们也可以利用ZRANK命令通过username来获取玩家的排行信息。最后我们将组合使用ZRANGE和ZRANK命令快速的获取和某个玩家积分相近的其他用户的信息。

在Ubuntu下的终端操作:

  启动redis服务器后,进入redis客户端,然后大家可以尝试如下命令的操作,一方面熟悉Linux操作,一方面熟悉redis支持的数据类型。

1、增加一条字符串记录key1

# 增加一条记录key1
redis 127.0.0.1:6379> set key1 "hello"
OK

# 打印记录
redis 127.0.0.1:6379> get key1
"hello"

2、增加一条数字记录key2

# 增加一条数字记录key2
set key2 1
OK

# 让数字自增
redis 127.0.0.1:6379> INCR key2
(integer) 2
redis 127.0.0.1:6379> INCR key2
(integer) 3

# 打印记录
redis 127.0.0.1:6379> get key2
"3"

3、增加一条列表记录key3

# 增加一个列表记录key3
redis 127.0.0.1:6379> LPUSH key3 a
(integer) 1

# 从左边插入列表
redis 127.0.0.1:6379> LPUSH key3 b
(integer) 2

# 从右边插入列表
redis 127.0.0.1:6379> RPUSH key3 c
(integer) 3

# 打印列表记录,按从左到右的顺序
redis 127.0.0.1:6379> LRANGE key3 0 3
1) "b"
2) "a"
3) "c"

4、增加一条哈希表记录key4

# 增加一个哈希记表录key4
redis 127.0.0.1:6379> HSET key4 name "John Smith"
(integer) 1

# 在哈希表中插入,email的Key和Value的值
redis 127.0.0.1:6379> HSET key4 email "[email protected]"
(integer) 1

# 打印哈希表中,name为key的值
redis 127.0.0.1:6379> HGET key4 name
"John Smith"

# 打印整个哈希表
redis 127.0.0.1:6379> HGETALL key4
1) "name"
2) "John Smith"
3) "email"
4) "[email protected]"

5、增加一条哈希表记录key5

# 增加一条哈希表记录key5,一次插入多个Key和value的值
redis 127.0.0.1:6379> HMSET key5 username antirez password P1pp0 age 3
OK

# 打印哈希表中,username和age为key的值
redis 127.0.0.1:6379> HMGET key5 username age
1) "antirez"
2) "3"

# 打印完整的哈希表记录
redis 127.0.0.1:6379> HGETALL key5
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "3"

# 查看所有的key列表
redis 127.0.0.1:6379> keys *
1) "key2"
2) "key3"
3) "key4"
4) "key5"
5) "key1"

# 删除key1,key5
redis 127.0.0.1:6379> del key1
(integer) 1
redis 127.0.0.1:6379> del key5
(integer) 1

# 查看所有的key列表
redis 127.0.0.1:6379> keys *
1) "key2"
2) "key3"
3) "key4"
           

在windows下的Eclipse操作

  新建java工程,需要引入客户端jedis的jar包,两种方式

  添加maven依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
           

  直接下载,(我的百度网盘)

  Jedis2.1.0,链接:https://pan.baidu.com/s/1o7K0Zai 密码:wabo

  Jedis2.9.0,链接:https://pan.baidu.com/s/1i5tNhx7 密码:u9si

  然后,执行下面的简单测试代码。看能否成功连接Ubuntu下的服务器,并实现key-value的操作。

  比如在Ubuntu终端下,输入命令:get key6;如果在终端界面输出:jedis test6,说明远程连接、操作数据库成功。

Jedis jedis = new Jedis("192.168.65.130", 6379);// 此ip为Ubuntu的ip地址
		jedis.auth("redis");// redis-cli的访问密码
		System.out.println("Connection to server sucessfully");
		// 查看服务是否运行
		System.out.println("Server is running: " + jedis.ping());
		// 进行数据库操作测试
		jedis.set("key6", "redis test");
		jedis.set("key7", "张瑞瑞");//中文在Ubuntu下没正常显示
		String string = jedis.get("key7");
		String string1 = jedis.get("key6");
		System.out.println(string + " " + string1);
           

  比如对set类型的详细操作,完整代码见开篇留的GitHub源码地址(几乎囊括了五种数据类型的所有操作)。主要是熟悉一些常用命令的功能及用法。

public static void SetOperate(Jedis jedis, ShardedJedis shardedJedis) {
		System.out.println("================set=================");
		System.out.println("清空库中所有数据:" + jedis.flushDB());

		System.out.println("=============增=============");
		System.out.println("向sets集合中加入元素element001:"
				+ jedis.sadd("sets", "element001"));
		System.out.println("向sets集合中加入元素element002:"
				+ jedis.sadd("sets", "element002"));
		System.out.println("向sets集合中加入元素element003:"
				+ jedis.sadd("sets", "element003"));
		System.out.println("向sets集合中加入元素element004:"
				+ jedis.sadd("sets", "element004"));
		System.out.println("向sets集合中加入元素element001:"
				+ jedis.sadd("sets", "element001"));
		System.out.println("查看sets集合中的所有元素:" + jedis.smembers("sets"));
		System.out.println();

		System.out.println("=============删=============");
		System.out.println("集合sets中删除元素element003:"
				+ jedis.srem("sets", "element003"));
		// smembers(key) :返回名称为key的set的所有元素
		System.out.println("查看sets集合中的所有元素:" + jedis.smembers("sets"));
		/*
		 * System.out.println("sets集合中任意位置的元素出栈:"+jedis.spop("sets")); --无实际意义
		 * System.out.println("查看sets集合中的所有元素:"+jedis.smembers("sets"));
		 */
		System.out.println();

		System.out.println("=============查=============");
		// sismember(key, member) :member是否是名称为key的set的元素
		System.out.println("判断element001是否在集合sets中:"
				+ jedis.sismember("sets", "element001"));
		// scard(key) :返回名称为key的set的基数
		System.out.println("基数: " + jedis.scard("sets"));
		System.out.println("循环查询获取sets中的每个元素:");
		Set<String> set = jedis.smembers("sets");
		Iterator<String> it = set.iterator();
		while (it.hasNext()) {
			Object obj = it.next();
			System.out.println(obj);
		}
		System.out.println();

		System.out.println("=============集合运算=============");
		System.out.println("sets1中添加元素element001:"
				+ jedis.sadd("sets1", "element001"));
		System.out.println("sets1中添加元素element002:"
				+ jedis.sadd("sets1", "element002"));
		System.out.println("sets1中添加元素element003:"
				+ jedis.sadd("sets1", "element003"));

		System.out.println("sets2中添加元素element002:"
				+ jedis.sadd("sets2", "element002"));
		System.out.println("sets2中添加元素element003:"
				+ jedis.sadd("sets2", "element003"));
		System.out.println("sets2中添加元素element004:"
				+ jedis.sadd("sets2", "element004"));

		System.out.println("查看sets1集合中的所有元素:" + jedis.smembers("sets1"));
		System.out.println("查看sets2集合中的所有元素:" + jedis.smembers("sets2"));
		System.out.println("sets1和sets2交集:" + jedis.sinter("sets1", "sets2"));
		System.out.println("sets1和sets2并集:" + jedis.sunion("sets1", "sets2"));
		// 记A,B是两个集合,则所有属于A且不属于B的元素构成的集合,为“差集”
		System.out.println("sets1和sets2差集:" + jedis.sdiff("sets1", "sets2"));
	}
           

------至所有正在努力奋斗的程序猿们!加油!!

有码走遍天下 无码寸步难行

1024 - 梦想,永不止步!

爱编程 不爱Bug

爱加班 不爱黑眼圈

固执 但不偏执

疯狂 但不疯癫

生活里的菜鸟

工作中的大神

身怀宝藏,一心憧憬星辰大海

追求极致,目标始于高山之巅

一群怀揣好奇,梦想改变世界的孩子

一群追日逐浪,正在改变世界的极客

你们用最美的语言,诠释着科技的力量

你们用极速的创新,引领着时代的变迁

——乐于分享,共同进步,欢迎补充

——Any comments greatly appreciated

——诚心欢迎各位交流讨论!QQ:1138517609

——CSDN:https://blog.csdn.net/u011489043

——简书:https://www.jianshu.com/u/4968682d58d1

——GitHub:https://github.com/selfconzrr

继续阅读