1. 首页
  2. 文章列表
  3. 补上博文一篇:由鹿晗公布恋情而引发的一次人生思考🤔

人生处处是惊喜。国庆假期最后一天,当红流量小生鹿晗于正午 12 点,在微博公布恋情。

懒得勤快的博客_全栈开发者_互联网分享精神

微博一经发布后,瞬间刷爆朋友圈、各大网站头条,还导致了微博一时间瘫痪。微博客服也很无奈表示:具体怎么造成的,大家心里也都有数。

懒得勤快的博客_全栈开发者_互联网分享精神

然整个事件当中,除了无数粉碎的少女心,最无辜躺枪还如此敬业的小哥哥,非这位微博搜索工程师丁振凯莫属啦,结婚当日,遇鹿晗公布恋情,不得不从酒席上离开处理微博异常后继续婚礼,心疼小哥哥三秒钟......

懒得勤快的博客_全栈开发者_互联网分享精神

回顾一下,究竟是多大的流量使得曾豪言“微博服务器稳定,能同时应付三对劈腿的”壮志秒破功,具体数据如下图所示:

懒得勤快的博客_全栈开发者_互联网分享精神

按照微博明星势力榜各个榜单计分方式:满分100分,由阅读数、互动数、社会影响力、爱慕值四项组成,所占比例分别为30%、30%、20%、20%。

由上可以看出,鹿晗所发微博的每一项到达到了峰值,那么在如此高流量的情况之下,作为开发者是否有好的方法来快速提高系统性能呢?

接下来,来自 CSDN 博客专家的“拿客_三产”,拿客站长,与我们分享目前最为炙手可热的 Key-Value 数据库、常用做缓存、Session 共享中间件、分布式锁等的——Redis 技术。

说明:阅读本文章需要一定 Web 开发经验,最好对 Redis 有一个基本的认知,文章最后的附录也会为大家提供一些相关的文章,本文章只是为了让那些对 Redis 的应用仅仅局限于 缓存 的开发人员了解到 Redis 更多可能的应用场景,由于篇幅限制,文中很多场景只是阐述了实现的思想及部分原理,仅仅提供了部分功能的具体实现。

现代高并发复杂系统面临的挑战

现代系统随着功能的复杂化,各种各样需求层出不穷,面对愈加复杂话的业务系统、越来越庞大的用户群体,以及用户对体验的要求越来越高,性能就变得更加重要。

抛开代码逻辑、服务器性能的相关问题外,提高性能的方式有以下几种:

动静分离
负载均衡
分布式
集群化
缓存
限流处理
数据压缩
其他

我们来分析一下负载均衡、分布式、集群化涉及的问题:

配置管理变得复杂,因此需要设置配置中心来解决该问题。
同一个用户的请求会转发至不同的 Web 服务器,从而导致 Session 丢失等问题。
同一个请求在分布式环境中需要不同服务来提供不同处理,从而需要分布式事务来确保数据的一致性。
分布式唯一 ID 问题。

另外针对不同部分系统中的一些特定问题又有其他的一些特殊业务需求:

IP统计
用户登录记录统计
实时的排行榜
原子计数
最新评论

诚然,以上各种问题都有花样繁多的解决方法,例如:

配置中心可以使用 Zookpeer、Redis 等实现。

Session 丢失可以使用 Session 同步、客户端 token、Session 共享等解决,其中 Session 共享又可以细分不同实现方式。

面对层出不穷的概念,以及各种新兴的技术,我们往往会显得力不从心,那么有没有一个银弹可以解决这些问题呢?

Redis 非银弹却无比接近

我这里为大家推荐的就是 Redis ,虽然它离真正意义的银弹还是有些距离,但是他是为数不多的接近银弹的解决方案:

Redis 使用 C 开发,是一款内存 K/V 数据库,架构设计极简,性能卓著。
Redis 采用 单线程 多路复用的设计,避免了并发带来的锁性能损耗等问题。
Redis 安装、测试、配置、运维较其他产品更为容易。
Redis 是目前为止最受欢迎的 K/V 数据库,支持持久化,value 支持多种数据结构。
Redis 命令语法简单,极易掌握。
Redis 提供了一种通用的协议,使得各种编程语言都能很方便的开发出与其交互的客户端。
Redis 开放源码,我们可以对其进行二次开发来定制优化。
Redis 目前有较好的社区维护,版本迭代有所保障,新的功能也在有条不紊的添加完善。
Redis 有较好的主从复制、集群相关支持。

最新版本提供模块化功能,可以方便的扩展功能。

接下来我们就来说说怎么使用 Redis 解决之前提到的问题:

1. 配置中心

Redis 本身就是内存 K/V 数据库,支持 哈希、集合、列表等五种数据结构,从而配置信息的存储、读取速度都能够得到满足,Redis 还提供订阅/发布功能从而可以在配置发生改变时通知不同服务器来进行更新相关配置。

2. 分布式锁

使用 Redis 的 SETNX 命令或者 SET 命令配合 NX 选项的方式以及过期时间等功能可以很方便的实现一个性能优越的分布式锁。

3. 缓存

Redis 支持多种过期淘汰机制,本身性能的优势也使 Redis 在缓存方面得到广泛使用。

4. Lua 脚本

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并开放源代码。Redis 支持 Lua 脚本的运行,从而可以扩展 Redis 中的命令实现很多复杂功能。

Redis 支持使用 Lua 脚本来实现一些组合命令逻辑处理,从而可以使用 Redis 做为限流、分布式唯一 ID 相关技术的实现。

5. Redis 支持 BitMaps

位图(bitmap)是一种非常常用的结构,在索引,数据压缩等方面有广泛应用,能同时保证存储空间和速度最优化(而不必空间换时间)。

使用 Redis 的 BitMaps 做为用户登录记录统计,不仅统计速度极快,而且内存占用极低。

6. Redis 支持 HyperLogLog 算法

Redis HyperLogLog是一种使用随机化的算法,以少量内存提供集合中唯一元素数量的近似值。

HyperLogLog 可以接受多个元素作为输入,并给出输入元素的基数估算值:

HyperLogLog 的优点是,即使输入元素的数量或者体积非常非常大,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。使用 HyperLogLog 算法,我们可以轻而易举的实现 IP 统计等对数据容许些许误差的统计功能。

基数:集合中不同元素的数量。比如 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基数就是3。
估算值:算法给出的基数并不是精确的,可能会比实际稍微多一些或者稍微少一些,但会控制在合理的范围之内。

7. Redis 支持 Geo 功能

我们可以使用基于 Redis 来实现地理位置相关管理,附近的人、两地理位置间距离计算等功能变得极为容易实现。

8. 简单消息队列

Redis 列表 + 发布/订阅功能可以很方便的实现一个简单的消息队列,将消息存入 Redis 列表中,通过 发布/订阅功能通知指定成员,成员获取到通知后可以根据通知内容进行对应处理。

9. 全文检索

Redis 官方团队开发了 RediSearch 模块,可以实现使用 Redis 来做全文检索的功能。

10. 分布式唯一ID

Redis 的设计使其可以避免并发的多种问题,使其命令都是原子执行,这些特性都天生匹配分布式唯一ID生成器的要求。

而且通过与 Lua 脚本的结合使用更是能生成复杂的有某些规律的唯一ID。

部分代码实现

下面我们以 Java代码作为演示(编程语言实现方式原理类似只是具体实现方式有些许差别而已)讲解几个功能的实现:

Session 共享

原理:将不同 Web 服务器的 Session 信息统一存储在 Redis 中,并且获取 Session 也是从 Redis 中获取

实现方法:

方法一:基于 Tomcat 实现 Sessioin 共享:

Tomcat 配置步骤(相关代码资源可以从https://gitee.com/coderknock/Tomcat-Redis-Session-Manager-Demo 获取):

将 commons-pool2-2.4.2.jar、jedis-2.9.0.jar、commons-pool2-2.4.2.jar 三个 jar 包放到 Tomcat 下的 lib 目录下(注意:不是项目的 lib 目录)。

修改 Tomcat conf 下 context.xml:

   <Context>
       ......
       <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />  
       <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"  
           host="127.0.0.1"  
           port="6379"  
           database="0"  
           maxInactiveInterval="60"
           password="admin123" />
         ......
   </Context>

方法二:基于 Fileter 、 自行实现 HttpServletRequestWrapper 、 HttpSession :

关键代码:

HttpSessionWrapper.java

   import com.alibaba.fastjson.JSON;
   import com.alibaba.fastjson.JSONException;
   import com.coderknock.jedis.executor.JedisExecutor;
   import com.coderknock.pojo.User;
   import org.apache.commons.lang3.StringUtils;
   import org.apache.logging.log4j.LogManager;
   import org.apache.logging.log4j.Logger;
   import javax.servlet.ServletContext;
   import javax.servlet.http.HttpServletRequest;
   import javax.servlet.http.HttpServletResponse;
   import javax.servlet.http.HttpSession;
   import javax.servlet.http.HttpSessionContext;
   import java.util.Enumeration;
   /**
    * <p></p>
    *
    * @author 三产
    * @version 1.0
    * @date 2017-08-26
    * @QQGroup 213732117
    * @website http://www.coderknock.com
    * @copyright Copyright 2017 拿客 coderknock.com  All rights reserved.
    * @since JDK 1.8
    */
   public class HttpSessionWrapper implements HttpSession {
       protected final Logger logger = LogManager.getLogger(HttpSessionWrapper.class);
       private String sid = "";
       private HttpServletRequest request;
       private HttpServletResponse response;
       private final long creationTime = System.currentTimeMillis();
       private final long lastAccessedTime = System.currentTimeMillis();
       //过期时间单位秒
       private int expire_time = 60;
      public HttpSessionWrapper() {
       }
      public HttpSessionWrapper(String sid, HttpServletRequest request,
                                 HttpServletResponse response) {
           this.sid = sid;
           this.request = request;
           this.response = response;
       }
      public Object getAttribute(String name) {
           logger.info(getClass() + "getAttribute(),name:" + name);
           try {
               Object obj = JedisExecutor.execute(jedis -> {
                   String jsonStr = jedis.get(sid + ":" + name);
                   if (jsonStr != null || StringUtils.isNotEmpty(jsonStr)) {
                       jedis.expire(sid + ":" + name, expire_time);// 重置过期时间
                   }
                   return jsonStr;
               });
               return obj;
           } catch (JSONException je) {
               logger.error(je);
           } catch (Exception e) {
               logger.error(e.getMessage());
           }
           return null;
       }
       public void setAttribute(String name, Object value) {
           logger.info(getClass() + "setAttribute(),name:" + name);
           try {
               JedisExecutor.executeNR(jedis -> {
                   if (value instanceof String) {
                       String value_ = (String) value;
                       jedis.set(sid + ":" + name, value_);//普通字符串对象
                   } else {
                       jedis.set(sid + ":" + name, JSON.toJSONString(value));//序列化对象
                   }
                  jedis.expire(sid + ":" + name, expire_time);// 重置过期时间
               });
           } catch (Exception e) {
               logger.error(e);
           }
      }
       public void removeAttribute(String name) {
           logger.info(getClass() + "removeAttribute(),name:" + name);
           if (StringUtils.isNotEmpty(name)) {
               try {
                   JedisExecutor.executeNR(jedis -> {
                       jedis.del(sid + ":" + name);
                   });
               } catch (Exception e) {
                   logger.error(e);
               }
           }
       }
       //...... 省略部分代码
   }

SessionFilter.java

   import com.coderknock.wrapper.DefinedHttpServletRequestWrapper;
   import org.apache.commons.lang3.StringUtils;
   import org.apache.logging.log4j.LogManager;
   import org.apache.logging.log4j.Logger;
  import javax.servlet.*;
   import javax.servlet.http.Cookie;
   import javax.servlet.http.HttpServletRequest;
   import javax.servlet.http.HttpServletResponse;
   import java.io.IOException;
   import java.util.UUID;
   /**
    * <p></p>
    *
    * @author 三产
    * @version 1.0
    * @date 2017-08-26
    * @QQGroup 213732117
    * @website http://www.coderknock.com
    * @copyright Copyright 2017 拿客 coderknock.com  All rights reserved.
    * @since JDK 1.8
    */
   public class SessionFilter implements Filter {
       protected final Logger logger = LogManager.getLogger(getClass());
       private static final String host = "host";
       private static final String port = "port";
       private static final String seconds = "seconds";
       public void init(FilterConfig filterConfig) throws ServletException {
           logger.debug("init filterConfig info");
       }
       public void doFilter(ServletRequest request, ServletResponse response,
                            FilterChain chain) throws IOException, ServletException {
           //从cookie中获取sessionId,如果此次请求没有sessionId,重写为这次请求设置一个sessionId
           HttpServletRequest httpRequest = (HttpServletRequest) request;
           HttpServletResponse httpResponse = (HttpServletResponse) response;
           String sid = null;
           if (httpRequest.getCookies() != null) {
               for (Cookie cookie : httpRequest.getCookies()) {
                   if (cookie.getName().equals("JSESSIONID")) {
                       sid = cookie.getValue();
                       break;
                   }
               }
           }
           if (StringUtils.isEmpty(sid)) {
               try {
                   Cookie cookie = new Cookie("JSESSIONID", httpRequest.getLocalAddr() + ":" + request.getLocalPort() + ":" + UUID.randomUUID().toString().replaceAll("-", ""));
                   httpResponse.addCookie(cookie);
               } catch (Exception e) {
                   e.printStackTrace();
               }
           }
           logger.info("JSESSIONID:" + sid);
           chain.doFilter(new DefinedHttpServletRequestWrapper(sid, httpRequest, httpResponse), response);
       }
       public void destroy() {
       }
   }

排行榜:

原理:通过 Redis 有序集合可以很便捷的实现该功能

关键命令:

ZADD key [NX|XX][CH][INCR] score member [score member ...]: 初始化排行榜中成员及其分数。
ZINCRBY key increment member:为某个成员增加分数,如果该成员不存在则会添加该成员并设定分数为 increment 。
ZUNIONSTORE destination numkeys key [key ...][WEIGHTS weight [weight ...]][AGGREGATE SUM|MIN|MAX]: 可以合并多个排行榜,该操作会将几个集合的并集存储到 destination 中,其中各个集合相同成员分数会叠加或者取最大、最小、平均值等(根据 [AGGREGATE SUM|MIN|MAX] 参数决定,默认是叠加),从而可以实现根据多个分排行榜来计算总榜排行的功能。
ZREVRANGE key start stop [WITHSCORES]:该命令就是最关键的获取排行信息的命令,可以获取从高到低的成员。

Redis 命令演示(“#”之后为说明):

   # 1、存储几个排行榜成员数据(这里可以理解为把自己系统已有数据加载到 Redis 中)
   ZADD testTop 23 member1 25 member2
   # 2、增加某个人的分数(这里的分数就是排行的依据可以是浮点类型)
   ZINCRBY  testTop 20 member1   # 此时 testTop 中 member1 的分数就编程了 43
   ZINCRBY  testTop -10 member2  # 此时 testTop 中 member2 的分数就编程了 15
   ZINCRBY  testTop 20 member3   # 此时向 testTop 中添加了 member3 成员,分数为 20
   # 3、查询排行榜前两名,并且查询出其分数【WITHSCORES 选项用于显示分数,不带该参数则只会查出成员名称】
   ZREVRANGE testTop 0 1 WITHSCORES
   #结果:
   # 1) "member1"
   # 2) "43"
   # 3) "member3"
   # 4) "20"
   # 假设此时还有一个 排行榜
   ZADD testTop2  100 member2 200 member3 123 member4
   # 将 testTop testTop2 合成一个总榜 top
   ZUNIONSTORE  top 2 testTop testTop2
   # 查询总榜所有成员排行情况
   ZREVRANGE top 0 -1 WITHSCORES
   1) "member3"
   2) "220"
   3) "member4"
   4) "123"
   5) "member2"
   6) "115"
   7) "member1"
   8) "43"

Java 相关实现代码可以查看。

https://gitee.com/coderknock/Redis-Top-And-Around

/src/test/java/TopDemo.java 有具体测试用例

Geo 相关功能

Redis 的 Geo 功能提供了查询两个成员距离、某个成员附近范围成员等功能可以用其实现一个简单的附近的人

Java 相关实现代码可以查看:

https://gitee.com/coderknock/Redis-Top-And-Around

/src/test/java/GeoDemo.java 有具体测试用例。

缓存

原理:将经常会访问的数据根据一定规则设置一个 Key 后存入 Redis,每次查询时先查询 Redis 中是否包含匹配数据,如果缓存不存在再查询数据库。

注意点:对于不存在的数据应该存入一个自己设定的空值并设置过期时间,这样可以避免缓存击穿(由于数据不存在,所以设置 Key 对应的值为 null(Java中的表示形式),因为 Redis 会移除值为 null 的 key 这样会导致,每次查询还是会访问数据库)。

Java 相关实现代码可以查看:

https://gitee.com/coderknock/Redis-Cache

结束语

本文只是问了发散大家的思维,如对具体功能实现由兴趣可以在之后的交流中共同探讨。

由于个人的局限性,文中可能存在错误表述,大家可以在评论区中提出共同探讨。


文章历史版本:

修改次数:1 次 查看历史版本

版权声明:

本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:https://masuit.com/101

l  博主在此发文(包括但不限于汉字、拼音、拉丁字母)均为随意敲击键盘所出,用于检验本人电脑键盘录入、屏幕显示的机械、光电性能,并不代表本人局部或全部同意、支持或者反对观点。如需要详查请直接与键盘生产厂商法人代表联系。挖井挑水无水表,不会网购无快递。

l  文章内容部分来源于互联网,不代表本人的任何立场;涉及到的软件来源于互联网,仅供个人下载使用,请勿用于商业用途,版权归软件开发者所有,下载后请于24小时内删除,请支持正版!因下载本站任何资源造成的损失,全部责任由使用者本人承担!如果你是版权方,认为本文内容对您的权益有所侵犯,请联系博主,待博主进行严格地审查和背景调查后,情况属实的将在三天内将本文删除或修正。

l  博主的文章没有高度、深度和广度,只是凑字数。由于博主的水平不高(其实是个菜B),不足和错误之处在所难免,希望大家能够批评指出。

l  博主是利用读书、参考、引用、抄袭、复制和粘贴等多种方式打造成自己的纯镀 24k 文章,请原谅博主成为一个无耻的文档搬运工!

l  博主只是一名普通的互联网从业者,不懂修电脑,不会卖电脑,不会帮你盗号,不会破解开机密码,找不回你丢失的手机等,如有这样的想法请绕道!

相关推荐:

所有编程语言为我作证,随机数"骗局"大揭秘,随机数都是骗人的! 再谈http请求的GET和POST的区别,以及为什么网上的多数答案都是错的。
互联网架构之浅谈数据库用户表结构设计与第三方登录接入 从.NET+25台服务器支撑世界第54大网站浅谈网站优化
asp.net即时通信框架——signalR起步 你写的Try...Catch真的有必要么?

评论区:

    还没有评论哦,赶紧来写评论吧

    分享按钮