承接之前的文章,今晚我们一起聊聊关于Redis内部比较有趣的技术实现案例。在上一篇文章的最后我们留了一个尾巴,今天继续和大家分享这些场景下redis是怎么应用的。
微博、微信、陌陌
<附近的人>
微信<摇一摇><抢红包>
滴滴打车、摩拜单车<附近的车> 美团和饿了么<附近的餐馆>
布隆过滤器
附近的xxx案例
定位功能显示周边功能
这类功能在常见的互联网App如某团等应用上一般都能看到相关案例。
相关截图如下所示:
这让我想起了自己曾经遇到过的一个类似的需求,会员拿着手机到达指定地点打卡,即可领取相应奖励红包,但是要求到达指定地点范围200米内。(当然成为这类会员也是有一定门槛的,不然早就被羊毛党薅爆了🐶 )
当初的做法是将指定地点的坐标存储到MySQL的一张表中,然后从网上搜了一个根据经纬度计算距离的util类,外加接入一个腾讯地图手机定位的api简单粗暴地上线了。当时的场景还是比较简单的,所以也没有考虑到使用Redis来实现。
回归正题:
假设现在有个业务场景需要后台返回给前端一个指定地理位置方圆500m内所有自行车的坐标,你会怎么设计呢?
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
Redis GEO 操作方法有
geoadd:添加地理位置的坐标。 geopos:获取地理位置的坐标。 geodist:计算两个位置之间的距离。 georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。 georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。 geohash:返回一个或多个位置对象的 geohash 值。 复制代码
我们可以提前在redis中存储好对应的单车地理位置:
命令格式:
GEOADD key longitude latitude member [longitude latitude member ...] 复制代码
实操命令:
> geoadd shenzhen 21.123123 22.812312 car-10012 1 复制代码
给定坐标的经纬度之后,将对应的自行车id记录到redis中。
查看指定单车的经纬度信息:
> geopos shenzhen car-10012 car-10013 car-10014 car-10015 21.12312287092208862 22.81231173620882657 21.12312287092208862 22.81231173620882657 21.12312287092208862 22.81231427092998132 21.12312287092208862 22.81231427092998132 复制代码
计算两辆单车之间的距离
> geodist shenzhen car-10012 car-10015 0.2819 复制代码
根据用户定位的经纬度查询指定方圆内存在的单车(这一功能就有点类似于我上边截图的效果),例如查询方圆500m内的自行车
> georadius shenzhen 21.12312287092208862 22.81231173620882657 500 m car-10012 car-10013 car-10014 car-10015 复制代码
基于geo相关命令,我们还可以对定位功能做各式各样的扩展功能。
其实从这个案例我们也大概可以推测出类似的其他功能是如何实现的,例如摇一摇,附近的餐馆,附近的人等等。
抢红包案例
业务流程分析
如下图所示:
新建红包
在 DB、Redis 分别新增一条记录
抢红包(并发)
请求Redis,红包剩余个数,大于0才可以拆,等会0时,提示用户,红包已抢完
拆红包(并发)
Redis 中数据类型的 String 特性的原子递减(DECR key)和减少指定值(DECRBY key decrement)。请求 Redis ,当剩余红包个数大于 0,红包个数原子递减,随机获取红包。计算金额,当最后一个红包时,最后一个红包金额=总金额-总已抢红包金额
更新数据库
查看红包记录
查询 DB 即可
数据库设计
红包流水表
CREATE TABLE `red_packet_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `red_packet_id` bigint(11) NOT NULL DEFAULT 0 COMMENT '红包id,采⽤ timestamp+5位随机数', `total_amount` int(11) NOT NULL DEFAULT 0 COMMENT '红包总⾦额,单位分', `total_packet` int(11) NOT NULL DEFAULT 0 COMMENT '红包总个数', `remaining_amount` int(11) NOT NULL DEFAULT 0 COMMENT '剩余红包⾦额,单位 分', `remaining_packet` int(11) NOT NULL DEFAULT 0 COMMENT '剩余红包个数', `uid` int(20) NOT NULL DEFAULT 0 COMMENT '新建红包⽤户的⽤户标识', `create_time` timestamp COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='红包信息 表,新建⼀个红包插⼊⼀条记录'; 复制代码
红包记录表
CREATE TABLE `red_packet_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `amount` int(11) NOT NULL DEFAULT '0' COMMENT '抢到红包的⾦额', `nick_name` varchar(32) NOT NULL DEFAULT '0' COMMENT '抢到红包的⽤户的⽤户 名', `img_url` varchar(255) NOT NULL DEFAULT '0' COMMENT '抢到红包的⽤户的头像', `uid` int(20) NOT NULL DEFAULT '0' COMMENT '抢到红包⽤户的⽤户标识', `red_packet_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '红包id,采⽤ timestamp+5位随机数', `create_time` timestamp COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='抢红包记 录表,抢⼀个红包插⼊⼀条记录'; 复制代码
发红包 API
发红包接口开发
新增一条红包记录
往 mysql 里面添加一条红包记录
往 redis 里面添加一条红包数量记录
往redis里面添加一条红包金额记录
往db中就单纯存入一条记录,Service层和Mapper层,就简单的一条sql语句,主要是提供思路。
抢红包功能属于原子减操作
- 当大小小于 0 时原子减失败
- 当红包个数为0时,后面进来的用户全部抢红包失败,并不会进入拆红包环节
- 将红包ID的请求放入请求队列中,如果发现超过红包的个数,直接返回
- 抢到红包不一定能拆成功
抢红包算法拆解
通过上图算法得出,靠前面的人,手气最佳几率小,手气最佳,往往在后面
发 100 元,共 10 个红包,那么平均值是 10 元一个,那么发出来的红包金额在 0.01~20 元之间波动
当前面 4 个红包总共被领了 30 元时,剩下 70 元,总共 6 个红包,那么这 6 个红包的金额在 0.01~23.3 元之间波动。
企业微信消息是否已阅读
redis中的布隆过滤器实际上可用的案例非常多,这里我就不打算细说了,典型应用如:在1亿个手机号中查询是否存在对应手机号码,一亿个用户白名单记录表等等。
最近在工作中频繁会使用企业微信和别人沟通消息,假如让我们自己来实现企业微信消息的未读通知改如何实现呢?
对于消息后台可以分为离线和已发送消息。所谓的离线消息就是指用户还处于未读状态的消息,通常这部分的数据都会存储在离线消息库中。因此当用户首次访问企业微信的时候应该会触发一个机制:判断当前用户是否存在离线消息信息,如果存在则从离线消息库中拉去。
推测企业微信的用户群体如此庞大,这种判断应该也是利用一个类似于布隆过滤器之类的组件存放每个具有离线消息的用户id,然后请求后台的时候到其中去查找。(当然这只是我的一个简单猜测)
在Redis里有一个叫做bitmap的数据结构,使用技巧如下:
setbit指令
语法:setbit key offset value
127.0.0.1:6379> setbit bitmap-01 999 0 (integer) 0 127.0.0.1:6379> setbit bitmap-01 999 1 (integer) 0 127.0.0.1:6379> setbit bitmap-01 1003 1 (integer) 0 127.0.0.1:6379> setbit bitmap-01 1003 0 (integer) 1 复制代码
getbit指令
语法:getbit key offset
127.0.0.1:6379> setbit bm 0 1 (integer) 0 127.0.0.1:6379> getbit bm 0 (integer) 1 复制代码
bitcount指令
语法:bitcount key [start] [end] ,这里的start和end值为可选项
返回值:被设置为 1 的位的数量
127.0.0.1:6379> bitcount user18 (integer) 4 复制代码
bitop指令
语法:bitop operation destkey key [key …]
operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:
BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。 BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。 BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。 BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。