分享一个简单的小需求应该怎么设计实现以及有关Redis的使用前端
Redis
在实际应用中使用的很是普遍,本篇文章就从一个简单的需求提及,为你讲述一个需求是如何从头至尾开始作的,又是如何一步步完善的。以前写过一篇《如何实现页面广告随时上下线、过时自动下线及到时自动上线》,也涉及到了Redis
在项目中的实际应用,有兴趣的能够看一下。程序员
设定,如今咱们有一个APP,产品新提出一个叫“程序员树洞”的功能,具体功能就不说了,其中这个功能有一点须要作的是在使用该功能时,若是是首次进入会展现一个协议页面,用户须要勾选后点肯定才能进入功能,此后再进该功能,再也不显示协议页直接进入该功能。以下图所示,
web
需求就是这么的简单,咱们来分析一下。
一、用户点击该功能时前端须要知道该给用户显示哪一个页面,这一步须要请求后端接口,后台告诉前端这个用户有没有赞成过协议。
二、用户勾选协议点肯定,后端须要记录这步操做(记录用户已经赞成协议),这一步需在点肯定时前端请求后端接口。redis
前面需求分析里说了,后端须要告诉前端用户有没有统一过协议,因此后端须要把这个信息记录下来,最好是记录到数据库保存,那就须要一张表来记录赞成过协议的用户。表结构大体是:id,客户号,插入时间。算法
一、记录客户是否已赞成过协议并提供查询功能(查询是否赞成过协议)
二、没有赞成过的和赞成过的用户信息怎么存储
三、如何高效的查询是否赞成过
四、怎么保证高并发下服务的可用性,数据库的可用性数据库
后端提供两个接口,
一、hasAgree(),查询该用户是否已赞成协议
二、recordAgree(),记录用户已赞成协议后端
很容易嘛!不就是CRUD吗,小意思。用户进来先查数据库有没有记录,没有返回用户没有赞成过协议,前端给用户展现协议页,不然展现功能页;用户点赞成后,后台记录用户已点了赞成协议,记录到库。一个查询一个插入,5分钟搞定嘛。
缓存
@ResponseBody@RequestMapping("/hasAgree")
public Map<String,Object> hasAgree(){
Map<String,Object> response = new HashMap<String,Object>();
String custNo = SessionUtil.getUserAttr("CUST_NO",String.class);//客户号
//query in DB
Map<String,Object> userAgreeInfo = aggreementService.queryUserAgreeInfo(custNo);
//0没有勾选过赞成协议 1已经勾选过赞成协议
response.put("hasAgree", MapUtils.isEmpty(userAgreeInfo)?"0":"1";);
return response;
}
@ResponseBody@RequestMapping("/recordAgree")
public Map<String,Object> recordAgree(){
Map<String,Object> response = new HashMap<String,Object>();
String custNo = SessionUtil.getUserAttr("CUST_NO",String.class);//客户号
Long result = aggreementService.recordUserAgreeInfo(custNo);//add into DB
if(1==result){
response.put("STATUS","1");
response.put("MSG","成功");
}else{
response.put("STATUS","0");
response.put("MSG","失败");
}
return response;
}
复制代码
初版代码如上,我以为刚入门的程序员都可以写出来。若是用户量不大,该功能的点击量不大的话,这么作仍是勉强说得过去。为何说勉强说得过去,由于存在隐患,你看啊若是每次点击都会去查库,假若有人恶意攻击,仿造高并发,瞬时大量请求过来都去查库,极可能数据库顶不住就挂了。或者就算数据库没挂,每次查库也都是浪费啊。因此这是个隐患,或者潜在的危险,那么第二版咱们就去解决这个问题。安全
考虑到每次查库很浪费,那咱们使用缓存好很差?
进来先查缓存有没有对应的数据,缓存里有就直接返回,没有则查库,库里有就存缓存。这样redis就分担了一部分数据库的压力。
数据结构
@ResponseBody@RequestMapping("/hasAgree")
public Map<String,Object> hasAgree(){
Map<String,Object> response = new HashMap<String,Object>();
String custNo = SessionUtil.getUserAttr("CUST_NO", String.class);//客户号
//SISMEMBER key member 判断 member 元素是不是集合 key 的成员
if(CacheUtil.isMemberRedisSet("HAS_AGREE_USERS",custNo)){
response.put("hasAgree","1"); //已经勾选过赞成协议
return response;
}
//query in DB
Map<String,Object> userAgreeInfo = aggreementService.queryUserAgreeInfo(custNo);
if(MapUtils.isEmpty(userAgreeInfo)){
response.put("hasAgree","0"); //没有勾选过赞成协议
}else{
//使用redis的set数据类型,追加到其中
CacheUtil.saddRedisSet("HAS_AGREE_USERS", custNo);
response.put("hasAgree","1"); //已经勾选过赞成协议
}
return response;
}
@ResponseBody@RequestMapping("/recordAgree")
public Map<String,Object> recordAgree(){
Map<String,Object> response = new HashMap<String,Object>();
String custNo = SessionUtil.getUserAttr("CUST_NO", String.class);//客户号
Long result = aggreementService.recordUserAgreeInfo(custNo);//add to DB
if(1==result){
//使用redis的set数据类型,追加到其中
CacheUtil.saddRedisSet("HAS_AGREE_USERS", custNo);
response.put("STATUS","1");
response.put("MSG","成功");
}else{
response.put("STATUS","0");
response.put("MSG","失败");
}
return response;
}
复制代码
这一版好一点了,部分请求分摊到redis了,减轻了数据库的压力。
随着客户量的增长,点击这个功能的次数、频率愈来愈高,假若有人频繁点击该功能,弹出协议后,退出,再点,再退出…就是不点肯定
这样的话后台缓存中没有,数据库中也没有,每次都会走数据库,绕过了缓存,直接都走数据库,这类请求量多了也是个问题,这就是缓存穿透。因此第三版,咱们来解决缓存穿透的问题。
@ResponseBody@RequestMapping("/hasAgree")
public Map<String,Object> hasAgree(){
Map<String,Object> response = new HashMap<String,Object>();
String hasAgree = "0";
String custNo = SessionUtil.getUserAttr("CUST_NO", String.class);//客户号
String value = CacheUtil.hgetRedisHash("HAS_AGREE_USERS", custNo, String.class);
if("1".equals(value)){
hasAgree = "1"; //已经勾选过赞成协议
}else if("0".equals(value)){
hasAgree = "0";
}else{
//query in DB
Map<String,Object> userAgreeInfo = aggreementService.queryUserAgreeInfo(custNo);
hasAgree = MapUtils.isEmpty(userAgreeInfo)?"0":"1";
//使用redis的hash数据类型
CacheUtil.hsetRedisHash("HAS_AGREE_USERS", custNo, hasAgree);
}
response.put("hasAgree",hasAgree);
return response;
}
@ResponseBody@RequestMapping("/recordAgree")
public Map<String,Object> recordAgree(){
Map<String,Object> response = new HashMap<String,Object>();
String custNo = SessionUtil.getUserAttr("CUST_NO", String.class);//客户号
Long result = 0;
String hasAgree = "0";
try{
result = aggreementService.recordUserAgreeInfo(custNo);//add to DB
}catch(Exception e){
logger.error("recordUserAgreeInfo error:{}",e)
}
if(1==result){
hasAgree = "1";//入库成功
response.put("STATUS","1");
response.put("MSG","成功");
}else{
response.put("STATUS","0");
response.put("MSG","失败");
}
CacheUtil.hsetRedisHash("HAS_AGREE_USERS", custNo, hasAgree);//update or del
return response;
}
复制代码
能够看到,咱们的这个key-field-value没有设置过时时间,由于能够认为这个key是一个热点key,对于热点key咱们的处理方式是,永久有效或过时时间尽可能长一点。
另外一个关于缓存的问题,那就是缓存击穿。
何为缓存击穿?
假如该功能在前期宣传力度比较大,或预计该功能上线后点击量比较大的话,那么在功能上线后极可能就会一瞬间大量用户来点击这个功能,由于咱们前面的逻辑是首次进入该功能的用户展现协议页,咱们的后台处理虽然加了redis缓存,可是新上的功能全部用户都没有点过,那么redis里就没有缓存,是否是全部用户的请求都落到数据库了?一旦瞬间流量很是大,数据库安全性就存在隐患,有被搞垮的可能。
因此怎么解决呢?咱们能够在该功能上线前,提早将须要作缓存的数据放入redis,即缓存预热。
如何预热?
将全部用户的信息都放到redis.举个栗子(也许不是最佳的),咱们使用Redis的hash数据结构,key-field-value。key咱们能够固定一个字符串如coderTreeHole_Agreement_Check,field咱们能够用客户号(惟一),value是个标志位,用0表明没赞成过协议,1表明赞成过。通常在电商大促前都会对热点key进行预热,否则真的扛不住。
redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不一样的内容。在redis的每个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,能够理解为是一个集群管理的插件。当咱们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,而后把结果对16384求余数,这样每一个key都会对应一个编号在0-16383之间的哈希槽,经过这个值,去找到对应的插槽所对应的节点,而后直接自动跳转到这个对应的节点上进行存取操做。
看了上面这段话,明白了吧。那对于这个大key并且是热点key的请求,是否是都落到某一个redis节点上了?大key会带来不少问题,篇幅缘由之后再来细说,跑题了。。。
针对这个需求,你还有什么方法防治缓存击穿?
能够看到咱们上面的设计其实都是实时对数据库进行操做的。
例如,当用户点了赞成,前端就调后台的recordAgree方法将该记录记录到数据库,即这条记录是立马插入到数据库的。
若是刚上线这个功能,大量用户同时点这个功能,并发量大的话,请求走到后台,那么写库的操做就很是多,数据库链接数忽然激增,数据库会顶不住吧。
因此为避免流量集中落到数据库,此时咱们可使用消息队列MQ。将插入操做的请求发往消息队列,使插入操做以必定的速率到数据库执行,使得对数据库的请求数尽可能平滑,消息发给消息队列当即返回给前端成功,不用等待插库完成,用MQ实现了异步解耦,削峰填谷。
对于这个需求设计到哪一种程度取决于你的用户量和并发量,若是是像双十一那样,确定是要用消息队列的,那通常小的例如,用户量1千万,日活10万,请求最集中的也就是中午9-12点,下午13-17点吧,差很少8个小时,平均一个小时1.25万,用户都来点这个功能的话,每分钟208,每秒3.5,算不上高并发,数据库彻底扛得住。
总结一下,这个需求咱们用到的知识点(敲黑板),redis数据缓存
,redis缓存穿透
,缓存击穿
,热点key问题
,redis大key问题(没具体讲)
,消息队列异步解耦
等。
画图码字不易,若是以为我写的还能够,记得点赞
鼓励一下哦,若是以为有问题欢迎指正
。
886