小众点评项目要点
文章目录
- 小众点评项目要点
- 1.使用Redis代替Session登录
- 1.1 Session登录存在的问题
- 1.2 使用Redis代替Session登录分析
- 1.3 使用Redis登录的流程
- 1.4 解决Redis中有效期问题
- 2.使用Redis作为缓存
- 2.1 为什么使用缓存
- 2.2 缓存策略
- 2.3 解决缓存穿透
- 2.4 解决缓存雪崩
- 2.5 解决缓存击穿
- 3.秒杀功能
- 3.1 全剧唯一ID生成器
- 3.2 秒杀业务V1.0 使用乐观锁解决超卖问题
- 3.3 秒杀业务V1.1 实现一人一单秒杀
- 3.4 秒杀业务V1.2 实现集群环境下一人一单
- 3.5 可重入锁的原理
- 3.6 Redisson分布式锁
- 3.7 Redission锁的MutiLock
- 4.使用Lua脚本结合消息队列实现异步秒杀业务
- 5. 好友点赞和关注
- 5.1 好友点赞
- 5.2 好友共同关注
- 6. Feed流的实现
- 6.1 Feed流的实现方式
- 6.2 Feed流的实现
- 6.3 Feed流中的难点:滚动分页
- 7. 附近商铺
- 8. 即时通讯模块
- 8.1 即时通讯实现的步骤
- 8.2 代码修改
- 9. 定时任务
1.使用Redis代替Session登录
1.1 Session登录存在的问题
- Session的数据都保存在服务器中,数据量很大的时候可能导致服务器内存不足
- 在分布式系统下,每一个服务器的Session数据是独立的,用户在不同服务器之间切换的时候可能因为session问题而需要反复登录
1.2 使用Redis代替Session登录分析
为什么可以是用Redis代替Session
- Redis是基于内存的,读写速度非常快,和Session类似
- 多个服务器访问的是同一个Redis,就实现了数据的共享。此外Redis集群内部的数据一致性机制也很棒
Redis中数据结构和key的选择
- 保存验证码到Redis:由于在登录中还需要根据用户手机号来从Redis中获取验证码并比对,并且保存到信息相对简单,因此采用的key就是手机号,value保存验证码。采用String类型保存。
- 登录key设计:登录key需要满足脱敏性和唯一性。因此使用UUID作为key,value是经过脱敏后的用户对象JSON字符串。这里没有使用Hash存储,主要因为保存的数据相对简单,并且没有对用户中某一个属性进行访问的需求。
1.3 使用Redis登录的流程
发送验证码流程
- 验证手机号
- 获取验证码
- 将验证码保存到Redis中。key为手机号
登录流程
- 验证手机号
- 获取用户输入验证码,并根据手机号从Redis中获取验证码,进行比较
- 通过后,生成UUID的token。将用户信息脱敏后以token为key保存到Redis中
- 向客户端返回token
1.4 解决Redis中有效期问题
存在的问题
根据上述逻辑,Redis中保存用户信息的记录只在用户登录时设置了30min。用户访问其他网页时,记录并不会向session一样续期。
- 方案一:在登录拦截器中,从Redis中获取到用户信息,然后续期。但是对于这个项目来说,查看商店信息等请求并不会被拦截,因为不登录也可以看。当登录用户访问这些请求的时候,Redis并不会续期,导致记录过期。
- 方案二:在登录拦截器之间,加一个全局拦截器,这个拦截器拦截所有请求,并放行所有请求。它主要的作用时判断用户是否登录,如果登录,则续期。
2.使用Redis作为缓存
2.1 为什么使用缓存
在高并发的场景下,用户对于一些热点数据,如商品信息等访问请求量很大,如果每一次请求都访问数据库,那么对数据库等压力是非常大的。因此考虑使用缓存。
2.2 缓存策略
考虑了使用缓存就不得不谈缓存和数据库的数据一致性问题。在这个项目中,采用了在代码中手动更新的方式来保证数据的一致性。下面还有一些细节问题:
问题1: 当数据库中的数据发生变化的时候,是删除缓存还是更新缓存?
应该是删除缓存。因为当数据库中的某一个数据发生多次变化,而在这期间没有请求访问数据库,那么更新缓存的操作只有最后一次有效,因此应该选择删除缓存。当有访问请求的时候再去重建缓存。
问题2: 如何保证缓存和数据库的操作同时成功?
在单体项目中使用事务注解,在分布式中使用分布式事务框架
问题3: 先操作数据库还是先操作缓存?
应该是先操作数据库。因为如果先操作缓存,那么把缓存删除掉,此时有一个请求访问缓存,发现缓存中没有数据,那么就访问数据库重建缓存,而此时数据库还没有进行修改。当数据修改完成后,又有新的请求访问,一看缓存中有,那么直接读取之间的脏数据。
2.3 解决缓存穿透
什么是缓存穿透?
一个请求始终访问数据库和缓存中都没有的记录,导致每一次请求都需要到数据库中进行查询,给数据库造成巨大压力。这种现象就是缓存穿透。
解决方案: 本项目中采用缓存空对象的方法解决这个问题。
- 当一个请求请求某一个数据,然后查询到缓存中没有,则到数据库查询
- 如果数据库中也没有查询到,则在缓存中创建一个空对象。
- 这样当这个请求再次发送时,缓存中就有了一个空对象数据。我们在查询缓存时判断如果从缓存中查询到的结果是空对象,那么就直接返回查询不到即可。
这样就解决了缓存穿透的问题。但是这个方案还有一些缺点:比如如果一开始请求的这个数据不存在,后期存在了,然而缓存中保存的还是空对象,就导致这个新增的数据查询不到。我们可以通过合理的设置空对象的过期时间来缓解这个问题。
除此之外,在项目中增加ID值的长度,这样的话如果有人恶意的访问某一个不存在的数据,在前端检测的时候直接发现ID不合法,那么也可以避免对数据库的访问。
2.4 解决缓存雪崩
什么是缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。、
解决方案:给不同的Key的TTL添加随机值。保证key不会同时失效。利用Redis集群提高服务的可用性
2.5 解决缓存击穿
什么是缓存击穿
缓存击穿也叫做热点key问题。在高并发的情况下,某一个热点key缓存过期,导致大量的请求直接发送到数据库,导致数据库压力过大。
比如说商品列表,同时有很多人访问。一旦商品列表的缓存过期,那么很多人的请求就直接打到数据库上,数据库就很有可能出现宕机的情况。
解决方案1: 可以利用互斥锁的方式解决。
- 当一个请求发现热点key过期以后,直接获取互斥锁,一旦获取到互斥锁,那么就开始访问数据库,然后重建缓存。
- 其他的请求发现缓存过期,但是已经有其他的请求获取到了互斥锁,所以这个请求只能等待。
- 当重建缓存的请求结束以后。其他的请求又发过来,此时缓存已经建立完毕,所以请求不会到数据库中。
这个互斥锁可以使用setnx命令来实现,因为setnx命令只允许一个请求成功,其他的都失败。
由于使用了互斥锁,并发性会降低一些。
解决方案2: 使用逻辑过期的方式。
这种方案创建的热点key的TTL是-1。从而不会因为缓存失效而缓存击穿
- 在缓存中保存数据的同时会保存一个逻辑过期时间的时间戳。
- 当从缓存中查询到这个数据的时候,首先获取它的逻辑过期时间。
- 如果已经过期,单独的去开辟一个线程用来重建数据。当前线程直接返回缓存中已经过期的数据。
这种方法的好处是保证了程序的并发性,但是在某一段时间内,程序读取到的数据是脏数据。
3.秒杀功能
3.1 全剧唯一ID生成器
用户在对优惠券抢购时,一个订单就对应一个订单ID,在高并发情况下需要保证ID的一下特点:
- 唯一性:ID必须保证唯一
- 高可用:可以以极快的速度生成唯一ID
- 递增型:递增的ID有利于在数据库中建立索引
在本项目中,自己实现了一个ID生成器。生成的ID结构如下:
- 第一位,符号位,永远为0
- 时间戳位:31bit,以秒为单位,可以使用69年。当前时间戳–自定义的起始时间戳
- 32bit,秒内的计数器,支持每秒产生2^32个不同ID。这个使用Redis中的incr函数实现
Key的设计incr:业务名:日期
如果一个业务只建立一个key,那么随着时间的推移,redis中的value会达到上限,此时ID生成器就不可用了。
3.2 秒杀业务V1.0 使用乐观锁解决超卖问题
超卖问题的产生
假设目前库存位为1,在高并发环境下:
- 线程1执行判断是否有库存的操作,然后时间片结束。结束的时候线程1并没有完成创建订单的操作。
- 此时线程2获得时间片,开始判断是否有库存,由于线程1并没有完成下单操作,因此此时库存仍然为1。
- 这时,线程1获得时间片,完成下单操作。
- 线程2获得时间片,完成下单操作
那么就会出现超卖问题,此时库存应该为-1
解决方案
目前解决方案有两种:
- 使用悲观锁,在下单的业务上使用sync关键字。但是这样会导致秒杀业务变成串行执行,严重降低并发性。
- 使用乐观锁,在更新库存的时候,判断一下是否和查询库存的时候结果一样,如果一样,则说明数据没有被修改过,则可以执行。如果库存和之前不一样,则回滚。
使用乐观锁解决超卖问题
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1") //set stock = stock -1
.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update();
最开始的思路就是在更新库存的时候,判断一下库存是不是和当初查询库存的时候一样,如果一样就更新,否则就不更新。
在测试的时候发现,有很多的优惠卷没有卖出去。在还有库存的时候,就提示库存不足。
问题的原因在于:通过分析上述的逻辑可以得出,假设同时有100个线程同时拿到了100个库存,那么他们拿到的版本号就应该是相同的,此时只能有一个线程扣减库存成功,修改了版本号,其余所有线程会因为版本号不一致而扣减失败。因此我们还需要进一步的优化。
问题分析以后,我们发现实际上在更新库存的时候只需要判断库存不为空,就可以满足不超卖的条件,因此修改代码为:
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0);
//where id = ? and stock > 0
3.3 秒杀业务V1.1 实现一人一单秒杀
优惠卷的目的主要是引流,如果一个人可以购买多张优惠卷,那么就没有意义了。因此还需要完成一人一单的功能。
存在的问题
从理论上来说,只需要在代码中判断完库存充足以后,然后在判断一下用户是否下过单即可,如果下过单,则直接返回异常信息。但是在使用JMeter测试时,发现依然可以一个用户下多个订单的情况。
产生这个问题的原因还是线程的安全问题。
-
假设线程1和线程2都是同一个用户多次点击购买发出请求对应的线程。
-
那么当线程1判断用户有购买资格的时候。线程2突然获得时间片,那么线程2也判断用户是否下过单。
-
由于线程1还没有下单,因此线程2查询到用户也是有购买资格的。然后就继续执行下单操作。
-
此时,线程1获取时间片,由于已经判断过是否有购买资格,线程1直接执行下单操作,这样就导致了一个人下单多次的情况。
解决方案
根据刚才解决超卖问题的经验,那么解决这个问题也应该可以使用悲观锁或乐观锁,但是实际上这个问题只能使用悲观锁。因为这一次时插入数据,并不是更新数据,因此不存在所谓的版本号,也就无法使用乐观锁。
确定加锁范围
经过分析可以发现:需要保证判断用户是否下过单和创建订单的操作要满足原子性。也就是说在判断完是否可以下单以后,其他的线程不得访问这一部分代码。所以我们要把锁加在判断用户是否下过单和创建订单的逻辑上。
确定锁的粒度
通常情况下,在非静态方法中,sync的锁监视器都是this对象,在代码中this指代应该是VoucherOrderServiceImpl
,而这个对象是一个bean,由Spring创建,并且是单粒的。因此如果在sync中使用this作为锁监视器,那么所有线程都共享这一个锁监视器,那么加锁部分的代码就变成了完全串行执行。
而实际上,我们只需要同一个用户发出的不同线程串行执行,不同用户的线程可以并发执行。因此不可以使用this作为锁监视器。这里我们考虑使用UserId作为锁监视器,userId.toString().intern()
通过这个代码,把用户id变成常量,这样相同的用户会共享一个锁监视器,从而完成业务。
Spring事务管理与sync关键字
目前,加锁的方法位于createVoucherOrder()
中,这个方法上面有声明式事物注解,那么就有可能出现锁已经释放了,但是事物还没有提交,那么之中情况也会出现超卖的现象。
锁已经释放了,说明其他的线程可以进来,而此时还有提交事物,也就是说订单还没有写入数据库,此时进来道线程还是可以查询到该用户有购买资格,那么就会再次下单,导致一个人多次下单的问题。
因此需要先提交事务,再释放锁。所以需要将这部代码抽取成一个方法,然后从外部调用。
Spring实现事务方式
我们知道Spring中实现事务是通过动态代理来实现的,也就是说Spring调用的实际上是VoucherOrderServiceImpl的代理类对象,而在上面的代码中,我们直接使用了this,也就是说是直接调用的VoucherOrderServiceImpl中的方法,会导致声明式事物失效。因此需要先获取到当前对象的代理类对象,然后通过代理类对象对方法进行调用,才可以使声明式事务生效。
3.4 秒杀业务V1.2 实现集群环境下一人一单
存在的问题
在上一个版本中提到的解决一人一单的方法是通过加sync代码块实现。但是这种方法在集群环境下不适用。主要原因是因为在集群环境下,每一个服务器都是一个独立的JVM,线程1和线程2属于同一个服务器,那么他们之间可以使用sync代码块实现互斥访问,但是线程3线程4位于另一台服务器。由于不同的JVM,他们之间的锁监视器是不共享的,因此线程12和线程34之间是一种并发的状态。那么一人一单的问题就不能得到保证。
解决方法
利用Redis实现一个分布式锁。需要满足一下条件
- 利用Redis中的setnx命令,并加以设置过期时间。
- setnx满足互斥性,多个线程执行只有一个返回true
- 使用过期时间可以保证出现故障后锁依然可以释放,不会产生死锁问题。
- 利用redis集群来提高可用性。
- 释放锁的时候需要判断当前的锁是不是自己加的,只有当前的锁是自己加的才可以删除。
- 删除锁的动作需要具备原子性,因此我们使用了Lua脚本实现多条指令的原子性。
因此,我们可以利用Redis构建一个分布式锁。
核心思想是利用了Redis的setnx方法,当多个线程进入时,只有一个线程能够执行setnx方法返回值为true,其余线程因为key已经存在返回的都是false,这就实现互斥。
另外,当该用户的其他线程得到的结果是false的时候,应该直接返回"一个用户只能下一单"的提示,而不是继续等待。
- 利用Redis中的setnx命令,并加以设置过期时间。
- setnx满足互斥性,多个线程执行只有一个返回true
- 使用过期时间可以保证出现故障后锁依然可以释放,不会产生死锁问题。
- 利用redis集群来提高可用性。
- 释放锁的时候需要判断当前的锁是不是自己加的,只有当前的锁是自己加的才可以删除。
- 删除锁的动作需要具备原子性,因此我们使用了Lua脚本实现多条指令的原子性。
Redis分布式锁的key value问题
从理论上来说,key应该是业务名+用户id,但是对value并没有什么要求。但是在实际情况下,value需要设置的UUID+线程ID。具体原因如下:
试想一下一种情况,当线程1获取到锁,在执行业务的时候花费时间很长,导致锁自动超时释放。此时线程2获取到了锁,正在执行业务。此时线程1的业务也执行完了,直接释放锁。导致线程1把线程2的锁给释放了,产生了线程安全的问题。
这个问题实际上就是一个线程删除了本来不属于自己的锁
我们的解决方法是在value里面保存UUID+线程ID,使用UUID的目的是在集群环境下可能会存在线程ID相同的问题。这样在删除锁的时候,线程需要先根据value值判断是不是自己加的锁,如果不是,则说明其他线程已经获取到锁,那么自己执行的业务就应该回滚。如果是自己的锁,那么可以直接释放。
此外,判断锁是不是属于自己和删除锁这两个操作应该保证原子性,否则如果一个线程已经判断完是自己的锁还没有删除的时候,突然失去时间片,导致锁自动释放。另外一个线程又获取到了锁。那么当原来的已经判断完是自己的锁的那么线程再次获取到时间片,就会直接释放锁,而不会再次判断是不是自己的锁,所以还是会释放掉不属于自己的锁。
在这个项目中,采用了Lua脚本的方式来保证判断锁和删除锁的原子性。
3.5 可重入锁的原理
上面我们实现的锁并不具备可重入的功能,当一个线程获取到锁以后,即使它再次获取锁,也会被阻塞。实际上我们可以通过使用hash结构实现可重入锁。
底层采用Redis的哈希存储方式,除了存储以 lock:order:userId作为key,以字段名threadId值为statsu的变量作为值。当线程获取锁的时候,当重入时会先判断一下当前获取锁的线程是不是threadid里面的线程,如果是则status+1
当释放锁的时候,首先判断是不是自己获取的锁。如果是,将statsu-1,然后判断status是不是为0,如果此时为0,则释放锁,否则不释放锁。
3.6 Redisson分布式锁
- 利用哈希结构实现重入
- 利用看门狗机制实现续期
- 利用信号量控制锁重试
3.7 Redission锁的MutiLock
问题:为了提高redis的可靠性,通常会搭建主从集群来扩展redis。试想一下下面的情况:
- 线程1获取锁,redis执行写操作,将锁从master上写入,而由于redis主从之间同步信息是需要时间的,主机上的信息还没有完全同步到从机上,结果主机宕机了。
- 此时根据哨兵机制,会从从机上选择一个作为新的主机,而新的主机上还没有保存之前的锁,就造成了线程安全问题。
解决方案:使用redisson中的MutiLock。
原理:不在使用主从机制。而是所有的redis都是地位相同的节点。此时获取锁需要分别从3个redis结点中获取锁,只有3个结点都写入锁成功,才算获取到锁。
4.使用Lua脚本结合消息队列实现异步秒杀业务
问题分析
在之前的秒杀业务中,我们发现需要多次访问数据库,并且业务也是串行执行的。但是分析一下我们可以发现,我们可以将业务拆分成两个子业务,
-
一个业务只负责判断是否有购买资格,如果有购买资格则直接创建订单信息到消息队列。此时并没有真正的访问数据库创建订单,因此效率会非常高。
-
第二个业务开辟一个单独的线程,从消息队列中读取数据,保存的数据库。
第二个业务并不需要很高的即时性,当第一个业务判断完用户有购买资格后,直接返回,通知用户下单成功即可。
具体实现
- 首先这些优惠券的热点信息,包括库存等信息需要提前保存的Redis中
- 在Redis中一个优惠券对应一个键,value保存库存量
- 在Redis中一个优惠券对应一个set集合,里面保存不可重复列表,列表中每一个元素保存下单成功的用户id
- 用户下单,可以直接访问Redis先判断是否有库存,然后判断是否下过单,如果条件都满足则直接通知用户下单成功。向订单信息保存到消息队列中,由独立进程慢慢的将所有的订单信息都写入数据库。
消息队列的好处在于解耦。最简单的例子生活中取快递的例子。消息队列就相当于菜鸟驿站。快递员就相当于生产者,快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,两者之间没有耦合。但是如果去掉菜鸟驿站,让快递员亲手交给我,那么如果我不在家,那么快递员就只能等待,这就浪费了大量的时间,耦合性高。
5. 好友点赞和关注
5.1 好友点赞
好友点赞
好友点赞的问题应该保证一个好友一个笔记只能点赞一次。因此考虑使用Redis中的set集合实现这个功能。
具体实现方法
在Redis中建立set集合,每一个日志对应一个set集合,set集合中保存的数据就是用户点赞的用户id,这样在用户点赞的时候就可以先查询一下是否点过赞,如果点过赞,则返回错误信息,否则则将用户id记录到set集合中。
有些情况下还会显示最近点赞的用户,我们可以修改set集合为zset,value值就是点赞的时间戳,这样倒序排列求前五个用户即可
5.2 好友共同关注
好友共同关注可以利用Redis中的集合求交集运算来实现。在Redis中保存set集合,每一个集合对应一个用户,集合里面的内容就是该用户关注用户的用户id,这样两个用户求共同关注只需要使用交集运算即可实现。
6. Feed流的实现
6.1 Feed流的实现方式
实现Feed流的三种方式
- 拉模式,也叫做读扩散。当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序
优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清楚。
缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大。
- 推模式,也叫做写扩散。推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了。
优点:时效快,不用临时拉取
缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去
- 推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。推拉模式是一个折中的方案。
- 站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力
- 如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去
- 现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。
6.2 Feed流的实现
在本项目中实现推模式的Feed流功能。
当用户发送笔记的时候,会获取到用户所有关注人的列表,然后将用户发送的笔记投放到其粉丝的邮箱中。
当用户刷动态的时候,会从自己的邮箱中读取到笔记,然后显示。
6.3 Feed流中的难点:滚动分页
问题描述
传统的分页一般是基于下标来实现的,但是在Feed流中,因为我们需要根据动态发布实现逆序排列所有值,也就表明最新发布的动态实际上在数据的最上面,那么一旦发布新动态,那么再使用这种传统的分页方式就会重复的读取部分动态。
传统的分页查询一般通过下标即可实现。但是在feed流中,使用下标会产生问题。原因是因为feed流中的数据是不断变化的,这就会导致消息的下标也是变化的,使用传统的分页方式就会造成数据的重读。下面通过一个例子说明
- 假设在t1 时刻,我们去读取第一页,此时page = 1 ,size = 5 ,那么我们拿到的就是10~6 这几条记录
- 假设现在t2时候又发布了一条记录
- 此时t3 时刻,我们来读取第二页,读取第二页传入的参数是page=2 ,size=5 ,那么此时读取到的第二页实际上是从6 开始,然后是6~2 ,那么我们就读取到了重复的数据,所以feed流的分页,不能采用原始方案来做。
解决方案:使用滚动分页方式
和传统分页不同,我们使用滚动分页,每一次记录上次访问的所有动态中时间戳最小的数据,下一次再从这个数据开始访问pagesize个动态,这样就不会受新发布的动态的影响。
此外在极端情况下,还有可能出现同一个时间戳下发布了多个动态的情况。
此外,我们还需要考虑时间戳相同的情况。虽然概率很小。
举个例子:假设现在有5条数据,时间戳分别是 5 5 5 5 4 3 3 2 ,pageSize是2
- 第一次查询 5 5 lastId = 5 size = 2
- 第二次查询 4 3 lastId = 4 size = 2
显然发生了漏读,因此针对这种情况,我们还需要设置一个偏移量,它的值就是本次查询中最小的时间戳出现的次数。
- 第一次查询 5 5 lastId = 5 size = 2 offset = 2 因为查询结果 5 5 里面最小的时间戳就是5 出现了2次
- 第二次查询,从第1个5开始(包含第一个5)往后走offset个位置的下一个位置就是本次分页的起始位置。
引入了offset以后,就解决了滚动分页查询中时间戳相同导致出漏读问题。
通过使用这种方式,就可以解决Feed流中传统分页模式失效的问题。
7. 附近商铺
使用Redis中GEO数据类型实现。将所有的商铺信息按照商铺类型在Redis中建立对应的数据,然后进行查询即可。
8. 即时通讯模块
互相关注的好友之间会建立好友关系,并可以实现即时通讯功能。
8.1 即时通讯实现的步骤
- 首先所有注册用户都在环信服务器对应一个用户名和密码,这个用户名密码和用户信息都保存在本地的数据库中
- 此外,要想实现好友之间的通讯,还需要记录用户之间的好友关系,这个可以在点击关注的代码中添加判断如果是共同好友,则将好友关系注册到环信中的代码
- 当用户登录到APP后,会从数据库中获取到对应的环信用户名和密码,然后自动登录到环信服务器
- 两个手机端都链接到环信服务器后,就可以进行实时的聊天了,聊天实际上走的是环信的服务器,和本地的探花交友服务器之间没有交互信息。
8.2 代码修改
- 用户注册:在注册用户的同时注册环信用户,并注册到环信服务器
- 用户登录:同时从数据库中获取到环信用户和密码,然后登录到环信服务器
- 用户点击关注:判断是否为共同关注,如果是,则需要注册好友关系到环信服务器
- 用户取消关注:需要删除环信服务器中的好友关系
真正的即时通讯的相关服务器是借助了环信服务器,并不走本地
9. 定时任务
在APP的后台会对每一天,每一周和每一个月的相关用户活跃数据等信息进行实时的统计并展示。这些统计运算实际上非常消耗数据库的资源,因此如果每一次点击这个统计的页面都需要从数据库中重新计算是非常消耗资源的,因此我们的解决方法是将这些统计数据单独的存放到一张数据表中,并且使用定时任务,在服务器压力相对较小的时候来计算这些数据。
这样的话,前台再次访问这些统计数据的时候,就可以直接从数据表中获取到计算好的结果,从而降低数据库压力的同时提高了程序的响应速度。
实现方式就是使用了Spring Task的定时任务,通过编写CORN表达式来控制程序的执行。