项目整理...

一、短信登录功能

用户提交手机号,首先会对手机号进行校验,是否符合手机号的规则,符合则生成一个验证码,将验证码保存在session中,并将验证发送给用户。用户收到验证码后,会带着手机号和验证码来做一个登录请求,服务器收到这个请求后,首先会拿着sessionID去session中取出手机号和验证码,跟请求中的手机号和验证码进行一个校验,如果校验成功,则会拿着手机号去user数据库里面查该用户是否存在,不存在则会创建新用户,存在则直接取出保存到session,以便后续对一些需要做登录校验的请求进行一个校验。登录校验这一块的话,则是某一些请求需要在操作前去做一个登录校验,比如像我的主页,或者查询购物车这类请求。通过从请求的cookie中拿到sessionId,去session中判断是否存在该登录用户,没有的话则会拦截到登录界面,有的话则会保存到ThreadLocal进而放行。

但是上面这种基于session的短信登录会存在session集群共享的问题,比如当前这个服务器上用户完成了登录,session中存了用户的信息,但是在另一个服务器的session中却没有用户信息,这样没法在不同服务器上做登录校验。这里如果使用Tomcat提供的Session拷贝,回增加服务器额外的内存开销,还会带来数据一致性的问题。所以这里我们使用了Redis缓存来做用户登录校验。

用Redis来实现短信验证码登录和登录校验功能。之前存在Session中的东西,现在都是存到Redis。验证码的保存中,key是phone:手机号,value则是验证码的值。这里做验证码的校验则是从Redis中取。登录之后会通过手机号去user数据库中查询用户是否存在,不存在创建,存在则会将用户保存到Redis中,以方便后面做登录校验。将用户保存到Redis中,我们使用Hash的数据结构,我们key是token:xxx...(这里后面是随机生成的一个UUID),value则是用户信息的hash-key,hash-value的pair。这里登录校验时,我们则是从请求中取出token,拿token去Redis中获取用户数据,判断用户是否登录。

登录校验功能我们则是通过拦截器来做的。拦截器的话需要先写拦截器类,实现HandlerInterceptor接口,重写preHandle方法(看需要可能还要重写postHandle和afterCompletion)。然后需要在webMvcConfig中重写addInterceptors方法去添加拦截器。这里我们实现了两个拦截器,做成一个拦截器链,其中一个拦截器拦截一切路径,用于刷新token,另一个拦截器则拦截需要登录的路径,用来做登录校验。首先,拦截一切路径的拦截器会从请求中获取token,并拿着token去Redis中查询用户,将查到的用户保存在ThreadLocal中,并将token有效期刷新(这个token刷新的意义在于,如果用户登录了之后,用户信息存储在Redis中,但是我们设置了TTL,比如30分钟,这样时间一到,这个用户信息会自动删除,从而在下次要做登录校验时会不通过,这样会严重影响用户体验)。第二个拦截器则只拦截需要登录的路径,直接去ThreadLocal中查询是否存在用户,也就是前面拦截器中拿着token在Redis中有没有查到用户信息,如果ThreadLocal中有用户,则放行,否则则拦截。

二、添加商铺缓存

缓存的使用降低了后端数据库压力,提高了读写的效率,降低了响应时间。这里我们将商铺信息缓存到Redis中,这样客户端查询店铺信息时,则拿着商铺id,先去Redis查询商铺缓存,如果命中则直接返回,如果未命中则拿着id去查询数据库,不存在则返回404,存在则将店铺数据写入Redis,并返回店铺信息。这里我们使用的是String数据结构来存店铺信息,key是cache_shop_key拼上店铺id,value是json字符串。

目前这样还会有一些数据一致性的问题,也就是我们在修改数据库数据后(比如修改了商铺信息),会造成缓存数据与数据库数据不一致,我们需要选择一个合适的缓存更新策略。

这里,我们使用主动更新策略,在修改数据库的同时更新缓存。首先需要考虑一个问题,在更新完数据库后,是使用更新缓存模式还是使用删除缓存模式。如果是更新缓存模式,也就是每次更新数据库都更新缓存,这实际上会造成很多无效写操作。如果执行多次数据库更新,同时更新缓存,但这期间并没有查询请求,这样更新缓存是没有意义的。所以我们使用删除缓存,也就是更新数据库并删除缓存,查询时更新缓存,这样无效写操作较少。确认了使用删除缓存模式之后,还有一个问题就是先删缓存再更新数据库,还是先更新数据库再删除缓存。先删缓存再更新数据库,如果在高并发的场景下,删除缓存可能导致请求直接打到数据库,给数据库带来巨大压力,这是很可能发生的,因为更新数据库可能耗时会比较久。所以我们这里选择先更新数据库再删缓存,这里为了保证缓存与数据库的操作的原子性,对于单体系统,我们将缓存与数据库操作放在了同一个事务中。

综上,我们采用缓存主动更新来解决数据一致性问题,使用先操作数据库再删除缓存的模式,避免线程安全问题,采用TTL过期和内存淘汰机制作为兜底方案,同时将缓存和数据库的操作放到同一个事务中来保证操作的原子性。

三、缓存三大问题

缓存穿透

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远命中不了,所有的请求都直接打在数据库上,给数据库带来巨大压力,甚至直接导致数据库崩溃。

解决:

一个是缓存空对象,也就是在数据库未命中时,向Redis中写入空对象,并设置TTL,这样下次再查询这个id时,则Redis直接会命中并返回空对象。另一个则是布隆过滤器,也就是在Redis之前添加一个布隆过滤器,先对商铺id是否存在做出判断,如果存在则放行,否则直接拒绝。

上面两种都是被动的解决缓存穿透的方案,除此之外,我们还可以采用主动的方案来预防缓存穿透。比如:增强id的复杂度以免id被猜到,做好数据的基础格式校验,加强用户权限校验,做好热点参数的限流等。

这里,我们在之前查询商户缓存的流程中添加了解决缓存穿透的部分:

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求直接打到数据库,带来巨大压力。

解决:给不同的key的TTL添加随机值,利用Redis集群提高服务的可用性,给缓存业务添加降级限流策略,给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且重建缓存比较复杂的key突然失效了,导致大量的请求直接打到服务器上。

解决:

一、互斥锁(防止所有线程请求未命中都去重建缓存,现在是只有拿到互斥锁的才能去)

这里使用Redis中的setnx来实现互斥锁,只有当值不存在时,才能进行set操作。也就是添加一个key为lock:shop:+id,值为set数据类型的key-value,并设置TTL防止死锁,这里TTL的设置一般是业务处理时长的10-20倍。

二、逻辑过期

逻辑过期并不是真的过期(这里我们给数据设的TTL为-1),而是增加一个字段来标记key的过期时间(比如店铺信息修改了,通过这个逻辑过期时间来去更新缓存),这样数据就永不过期了,从根本上解决了热点key过期导致的缓存击穿。一般秒杀场景,请求量比较大时,就可以使用逻辑过期,等活动结束再手动删除逻辑过期的数据。

注意这里跟互斥锁方案不同的是,因为我们已经将店铺数据加入缓存并且设置了TTL为-1,所以如果查询缓存未命中,我们直接返回空即可。如果命中,则判断缓存是否逻辑过期,如果未过期则返回店铺信息(表示此时缓存数据就是最新数据),如果过期,则尝试获取互斥锁,并判断获取互斥锁是否成功,如果不成功,则直接返回缓存命中的店铺信息,如果成功,则开启独立线程去数据库查店铺数据,并直接返回店铺信息,而独立线程那边则根据id查询数据库,并将查询到的数据写入Redis,重新设置逻辑过期时间,最后释放互斥锁。

可以看到,基于互斥锁的解决方案,优点一方面是实现起来更加容易,且数据一致性高,内存占用较小,但是缺点则是性能比较低,因为相当于只有一个线程拿着互斥锁去进行缓存重建,其他所有线程都在重复查询缓存未命中获取互斥锁这个流程,导致串行化,另一个缺点则是可能会造成死锁。而基于逻辑过期的解决方案,优点性能高,缺点则是内存占用大,一致性相对来说差一些,一致性的问题就出在,当逻辑过期时,在重建好缓存之前,获取到互斥锁的线程,去开一个独立线程来做缓存重建工作,直接返回之前老的店铺信息,而未获取到互斥锁的线程,也是直接返回老的店铺信息,这里就会造成返回的店铺信息和缓存中的店铺数据不一致的情况。

四、优惠券秒杀

分布式ID

优惠券秒杀业务即下单秒杀券,也就是生成订单保存到voucher_order这张表中,首先会涉及到一个订单id生成的问题。这个订单id如果直接使用数据库自增id,会存在一系列问题,包括id规律太明显,可能被别人猜到然后做一些恶意攻击,以及泄露订单数量和MySQL单表数据量限制等问题。

这里我们使用了分布式ID来解决上述问题,我们使用的分布式ID实现方案为:

一个符号位,永远为0(表示正数),31个bit为时间戳,以秒为单位,这里可以用69年,后面32bit为秒内的计数器,支持每秒生成2^32个不同的ID。

最原始的优惠券秒杀过程:

最原始的流程就是提交优惠券id,然后拿着id去seckill_voucher表中查询秒杀券信息,先判断秒杀是否开始,在判断秒杀券库存是否充足,都满足就去扣减库存,创建订单,再把订单id返回出去。

但是目前这样在高并发场景下会存在问题,判断库存和扣减库存这里可能会出现线程不安全问题:

超卖(查询库存到扣减库存之间发生线程安全问题)

也就是线程1在查询完库存之后,创建订单完成之前,线程2和线程3也去做了查询库存操作,发现库存充足,也去创建订单,最终导致超卖的问题产生。

解决方案:

悲观锁:认为线程安全问题一定会发生,因此在扣减库存之前都要先获取锁,确保线程串行执行。常见的悲观锁有:syschronized、lock

乐观锁:认为线程安全问题不一定发生,因此不加锁,只会在更新数据库的时候去判断其他线程是否对数据进行了修改,如果没有修改则认为是安全的,直接更新数据库中的数据,如果修改了则认说明不安全,直接抛异常或者等待重试。常见的乐观锁实现方法有:版本号法、CAS操作、乐观锁算法

乐观锁和悲观锁的比较:

  • 悲观锁比乐观锁的性能低:悲观锁需要先加锁再操作,而乐观锁不需要枷锁,所以乐观锁通常具有更好的性能
  • 悲观锁比乐观锁的冲突处理能力低:悲观锁在冲突发生时直接阻塞其他线程,乐观锁则是在提交阶段检查冲突并重试
  • 悲观锁比乐观锁的并发度低:悲观锁存在锁粒度较大的问题,可能会限制并发性能,而乐观锁可以实现较高的并发度
  • 应用场景:两者都是互斥锁,悲观锁适合写多读少,冲突频繁的场景,而乐观锁适合读多写少、冲突较少的场景

下面通过乐观锁解决超卖问题(查询库存到扣减库存之间发生线程安全问题):

实现方式一:版本号法

首先我们要为seckill_voucher表新增一个版本号字段version,线程1查询库存时同时会查询版本号,在进行库存扣减时,同时会将版本号跟是之前查询的版本号进行比对,如果相同,则进行库存扣减操作,并且将版本号+1。在线程1查询库存到扣减库存之间,线程2去查询了库存和版本号,但是当线程2发现库存充足准备去扣减库存时,会发现版本号和之前查询到的版本号不一致,这就说明数据库中的数据已经发生了修改,需要进行重试或者直接抛异常中断。

实现方式二:CAS法

CAS法其实和版本号法类似,只是不需要添加一个version字段,消耗更多内存,而是直接使用库存作为版本号,也就是在进行扣减库存时,同时去判断此时库存是否与查询库存时的库存是否相同,相同则进行扣减操作,不同则重试或者抛异常中断。

但是上面这两种乐观锁的方案,实际上会有很大的问题,就是成功率非常低,像乐观锁这种基于数据变更来判断的,实际上在扣减库存的时候只要库存充足都应该执行扣减,而由于乐观锁基于数据变更来判断,会导致只要数据被修改了,都不允许扣减(比如1000个线程来去做扣减库存的操作,他们查到库存都是100,只有最先做完扣减的那个线程能扣减成功,其他都会失败)。

解决:由于我们这里时判断库存比较特殊,所以实际上我们不需要去判断数据与查询的时候是否一致,而是仅仅要求库存大于0即可。除了这个方案还有一些其他方案,比如有些场景下,它不是库存,只能通过数据有没有变化来去判断是否安全,那么这种情况下要想提高成功率,我们可以采用分批加锁的方案(分段锁),也就是说我们可以把数据里的资源分成几份,比如我们资源是100,我们可以把这100个资源分到十张表里面,那么在用户在抢的时候可以在多张表里面分别去抢,这样一来成功率能提高十倍,这样的分段锁方案实际上是让每个锁锁的资源减少,来提高成功率。

单体下的一人多单问题

初始解决方案是在下单时,拿着voucherId和userId去voucher_order中查是否存在订单,如果订单不存在则创建,订单存在则返回异常。

通过Jmeter压测,目前这样还是会出现一人多单的问题,问题原因就是多个线程在去查询订单的时候都发现订单不存在,然后这些线程都开始去创建订单扣减库存,依然照成了一人多单的问题。

解决方案:

前面乐观锁是判断数据是否更改,而当前是判断数据是否存在。这里我们直接使用了悲观锁来解决一人多单的问题,使用了synchronized对创建订单部分代码加锁。

集群下的一人多单问题

之前在单体模式下,我们通过synchronized这种悲观锁对下单过程进行加锁,解决了一人多单的问题。但是在集群下,这样的解决方式则存在问题,由于syschronized是本地锁,只能提供线程级别的同步,每个JVM都有一把synchronized锁,不能跨JVM进行上锁,当一个线程进入被synchronized 关键字修饰的方法或代码块时,它会尝试获取对象的内置锁(也称为监视器锁)。如果该锁没有被其他线程占用,则当前线程获得锁,可以继续执行代码;否则,当前线程将进入阻塞状态,直到获取到锁为止。而现在我们是创建了两个节点,也就意味着有两个JVM,所以synchronized会失效。

解决:

分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁

前面synchronized锁失效时因为由于每一个JVM都有一个独立的锁监视器,用于监视当前的JVM中的synchronized锁,所以无法保证在集群下只有一个线程访问一个锁内的代码块。所以这里我们将使用一个分布式锁,在整个系统的全局中设置一个锁监视器,从而保证不同的JVM都能够识别到,从而实现集群下只有一个线程能拿到锁,执行加锁的内容。

这里我们实现使用的是基于Redis的分布式锁,使用setnx来作为分布式锁,setnx命令用于指定的key不存在时,为key设置指定的值,也就是setnx的key如果已经存在,则会返回0,如果这个key不存在,可会将key value进行存入(set [key] [value] ex [time] nx)

实现分布式锁需要创建一个分布式锁类,继承Lock接口,重写tryLock和unLock方法,这里tryLock则是执行SET "lock:order:userId" id EX timeoutSec NX,这里key是lock:order:userId,也就是锁加业务名加用户id,一个用户对应一个锁,value则是当前线程id,timeoutSec是锁过期时间。这样,我们一个用户就有一个唯一的分布式锁,当第一次执行SET "lock:order:userId" id EX timeoutSec NX时,能成功,而后面其他所有线程拿着这个userId都无法再进行setnx操作,起到一个互斥的作用。注意,这里为了保证锁发生异常时能够释放,我们使用了try...finally...来确保锁释放。

当前分布式锁的问题一

当线程1获取了锁之后,由于阻塞,线程1拿到的锁超时释放了,这时线程2拿到了锁并去执行业务,在执行业务期间,线程一把业务完成了开始释放锁,然后把线程2获取到的锁给释放了,这时线程3又获取到锁,继续去执行业务。这样就还是会出现一人多单的一个问题。

解决:这里我们通过为分布式锁添加线程表示的方法来解决该问题,也就是在释放锁的时候,去判断一下当前锁是不是自己的拿到的锁,如果是自己的锁则直接释放,如果不是自己的就不释放锁,从而解决多个线程同时获得到锁导致的一人多单问题。

这里存入线程标识的实现方法,就是在SET "lock:order:userId" id EX timeoutSec NX这里,这个value存入的是"UUID-线程id",这里UUID是随机生成的,因为为了区分开不同线程可能拿到相同的线程ID,这样每当执行unlock时,我们需要拿着当前UUID-线程id和redis里面存的setnx的value进行一个比对(看这把锁是不是自己的),只有一致才去做释放锁的操作。

当前分布式锁的问题二

上面我们通过给锁添加了一个UUID-线程id的锁标识,在释放锁的时候先去做一个判断,从而解决了锁超时释放后,误删锁的一个问题。但是还会有的问题是,线程1获取锁,执行完业务,判断当前锁是自己的锁后,这时发生了阻塞,结果锁又被超时释放了,线程2立马获取到锁开始执行业务,此时线程1阻塞完成,由于已经判断过是自己的锁了,于是直接就去删除了锁,结果删的是线程2的锁,这又让线程3获取到锁开始执行业务。这样的话又出现了一人多单的问题。

解决:

那么我们该如何保证判断锁和释放锁的原子性?也就是让判断锁和释放锁的操作同成功同失败。这里我们使用了Lua脚本。

那么Lua脚本是如何确保原子性的呢?Redis使用(支持)相同的Lua解释器,来运行所有的命令。Redis还保证脚本以原子方式执行:在执行脚本时,不会执行其他脚本或Redis命令。这个语义类似于MULTI(开启事务)/EXEC(触发事务,一并执行事务中的所有命令)。从所有其他客户端的角度来看,脚本的效果要么仍然不可见,要么已经完成。

注意:虽然Redis在单个Lua脚本的执行期间会暂停其他脚本和Redis命令,以确保脚本的执行是原子的,但如果Lua脚本本身出错,那么无法完全保证原子性。也就是说Lua脚本中的Redis指令出错,会发生回滚以确保原子性,但Lua脚本本身出错就无法保障原子性。

Redisson
经过优化1和优化2,我们实现的分布式锁已经达到生产可用级别了,但是还不够完善,比如:

  • 分布式锁不可重入:不可重入是指同一线程不能重复获取同一把锁。比如,方法A中调用方法B,方法A需要获取分布式锁,方法B同样需要获取分布式锁,线程1进入方法A获取了一次锁,进入方法B又获取一次锁,由于锁不可重入,所以就会导致死锁
  • 分布式锁不可重试:获取锁只尝试一次就返回false,没有重试机制,这会导致数据丢失,比如线程1获取锁,然后要将数据写入数据库,但是当前的锁被线程2占用了,线程1直接就结束了而不去重试,这就导致数据发生了丢失
  • 分布式锁超时释放:超市释放机机制虽然一定程度避免了死锁发生的概率,但是如果业务执行耗时过长,期间锁就释放了,这样存在安全隐患。锁的有效期过短,容易出现业务没执行完就被释放,锁的有效期过长,容易出现死锁,所以这是一个大难题!
  • 我们可以设置一个较短的有效期,但是加上一个 心跳机制 和 自动续期:在锁被获取后,可以使用心跳机制并自动续期锁的持有时间。通过定期发送心跳请求,显示地告知其他线程或系统锁还在使用中,同时更新锁的过期时间。如果某个线程持有锁的时间超过了预设的有效时间,其他线程可以尝试重新获取锁。
  • 主从一致性问题:如果Redis提供了主从集群,主从同步存在延迟,线程1获取了锁

我们如果想要更进一步优化分布式锁,当然是可以的,但是没必要,除非是迫不得已,我们完全可以直接使用已经造好的轮子,比如:Redisson。Redssion是一个十分成熟的Redis框架,功能也很多,比如:分布式锁和同步器、分布式对象、分布式集合、分布式服务,各种Redis实现分布式的解决方案。简而言之Redisson就是一个使用Redis解决分布式问题的方案的集合,当然它不仅仅是解决分布式相关问题,还包含其它的一些问题。

秒杀业务总结:

最开始我们遇到自增ID问题,我们同通过分布式ID解决了问题,具体来说就是ID设置为了32bit,第一位是0,然后31bit是时间戳,最后32位是秒内计数器。后面我们在单体系统下遇到了超卖的问题,我们通过乐观锁(版本号法,CAS法)解决了,我们先尝试了使用CAS法,但是错误率较高,基于特殊的库存场景,我们将乐观锁做成了判断库存大于0,解决了超卖的问题。我们通过用户在下单时,拿着用户id和优惠券id去订单表查询订单是否存在,初步解决了一人多单的问题,结果在高并发的场景下依然出现了一人多单,因为这里在判断存在和下单中间可能有线程安全问题,最后我们用悲观锁进行了解决,也就是对下单流程加了synchronized锁。由于用户量激增,我们将单体系统升级为了集群,结果由于synchronized锁是JVM内部锁监视器可见,导致在高并发场景下,同一用户发送多个下单请求导致一人多单的问题。这样我们则需要一把所有JVM可见的锁,我们通过实现分布式锁解决了集群下一人多单的问题。具体来说,我们使用了setnx来作为分布式锁,这个key是lock:order:userId的模式,使得一个用户只能获取到一把锁,初步解决了一人多单的问题。但是这样设计的分布式锁仍然有问题,如果出现阻塞导致锁超时释放,其他线程拿到锁进行下单操作,这时最开始拿到锁的线程直接去把锁释放了,导致又有其他线程拿到锁执行下单,最终导致一人多单,这里我们通过给锁添加线程标识解决了问题,也就是我们给value赋值为了UUID-线程id的格式,在每次释放锁时都要先去拿着当前的UUID-线程id和Redis中取出的value进行匹配。但是这里判断锁和释放锁的操作中间仍然可能发生阻塞,也就是判断完锁是自己的之后,阻塞导致超时释放,再去释放锁的时候又释放了其他线程拿到的锁。这里判断锁和释放锁的操作不是原子性的,依旧可能导致一人多单问题出现,我们通过将判断锁和释放锁两个操作封装到一个Lua脚本解决了原子性问题。为了解决锁的不可重入的问题,我们通过将锁以hash结构的形式存储,每次获取锁时value+1,释放锁时value-1,只有当value为0时才能释放锁,从而实现了锁的可重入性,并且将获取锁和释放锁的操作封装到Lua脚本中以确保原子性。最后我们直接使用了现有的成熟的分布式锁Redisson来解决我们设计的分布式锁的不可重试、不可重入、原子性等问题。

点赞功能和点赞排行榜

首先点赞功能是针对blog,也就是探店笔记的,blog表中有一个likes字段来保存该blog的点赞次数。对于点赞这种高频变化的数据,如果我们使用MySQL来存储一个blog的点赞对象,是不合适的,因为MySQL慢,且并发请求MySQL会影响到其他重要业务,容易影响整个系统的性能,继而降低了用户体验,这里我们使用Redis来存每篇blog点赞的人。现在存在一个问题,一个用户可以无限点赞,这肯定不合理,所以我们首先需要对点赞功能进行一个优化,实现一人只能点一次赞。根据这个要求,Redis的Set数据结构则非常合适用来存储,一方面,Set有不重复的特性,保证一个用户只能点一次赞,其次,Set具有高性能,它内部实现了高效的数据结构(Hash表),最后,Set具有很强的灵活性,它可以实现一对多,一个用户可以给多个blog点赞,也符合业务逻辑。

在平时我们使用的软件中,点赞功能都是默认按照时间顺序对点赞用户进行一个排序,先点赞的用户会放在最前面,而Set是无须的,无法满足这个要求。这里我们使用SortedSet这个数据结构来存储点赞用户,它满足了唯一、有序、查找效率高的要求。在使用SortedSet来存储点赞用户时,key为特定前缀加blogId的模式,value为点赞用户的id,score则为对应点赞用户点赞的时间戳

跟Set集合不同的是,在判断用户是否点过赞这个问题上,Set是用isMember来判断,而SortedSet没有isMember方法,这里我们使用ZSCORE方法来判断是否存在,ZSCORE是从SortedSet中获取对应value的score,返回获取到的score,如果获取到了score,则说明该用户已经点过赞。之前Set是无法进行范围查询的,也就无法获得排行榜的前几名数据,SortedSet可以使用ZRANGE方法实现范围查询,比如需要取出前五名,则opsForZSet().range(ket, 0, 4)。

五、好友关注和取关

关注是用一张follow表来做的,字段包括user_id,follow_user_id和create_time。那么判断是否关注则是拿着user_id和follow_user_id去follow表中做count操作,如果count>0则已关注,isFollow=true,这样我们在去点击关注时,则会一起传入一个isFollow这个参数,根据isFollow来判断是向follow中添加数据还是删除数据。

共同关注:

我们想要查询出两个用户的共同关注用户,则需要求交集。也就是我们在之前关注和取关的实现中,我们不仅将用户关注数据存入follow表中,还将每个用户的关注用户存入了Redis中来做这个共同关注的功能。我们这里用了Set的数据结构,key则是FOLLOW_LEY(前缀)+userId的格式,value则是followUserId。这样一来,我们在Redis中,我们以Set的数据结构存入了用户的关注用户。那么/follow/common/{id}查询共同关注用户时,我们只需要通过opsForSet().intersect(key1, key2)来求取两个关注集合的交集即可。

六、附近商铺搜索

GEO数据结构

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:

  • GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式并返回
  • GEOPOS:返回指定member的坐标
  • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
  • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
  • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能

七、用户签到和连续签到统计

BitMap的操作命令有:

SETBIT:向指定位置(offset)存入一个0或1(SETBIT key offset value)

GETBIT :获取指定位置(offset)的bit值

BITCOUNT :统计BitMap中值为1的bit位的数量

BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值

BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回

BITOP :将多个BitMap的结果做位运算(与 、或、异或)

BITPOS :查找bit数组中指定范围内第一个0或1出现的位置

那么签到的流程就是,先获取当前登录用户的userId,获取日期,将日期转化为yyyyMM的格式,这样key就是USER_SIGH_KEY(前缀):userId:yyyyMM的格式,也就是一个数据保存一个用户一个月的签到情况,这样也方便后续统计,获取到当天是这个月的第几天dayOfMonth-1后,通过opsForValue().setBti(key, dayOfMonth-1, true)进行存储。

连续签到天数则是从最后一次签到开始,向前统计,知道遇到第一次未签到为止,计算总的签到次数,就是连续签到天数。我们先通过BITFIELD key GET u[dayOfMonth] 0获取当前用户本月到今天为止的所有签到数据,比如u20则是本月前21天的签到数据,0表示从第0天开始。我们通过与1做与运算和移位的操作来进行统计。补签的话我们则可以直接使用SETBIT命令来补签。-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值