Redis是什么?

Redis就是个key-value结构的存储系统。他提供了非常丰富的数据结构,包括 字符串(String)哈希(Hashs) 列表(Lists) 集合(Sets) 有序集合(Sorted Sets),当然还提供了操作这些数据结构的API。

Redis具有性能极高 – Redis能支持超过 100K+ 每秒的读写频率。

Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。

Redis还支持 publish/subscribe, Lua脚本,key过期等等特性。

Redis的作者是个很努力的大神,版本更新快,2.6里又多了好多实用的API

Redis的应用场景

  1. 取最新N个数据的操作
  2. 排行榜应用,取TOP N操作
  3. 需要精准设定过期时间的应用
  4. 计数器应用
  5. Uniq操作,获取某段时间所有数据排重值
  6. 实时系统,反垃圾系统
  7. Pub/Sub构建实时消息系统
  8. 构建队列系统
  9. 缓存

这些都是作者自己说的,这里不讨论其他的,适用当前场景就好,我们还是来关注一下排行榜的实现策划需求描述:

SNS游戏中,一般只有一个服,玩家数量非常多,玩家角色在线或离线状态比较模糊,这类游戏玩家角色一般是看不见的,所以排行榜是他们努力活下来依靠之一。
因此实现一个实时的排行榜尤为重要
一个类似下图的排行榜,要求是实时动态排行榜,一但后面的功勋值变化,要是排行榜上马上能看到自己的排行变化情况.

在了解了Redis数据结构发现,Redis中的sorted set数据结构,几乎就是为这个场景而生的。

假设一下排行的场景:有100W个角色,那么通过如下操作,就可以创建一个排行的角色基本信息的Hash:

    private int max = 100_0000;
    private Random random = new Random();
    
    @Test
    public void testAdd100WRoleBaseInfo() {
		for(int i = 1; i <= max; i ++){
			Map<String, String> role = new HashMap<String, String>();
			role.put("id", String.valueOf(i));
			role.put("name", "小流氓" + i + "号");
			role.put("level", String.valueOf(random.nextInt(200)));
			role.put("exp", String.valueOf(random.nextInt(100000000)));
			role.put("rank", String.valueOf(i));
			role.put("name", "联盟名字");
			role.put("meritorious", String.valueOf(random.nextInt(10000)));
			redis.hmset("role:" + i, role);
		}
	}
used_memory_human:157.25M

100W条角色基本数据占用内存为157M

下面模拟功勋值变化的情况,他们的功勋值在10000以内随机

    @Test
	public void testUpdateAllMeritorious() {
		String RANK_KEY = "rank_key";
		for (int i = 1; i <= max; i++) {
			String key = "role:" + i;
			int meritorious = random.nextInt(10000);
			redis.hset(key, "meritorious", String.valueOf(meritorious));
			redis.zadd(RANK_KEY, meritorious, key);
		}
	}

测试数据还在跑,先来介绍一下上面两个API

HSET key field value
加入版本 1.3.10。
时间复杂度 O(1)
设置 key 指定的哈希集中指定字段的值。如果 key 指定的哈希集不存在,会创建一个新的哈希集并与 key 关联。如果字段在哈希集中存在,它将被重写。
返回值
整数:含义如下
•    1如果field是一个新的字段
•	0如果field原来在map里面已经存在

redis> HSET role:1 name "Hello"
(integer) 1
redis> HGET role:1 name
"Hello"
redis> 
ZADD key
加入版本 1.1。
时间复杂度O(log(N)),N是有序集合中元素的个数。
该命令添加指定的成员到key对应的有序集合中,每个成员都有一个分数。你可以指定多个分数/成员组合。如果一个指定的成员已经在对应的有序集合中了,那么其分数就会被更新成最新的,并且该成员会重新调整到正确的位置,以确保集合有序。如果key不存在,就会创建一个含有这些成员的有序集合,就好像往一个空的集合中添加一样。如果key存在,但是它并不是一个有序集合,那么就返回一个错误。
分数的值必须是一个表示数字的字符串,并且可以是double类型的浮点数。
对于有序集合的介绍,可以参考sorted sets页面。
返回值
整数, 如下整数:
•	返回添加到有序集合中元素的个数,不包括那种已经存在只是更新分数的元素。
历史
•	>= 2.4: 接受多个元素。在redis 2.4之前的版本中,每次只能添加或者更新一个元素。


redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 1 "uno"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "two"
(integer) 0
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "uno"
4) "1"
5) "two"
6) "3"
redis>

排行榜只占了一个Key (rank_key)

used_memory_human:253.80M

此时内存占用为253M

    @Test
	public void testUpdateMeritorious() {
		String RANK_KEY = "rank_key";
		String key = "role:1";
		int meritorious = 0;
		long hsetStartTime = System.nanoTime();
		redis.hset(key, "meritorious", String.valueOf(meritorious));
		long hsetEndTime = System.nanoTime();
		System.out.println("hset --> " + (hsetEndTime - hsetStartTime)/1000000 + " ms");
		
		long zaddStartTime = System.nanoTime();
		redis.zadd(RANK_KEY, meritorious, key);
		long zaddEndTime = System.nanoTime();
		System.out.println("zadd --> " + (zaddEndTime - zaddStartTime)/1000000 + " ms");
	}

测试结果

上面功勋值为0,由于Zadd是的时间复杂度来看,移动最大,所用时间最多,再次测试

    @Test
	public void testUpdateMeritorious() {
		String RANK_KEY = "rank_key";
		String key = "role:100";
		int[] ms = {0, 10001, 200, 8000, 5000};
		for (int i = 0; i < ms.length; i ++){
			int meritorious = ms[i];
			long hsetStartTime = System.nanoTime();
			redis.hset(key, "meritorious", String.valueOf(meritorious));
			long hsetEndTime = System.nanoTime();
			System.out.println("hset "+meritorious+" --> " + (hsetEndTime - hsetStartTime)/1000000 + " ms");
			
			long zaddStartTime = System.nanoTime();
			redis.zadd(RANK_KEY, meritorious, key);
			long zaddEndTime = System.nanoTime();
			System.out.println("zadd "+meritorious+" --> " + (zaddEndTime - zaddStartTime)/1000000 + " ms");
		}
	}

测试结果来看 100W数据的实时排行榜可行。

如果获取前50名角色Id

@Test
	public void testGetTop50() {
		int page = 1, size = 50;
		for(int i = 0; i < 5; i ++){
			long startTime = System.nanoTime();
			Set<String> ranks = redis.zrevrange(RANK_KEY, (page - 1) * size, page * size - 1);
			long endTime = System.nanoTime();
			System.out.println("testGetTop50  --> " + (endTime - startTime)/1000000 + " ms");
			System.out.println("size  --> " + ranks.size());
		}
	}
@Test
	public void testGetTop10RoleInfo() {
		int page = 1, size = 10;
		long startTime = System.nanoTime();
		Set<String> ranks = redis.zrevrange(RANK_KEY, (page - 1) * size, page * size - 1);
		long endTime = System.nanoTime();
		for(String roleId: ranks){
			System.out.println(redis.hgetAll(roleId));
		}
		System.out.println("testGetTop10RoleInfo  --> " + (endTime - startTime)				/ 1000000 + " ms");
	}

第10页 的50条

testGet10_50RoleInfo  --> 37 ms
testGet100_100RoleInfo  --> 35 ms
testGet1000_100RoleInfo  --> 35 ms
HGETALL key
加入版本 1.3.10。
时间复杂度
O(N), 其中 N 是哈希集的大小
返回 key 指定的哈希集中所有的字段和值。返回值中,每个字段名的下一个是它的值,所以返回值的长度是哈希集大小的两倍
返回值
多个返回值:哈希集中字段和值的列表。当 key 指定的哈希集不存在时返回空列表。

每周的排名变化

当前的排名 – 上周的的排名 = 排名变化

如何记录一次排名,简单粗暴的一次Get,100W 4秒左右

Redis2.6以后有个Dump一个key的功能,时间跟Get差不多,再restore回去使用

多条件排行

由于sorted set只有一个条件,所以多条件排行好像是个太奢侈的要求。

问题产生原因:从上面测试结果里也能发现,数据量大了,前50的人 都是9999功勋值,有些等级高的在下面,要是按此排行榜发送奖励,显然玩家会有意见

为解决此问题尝试一下曲线求国。

一个条件,那就加吧!功勋值*1000+等级 这样来排

这样需要重构此SortedSet的排行榜

如果再有经验为第三条件,经验正常为long类型,这有点难度了…

又看了一下SortedSet的API其分数的值必须是一个表示数字的字符串,并且可以是double类型的浮点数。

Double是个好东东,将其他条件加入小数部分

{id=7500, exp=94565268, rank=7500, level=168, meritorious=9999, name=联盟名字}
{id=406, exp=97149316, rank=406, level=142, meritorious=9999, name=联盟名字}
{id=4054, exp=73429686, rank=4054, level=189, meritorious=9998, name=联盟名字}
{id=3798, exp=61974309, rank=3798, level=41, meritorious=9998, name=联盟名字}
{id=3126, exp=16462104, rank=3126, level=73, meritorious=9996, name=联盟名字}
{id=7518, exp=59497309, rank=7518, level=164, meritorious=9995, name=联盟名字}
{id=2429, exp=10389944, rank=2429, level=98, meritorious=9995, name=联盟名字}
{id=431, exp=39615033, rank=431, level=24, meritorious=9995, name=联盟名字}
{id=5573, exp=75663066, rank=5573, level=124, meritorious=9994, name=联盟名字}
{id=7478, exp=75361854, rank=7478, level=43, meritorious=9994, name=联盟名字}
{id=1340, exp=73811757, rank=1340, level=185, meritorious=9993, name=联盟名字}
{id=7101, exp=98409114, rank=7101, level=118, meritorious=9993, name=联盟名字}
{id=364, exp=63513412, rank=364, level=126, meritorious=9990, name=联盟名字}
{id=4269, exp=32826804, rank=4269, level=69, meritorious=9990, name=联盟名字}
{id=3163, exp=49462278, rank=3163, level=136, meritorious=9988, name=联盟名字}
{id=4962, exp=98373781, rank=4962, level=44, meritorious=9987, name=联盟名字}

建议:如果一开始明确,能放在小数点左边就放左边,小数点右边留着扩展功能

Reids官方网站传送点

转载请注明原地址: http://blog.noark.xyz/article/2013/1/15/redis之实时排行榜应用/