Redis开发与运维-第2章 API的理解和使用-使用场景

1.字符串
1.1缓存功能
比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,大部分请求的数据从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
在这里插入图片描述
整个功能的伪代码如下:

UserInfo getUserInfo(long id){
	userRedisKey = "user:info:" + id;// 定义键
	value = redis.get(userRedisKey);//从Redis获取值
	UserInfo userInfo;
	if (value != null) {
		userInfo = deserialize(value);// 将值进行反序列化为UserInfo并返回结果
	} else {
		userInfo = mysql.get(id);// 从MySQL获取用户信息
		if (userInfo != null)
			redis.setex(userRedisKey, 3600, serialize(userInfo));// 将userInfo序列化,并存入Redis
	}
	return userInfo;
}

1.2计数
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。例如笔者所在团队的视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1:

long incrVideoCounter(long id) {
	key = "video:playCount:" + id;
	return redis.incr(key);
}

实际上一个真实的计数系统要考虑的问题会很多:防作弊、按照不同维度计数,数据持久化到底层数据源等。
1.3共享Session
一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
在这里插入图片描述
为了解决这个问题,可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
在这里插入图片描述
1.4限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,此功能可以使用Redis来实现,下面的伪代码给出了基本实现思路:

phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <=5){
	// 通过
}else{
	// 限速
}

上述就是利用Redis实现了限速功能,例如一些网站限制一个IP地址不能在一秒钟之内访问超过n次也可以采用类似的思路。

2哈希
2.1哈希类型存储用户信息
在这里插入图片描述
相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对field-value对应每个用户的属性,类似如下伪代码:

UserInfo getUserInfo(long id){
	// 用户id作为key后缀
	userRedisKey = "user:info:" + id;
	// 使用hgetall获取所有用户信息映射关系
	userInfoMap = redis.hgetAll(userRedisKey);
	UserInfo userInfo;
	if (userInfoMap != null) {
		// 将映射关系转换为UserInfo
		userInfo = transferMapToUserInfo(userInfoMap);
	} else {
		// 从MySQL中获取用户信息
		userInfo = mysql.get(id);
		// 将userInfo变为映射关系使用hmset保存到Redis中
		redis.hmset(userRedisKey, transferUserInfoToMap(userInfo));
		// 添加过期时间
		redis.expire(userRedisKey, 3600);
	}
	return userInfo;
}

但是需要注意的是哈希类型和关系型数据库有两点不同之处:

  • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)
  • 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。
    在这里插入图片描述
    三种方法缓存用户信息优缺点分析:
    1)原生字符串类型:每个属性一个键。
set user:1:name tom
set user:1:age 23
set user:1:city beijing

优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
2)序列化字符串类型:将用户信息序列化后用一个键保存。

set user:1 serialize(userInfo)

优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。

hmset user:1 name tomage 23 city beijing

优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
3.列表
3.1消息队列
lpush+brpop命令组合实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
在这里插入图片描述
3.2文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
1)每篇文章使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content:

hmset acticle:1 title xx timestamp 1476536196 content xxxx
...
hmset acticle:k title yy timestamp 1476512536 content yyyy

2)向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键:

lpush user:1:acticles article:1 article3
...
lpush user:k:acticles article:5

3)分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文章

articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}

使用列表类型保存和获取文章列表会存在两个问题。第一,如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用Pipeline(第3章会介绍)批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获取。第二,分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用Redis3.2的quicklist内部编码实现,它结合ziplist和linkedlist的特点,获取列表中间范围的元素时也可以高效完成。
实际上列表的使用场景很多,在选择时可以参考以下口诀:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpsh+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)
    4.集合
    4.1标签
    集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
    下面使用集合类型实现标签功能的若干功能。
    (1)给用户添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
...

(2)给标签添加用户

sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2

用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致
(3)删除用户下的标签

srem user:1:tags tag1 tag5
...

(4)删除标签下的用户

srem tag1:users user:1
srem tag5:users user:1
...

(3)和(4)也是尽量放在一个事务执行。
(5)计算用户共同感兴趣的标签
可以使用sinter命令,来计算用户共同感兴趣的标签,如下代码所示:

sinter user:1:tags user:2:tags

前面只是给出了使用Redis集合类型实现标签的基本思路,实际上一个标签系统远比这个要复杂得多,不过集合类型的应用场景通常为以下几种:

  • sadd=Tagging(标签)
  • spop/srandmember=Random item(生成随机数,比如抽奖)
  • sadd+sinter=Social Graph(社交需求)
    5.有序集合
    5.1排行榜
    有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能。
    (1)添加用户赞数
    例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby功能:
127.0.0.1:6379> zadd user:ranking:2016_03_15  3 mike 
(integer) 1

如果之后再获得一个赞,可以使用zincrby:

127.0.0.1:6379> zincrby user:ranking:2016_03_15 1 mike
"4"

(2)取消用户赞数
由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用zrem。例如删除成员 mike:

127.0.0.1:6379> zrem user:ranking:2016_03_15 mike
(integer) 1

(3)展示获取赞数最多的十个用户,此功能使用zrevrange命令实现:

127.0.0.1:6379> zrevrange user:ranking:2016_03_15 0 9
1) "tony"
2) "mike"

(4)展示用户信息以及用户分数
此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用zscore和zrank两个功能。

hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike

6.Bitmaps:Bitmaps本身不是一种数据结构,实际上它就是字符串。
6.1每个独立用户是否访问过网站场景:将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id。
具体操作过程如下,unique:users:2016-04-05代表2016-04-05这天的独立访问用户的Bitmaps:

127.0.0.1:6379> setbit unique:users:2016-04-05 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 11 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 15 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 19 1
(integer) 0

很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
获取键的第offset位的值(从0开始算),下面操作获取id=8的用户是否在2016-04-05这天访问过,返回0说明没有访问过:

127.0.0.1:6379> getbit unique:users:2016-04-05 8
(integer) 0

下面操作计算2016-04-05这天的独立访问用户数量:

127.0.0.1:6379> bitcount unique:users:2016-04-05
(integer) 5

2016-04-04和2016-04-03任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集。
2016-04-04和2016-04-03两天都访问过网站的用户数量,可以使用and求交集。
Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户)。
7.HyperLogLog基数算法
HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。统计某日访问用户数。
pfcount用于计算一个或多个HyperLogLog的独立总数,例如2016年3月5日和3月6日的访问独立用户数

pfcount 2016_03_05:unique:ids 2016_03_06:unique:ids

HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条即可:

  • 只为了计算独立总数,不需要获取单条数据。
  • 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势。
    8.发布订阅
    聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,下面以简单的服务解耦进行说明。如图所示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息。
    在这里插入图片描述
    假如视频管理员在视频管理系统中对视频信息进行了变更,希望及时通知给视频服务端,就可以采用发布订阅的模式,发布视频信息变化的消息到指定频道,视频服务订阅这个频道及时更新视频信息,通过这种方式可以有效解决两个业务的耦合性。
  • 视频服务订阅video:changes频道如下:
    subscribe video:changes
  • 视频管理系统发布消息到video:changes频道如下:
    publish video:changes “video1,video3,video5”
  • 当视频服务收到消息,对视频信息进行更新,如下所示:
 for video in video1,video3,video5
	update {video}
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值