前言
注:
大家好我是妈妈的好大儿,
笔者联系方式
QQ:3302254385
微信:yxc3302254385
交个朋友!
文章内容很多不想看业务场景的,可以直接通过目录找到需要的代码cv!
创作不易,三连十分感谢!!!
业务场景分析
先从业务场景说起:
类似于下图的这种小程序,投票排行榜的场景!!!
先进行分析
- 第一能够投票的爱豆很多,切需要排名
- 第二对于用户的投票数据的需要进行统计,通常并发很高
- 第三用户使用这个小程序,一进来的主页面就是这个排行榜,作为一个访问量很高的热点数据
技术解决方案
根据分析的业务场景我们应该使用什么技术解决方案更好,会产生什么问题呢!!
- 对于这种投票的场景其实数据的实时性是要求不高的,我们可以使用缓存全盘替代我们的数据库,因为1.并发高,访问量大的特点,不可能使用数据库直接去进行查询和同步更新,单机必炸!!!2.使用缓存节省数据库成本3.缓存速度快
- 由于我们使用缓存来全盘代替数据库,需要将爱豆点赞数,爱豆排行榜,用户的点赞记录等!!!将缓存数据异步更新到数据库或异步将数据库数据更新到缓存
更新爱豆点赞数:
我们可以去使用Redis中的ZSet通过她的权重系数(点赞量)进行爱豆的排名,每次用户点赞只需要去增加缓存中的这个权重系数即可,然后再将这个(点赞量定时更新到数据库)
更新爱豆排行榜:
因为爱豆的基本信息可能会存在修改,但是爱豆的点赞数是通过缓存去统计的,所以先点赞量更新到数据库,再将数据库的数据排序更新到缓存中!
查询全部爱豆排行数据,将数据放在Redis中的List类型通过我们的range方法和分页算法去取出数据!
更新用户的点赞记录
一.每个用户都会有自己的点赞票数,且每日刷新,对于点赞的这个事件,一般用户都是在一分钟只能完成所有的票数投递的!!所以在扣减用户的每日票数时,也可以使用缓存作为票数扣减而不必用户点赞10次就查询10次数据库!!
二.对于用户的每条点赞操作都会有所记录,当高峰期进行数据库插入操作是完成没有必要的,我们可以把数据放入缓存,作为一个中间件一样,对流量进行削峰,然后再批量,写入到数据库中!可以使用Redis的List作为一个双端队列,FIFO(先进先出)的形式批量的刷回到我们的数据库中! - 那么数据在更新的过程中,缓存失效了导致缓存击穿怎么办?要是直接打到我们数据库会导致db挂掉(采用缓存副本的方式,或者也可以进行限流,只有一个线程可以访问数据库并更新缓存使用JUC包下的Semaphore)
- 对于大批量的缓存操作,不单单只是我们存个简单的token这样的key,value那么简单,那么多数据怎么一次性或者批量写入到我们的缓存中,来节省类似于连接的这种不必要的耗时(使用Pipeline管道进行优化,来减少服务器连接中消耗的资源和时间,也就是减少)
Redis知识回顾Or补充
为了帮助大家回顾知识点,也为了帮助小白理解,就不涉及到具体的命令了!!!主要是数据结构和使用场景
List
- 单值多Value,有序集合,不唯一
- 它是一个字符串链表,left,right 都可以插入添加
- 如果键不存在,创建新的链表,如果值全移除,对应的键也就消失
- 使用Lists结构,我们可以轻松地实现最新消息排行等功能。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。
- Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部添加或者删除元素,这样List即可以作为栈,也可以作为队列
ZSet
- 与集合一样,排序集合由唯一的、不重复的字符串元素组成,类似于Set和Hash之间的混合
- 虽然集合内的元素不是有序的,但有序集合中的每个元素都与一个称为score的浮点值相关联,(这就是为什么该类型也类似于散列,因为每个元素都映射到一个值)。
- ZADD类似于SADD,但是有一个额外的参数(放置在要添加的元素之前),即分数。ZADD也是可变的,因此您可以自由指定多个得分-值对
- 排序集是通过一个双端口数据结构实现的,其中包含一个跳跃表和一个哈希表,所以每次我们添加一个元素,Redis执行O(log(N))操作。这很好,但当我们要求排序的元素时,Redis根本不需要做任何工作,它已经全部排序了
Pipeline
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。
这意味着通常情况下一个请求会遵循以下步骤:
客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
服务端处理命令,并将结果返回给客户端。
因此,例如下面是4个命令序列执行情况:
Client: INCR X
Server: 1
Client: INCR X
Server: 2
Client: INCR X
Server: 3
Client: INCR X
Server: 4
客户端和服务器通过网络进行连接。这个连接可以很快(loopback接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。
这个时间被称之为 RTT (Round Trip Time - 往返时间). 当客户端需要在一个批处理中执行多次请求时很容易看到这是如何影响性能的(例如添加许多元素到同一个list,或者用很多Keys填充数据库)。例如,如果RTT时间是250毫秒(在一个很慢的连接下),即使服务器每秒能处理100k的请求数,我们每秒最多也只能处理4个请求。
如果采用loopback接口,RTT就短得多(比如我的主机ping 127.0.0.1只需要44毫秒),但它任然是一笔很多的开销在一次批量写入操作中。幸运的是有一种方法可以改善这种情况。
Redis 管道(Pipelining)
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis很早就支持管道(pipelining)技术,因此无论你运行的是什么版本,你都可以使用管道(pipelining)操作Redis。下面是一个使用的例子:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
这一次我们没有为每个命令都花费了RTT开销,而是只用了一个命令的开销时间。
非常明确的,用管道顺序操作的第一个例子如下:
Client: INCR X
Client: INCR X
Client: INCR X
Client: INCR X
Server: 1
Server: 2
Server: 3
Server: 4
重要说明:
使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如10K的命令,读回复,然后再发送另一个10k的命令,等等。这样速度几乎是相同的,但是在回复这10k命令队列需要非常大量的内存用来组织返回数据内容。
管道(Pipelining) VS 脚本(Scripting)
大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。
应用程序有时可能在 pipeline 中发送 EVAL 或 EVALSHA 命令。Redis 通过 SCRIPT LOAD 命令(保证 EVALSHA 成功被调用)明确支持这种情况。
(ZSet,Lits)实现用户点赞功能
注:
这里有一个基于ip地址限速的注解:IpInterceptor,具体实现查看我这篇博文!
Java接口限速器—>注解与反射,枚举,AOP拦截器,异常处理中心,Redis实战
/**
* 用户点赞操作
* @param likeUserOpenId 点赞的爱豆id
* @param userSession 基于小程序用户的opendId生成第三方session令牌
* @return
*/
@IpInterceptor(requestCounts = 20,expiresTimeSecond = 60,isRestful = true,restfulParamCounts = 2)
@GetMapping("/userLikeDemo/{userSession}/{likeUserOpenId}")
public Result userLikeDemo(@PathVariable("likeUserOpenId") String likeUserOpenId,@PathVariable("userSession")String userSession) {
//1.获取用户当前的身份 通过3rdsession从redis中置换出当前报名用户的OpenId
String openId = (String)redisUtil.get(userSession);
if(SuperUtil.isNullOrEmpty(openId)){
return Result.handelLose("用户身份异常!!!",200);
}
//进行点赞操作
boolean clickFlag = userService.userLikeDemo(openId, likeUserOpenId);
//代表点赞成功
if(clickFlag){
return Result.handelSuccess("点赞成功");
//代表点赞失败
}else {
return Result.handelLose500("点赞失败,没有票数了");
}
}
//--------------------------------------------------------------------------------------------------------------
/**
* 用户点赞功能实现
* 1.通过userOpenId查看用户是否还有点赞的次数
* 2.如果有,给此likeUserOpenId用户点赞
* 3.并且生成点赞记录
* @param userOpenId
* @param likeUserOpenId
* @return true点赞成功 false点赞失败,没有票数
*/
@Override
public boolean userLikeDemo(String userOpenId, String likeUserOpenId) {
//初始参数准备
//记录标志 true减的是默认的票数 false减的是购买的票数
boolean recordFlag = true;
//是否有票数的标志位
boolean hasCountFlag=true;
//.先查询redis中 是否存在此用户的点赞次数
Integer defaultCount = (Integer) redisUtil.hget(USER_LIKE_COUNTS_PREFIX + userOpenId, "DEFAULT");//默认的点赞次数
Integer buyCount = (Integer) redisUtil.hget(USER_LIKE_COUNTS_PREFIX + userOpenId, "BUY"); //刷礼物的点赞次数
//如果缓存中没有数据
if (SuperUtil.isNullOrEmpty(defaultCount) || SuperUtil.isNullOrEmpty(buyCount)) {
//1.那么就根据用户编号,先查询数据库
User user = userMapper.selectOne(new QueryWrapper<User>().eq("user_open_id", userOpenId));
//2.判断用户 是否有默认的票数?使用默认票数:使用购买票数
if (user.getUserDefaultLikeCount() >= 1) {
//使用默认票数-1
user.setUserDefaultLikeCount(user.getUserDefaultLikeCount() - 1);
} else if (user.getUserBuyLikeCount() >= 1) {
//使用购买的票数-1
user.setUserBuyLikeCount(user.getUserBuyLikeCount() - 1);