目录
本文主要总结目前开源项目中Redis的使用场景,提供在项目中使用Redis来实现功能的思路,主要是提供实现思路,如果读者想要深入学习的话,可以去B站搜索对应课程学习。
苍穹外卖(缓存)
利用Redis缓存高频数据以适应高并发环境和构造锁来缓解优惠券秒杀等问题,并通过乐观锁解决商品超卖问题;
在我们餐饮项目里,系统显示餐厅的营业状态,营业状态分为营业中 和打烊中,若当前餐厅处于营业状态,自动接收任何订单,客户可在小程序进行下单操作;若当前餐厅处于打烊状态,不接受任何订单,客户便无法在小程序进行下单操作。虽然,可以通过一张表来存储营业状态数据,但整个表中只有一个字段,所以意义不大;最后,我们选择基于Redis的字符串来进行存储店铺的营业状态,用SHOP_STATUS来作为key,1/0作为value进行存储,其中,我们约定1表示营业,0表示打烊。
在我们餐饮项目中,有这样一个场景,用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大,这会造成系统响应慢、用户体验差等缺陷。最终,我们通过Redis来缓存菜品数据,减少数据库查询操作,从而来提高系统处理并发的能力。当第一次查询某个分类的商品时,会从数据库中进行查询,同时将查询的结果存储到Redis中,在后续的访问中,若查询相同分类的菜品时,直接从Redis缓存中查询,不再查询数据库。同时,当在后台修改商品数据时,为了保证Redis缓存中的数据和数据库中的数据时刻保持一致,当修改后,需要清空对应的缓存数据。用户再次访问时,还是先从数据库中查询,同时再把查询的结果存储到Redis中,这样,就能保证缓存和数据库的数据保持一致。



黑马头条(消息队列)
通过整合Redis技术实现了延迟任务的功能,并解决了未来数据定时刷新和分布式锁解决集群下方法抢占的难题;
在项目的开发过程中,有这样一个场景:对于文章发布的时间,有立即发布和定时发布两种场景,对于定时发布,这里需要通过延迟任务精准发布文章。对于延迟任务来说,没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件,任务可以立即执行,也可以延迟。要实现这一功能,我们通常有两种方式,第一种是基于RabbitMQ实现延迟任务,它是通过TTL(消息存活时间)+死信队列来实现的,即当消息成为Dead message后,可以重新发送另一个交换机里进行处理,这种方式具体实现流程如下所示:
而另一种方式是基于Redis来实现,这一实现原理是基于Redis的zset数据类型的去重有序(分数排序)的特点来达到延迟效果的,这一实现的主要流程是:
当我们发布定时任务后,首先需要将定时任务写入到数据库中;然后我们对任务的执行时间进行判断,如果执行时间小于或者等于当前时间的话,那么我们认为这条任务需要在当前时间处理,直接将该任务加入到当前消费队列当中去(也就是Redis的list数据类型),然后直接安排消费即可;相反,当任务的执行大于当前时间的话,我们会将当前任务写入到未来数据队列当中去(也就是Redis的zset数据类型);
其次,为了将Redis未来数据队列当中的数据定时刷新到当前消费队列中(也就是从Redis的zset转到list的过程),在Redis的管道中我们采取Pipeline请求模型定时地从Redis客户端到Redis 服务端传送数据,以此来达到将未来数据定时刷新的目的;
再次,在通过Redis技术实现了延迟任务的功能的过程中,如果有多个端口的服务都去执行refresh定时任务方法,简单来说就是有多个线程对共享数据进行变更,这样会导致数据不一致的情况出现,这就需要使用到分布式锁来解决这一问题。在Java中的常见的分布式锁有以下三种:

在本项目中,我们采取的是redis的分布式锁,底层是基于setnx来实现的,setnx命令即SET if Not Exists,它在指定的key不存在时,为key设置指定的值。在执行未来数据定时刷新的方法时,先通过refresh()方法获取锁,只有持有锁的线程才能够对共享变量进行操作;而获取锁失败的线程会不断重试直到达到阈值进入阻塞状态。
最后,数据库中的数据也需要同步到Redis里面去,我们当时定义了一个重新加载数据的方法,这个方法会每五分钟从数据库中加载即将执行的任务到Redis缓存当中去,在这个过程中,我们需要考虑到数据库和Redis的一致性的问题,所以在每次执行这一操作之前,我们都需要先清理Redis的缓存,以此来达到Redis缓存中的数据都是最新的并且是不重复的数据。
总结来说就是,第一,当用户提交文章之后,系统根据是否有id来判断该操作是发布还是保存操作,如果是草稿,系统直接保存到数据库中并返回成功,而如果是发布操作的话,系统会将文章信息保存到数据库中,并且将审核任务添加到Redis延迟队列中等待被消费;第二,将任务添加到延迟队列中后,根据任务的发布时间来决定任务啥时候被消费,当然,如果任务失败,也是需要记录日志并提供重试机制的;第三,设置定时任务进行消费,我们定义了一个任务每个1秒从延迟队列中拉取任务,如果拉取到了任务,我们进行反序列化任务参数,并获取文章id,再调用审核服务对文章进行审核,如果任务拉取失败的话,也是需要记录日志的,并且将任务重新返回队列或记录到失败队列里去,避免数据丢失的情况;第四,就是文章审核,这一模块需要对文章的文本内容和图片分别进行审核,我们需要根据审核结果更新文章状态,并且审核通过的文章也是需要同步到APP端展示给用户查看的,如果审核失败的话,需要记录失败原因,并且更新文章状态为"审核失败",方便用户查看文章状态。
乐尚代驾(缓存+GEO+Redisson)
利用Redis缓存机制存储用户信息,有效降低数据库负载,并通过GEO实现对附近司机的快速定位与订单推送;
在代驾这个项目里,我们考虑到在高并发环境下,如果大量的请求直接访问数据库的话,会对数据库造成一定的压力,甚至导致系统崩溃;为了提高系统处理并发的能力,在该项目中,我们利用redis来缓存一些高频数据,这样前端发起请求的时候,会先去redis里面查询数据,如果查询到了数据会直接返回数据,redis缓存中没有数据才会去查询数据库,也就是说部分高频请求不会到达数据库,这在一定提高了系统的并发能力。
其次,在该系统的开发中,我们用到了redis给我们提供的GEO功能,它主要有两个命令,一个是GEOADD+城市+经纬度,用来缓存一个地点的经纬度信息;另一个是GEORADIUS+城市+经纬度+半径,用来查询以该经纬度为中心,半径以内的其他位置信息。在该项目里,司机开启接单服务后,小程序会实时上传经纬度信息到GEO缓存,如果乘客下单,我们就需要查找附近适合接单的司机,如果有对应司机,那就给司机发送新订单消息。我们就是基于这种方式来实现对附近司机的快速定位与订单推送的。
利用Redisson提供分布式锁解决方案,有效避免司机抢单的竞争条件,确保订单分配的公平性和准确性;
在代驾项目中,我们有一个场景,就是乘客发送订单后,半径范围内的司机都可以看到这个订单,但是只能有一个司机接单,这就涉及到了一个典型的超卖问题了。在单体项目中,我们一般会使用synchronized对象锁来解决超卖问题,但是对于分布式项目来说,synchronized无法保证在分布式不同的节点上只能有一个节点获得对象锁;所以在代驾项目中我们选择了采取分布式锁来解决超卖问题。我们首先考虑的是使用redis的setnx命令来实现这一效果,由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候其他客户端是不能设置这个key的。
但是,redis的setnx指令不好控制分布式锁的时长,所以我们选择了redis的一个框架redisson来实现分布式锁,redisson的话它的底层也是基于setnx和lua脚本的,其中lua脚本用来保证了分布式锁的原子性;另外,redisson这个框架给我们提供了一个WatchDog,也就是看门狗机制,一个线程获取锁之后,这个看门狗线程会持续给持有锁的线程续期,默认是每隔10秒续期一次,这一就能控制Redis实现分布式锁有效时长了。
在上面的三个项目中的应用中,其中黑马头条中基于Redis的消息队列来实现延迟任务的思路较为复杂,如读者想深入了解,我另开帖子分析了,请到Redis类目中查找。