Redis 快速提升系统性能的银弹

GitChat 做者:拿客_三产
原文:Redis 快速提升系统性能的银弹
关注微信公众号:GitChat 技术杂谈 ,一本正经的讲技术html

【不要错过文末彩蛋】java

前言

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

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

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

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

  1. 动静分离
  2. 负载均衡
  3. 分布式
  4. 集群化
  5. 缓存
  6. 限流处理
  7. 数据压缩
  8. 其余

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

  1. 配置管理变得复杂,所以须要设置配置中心来解决该问题。
  2. 同一个用户的请求会转发至不一样的 Web 服务器,从而致使 Session 丢失等问题。
  3. 同一个请求在分布式环境中须要不一样服务来提供不一样处理,从而须要分布式事务来确保数据的一致性。
  4. 分布式惟一 ID 问题。

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

  1. IP统计
  2. 用户登陆记录统计
  3. 实时的排行榜
  4. 原子计数
  5. 最新评论

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

配置中心可使用 Zookpeer、Redis 等实现。数据库

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

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

Redis 非银弹却无比接近

我这里为你们推荐的就是 Redis ,虽然它离真正意义的银弹仍是有些距离,可是他是为数很少的接近银弹的解决方案:

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

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

  1. 配置中心

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

  1. 分布式锁

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

  1. 缓存

​Redis 支持多种过时淘汰机制,自己性能的优点也使 Redis 在缓存方面获得普遍使用。

  1. Lua 脚本

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

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

  1. Redis 支持 BitMaps

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

使用 Redis 的 BitMaps 作为用户登陆记录统计,不只统计速度极快,并且内存占用极低。

  1. Redis 支持 HyperLogLog 算法

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

HyperLogLog 能够接受多个元素做为输入,并给出输入元素的基数估算值:

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

HyperLogLog 的优势是,即便输入元素的数量或者体积很是很是大,计算基数所需的空间老是固定的、而且是很小的。

在 Redis 里面,每一个 HyperLogLog 键只须要花费 12 KB 内存,就能够计算接近 2^64 个不一样元素的基数。这和计算基数时,元素越多耗费内存就越多的集合造成鲜明对比。使用 HyperLogLog 算法,咱们能够垂手可得的实现 IP 统计等对数据允许些许偏差的统计功能。

  1. Redis 支持 Geo 功能

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

  1. 简单消息队列

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

  1. 全文检索

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

  1. 分布式惟一ID

    Redis 的设计使其能够避免并发的多种问题,使其命令都是原子执行,这些特性都天生匹配分布式惟一ID生成器的要求。
    
    并且经过与 Lua 脚本的结合使用更是能生成复杂的有某些规律的惟一ID。

部分代码实现

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

Session 共享

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

实现方法:

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

Tomcat 配置步骤(相关代码资源能够从 https://gitee.com/coderknock/... 获取):

  1. 将 commons-pool2-2.4.2.jar、jedis-2.9.0.jar、commons-pool2-2.4.2.jar 三个 jar 包放到 Tomcat 下的 lib 目录下(注意:不是项目的 lib 目录)。
  2. 修改 Tomcat conf 下 context.xml:
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

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

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 命令演示(“#”以后为说明):

# 一、存储几个排行榜成员数据(这里能够理解为把本身系统已有数据加载到 Redis 中)
   ZADD testTop 23 member1 25 member2
   # 二、增长某我的的分数(这里的分数就是排行的依据能够是浮点类型)
   ZINCRBY  testTop 20 member1   # 此时 testTop 中 member1 的分数就编程了 43
   ZINCRBY  testTop -10 member2  # 此时 testTop 中 member2 的分数就编程了 15
   ZINCRBY  testTop 20 member3   # 此时向 testTop 中添加了 member3 成员,分数为 20
   # 三、查询排行榜前两名,而且查询出其分数【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 相关实现代码(模拟了 sf.gg 的名望榜)能够查看。

https://gitee.com/coderknock/... /src/test/java/TopDemo.java 有具体测试用例

Geo 相关功能

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

Java 相关实现代码能够查看:

https://gitee.com/coderknock/... /src/test/java/GeoDemo.java 有具体测试用例。

缓存

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

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

Java 相关实现代码能够查看:

https://gitee.com/coderknock/...

结束语

本文只是问了发散你们的思惟,如对具体功能实现由兴趣能够在以后的交流中共同探讨。

因为我的的局限性,文中可能存在错误表述,你们能够在评论区中提出共同探讨。

附录

Redis环境搭建

在线体验: http://try.redis.io/

Windows版本: https://github.com/MSOpenTech...

Linux安装: https://www.coderknock.com/bl...

Redis 配置

https://www.coderknock.com/bl...

Redis 支持的五大数据结构

enter image description here

Redis 基础知识扩展阅读

Redis 基础知识扩展阅读

Redis 发布订阅图解

 Redis 发布订阅图解


实录:《拿客_三产:解析 Redis 如何快速提升系统性能》


彩蛋

重磅 Chat 分享:

《高效学习,快速变现:不走弯路的五大学习策略》

分享人:
一名会在 B 站直播写代码,会玩杂耍球、弹 Ukulele、极限健身、跑步、写段子、画画、翻译、写做、演讲、培训的程序员。喜欢用编程实现本身的想法,在 Android 市场上赚过钱,有屡次创业经历。擅长学习,习惯养成,时间管理。身体力行地影响他人作出积极的改变!目前就任于 ThoughtWorks,致力于传播快乐高效的编程理念。业余创立软件匠艺社区 CodingStyle.cn,组织超过30场技术活动。

Chat简介:
说到学习呀,真是头大哟:碎片化,没有较长的连续时间来学习难专一,捧起书,手机却在召唤:来呀,快活呀~ 反正有,大把时光~作不到,看了不少书,生活中却作不到然并卵,学了方法和工具,找不到使用场景效率低,学习速度跟不上知识产生的速度记不牢,学习速度赶不上遗忘速度在这个知识泛滥、跨界竞争的年代,学习能力才是核心竞争力。你想一想,过去一周,有没有哪一件工做是不须要学习就能完成的?尽管如此重要,大部分人却没研究过学习这件事,觉得上下班路上打开「获得」听本书,就是碎片时间终身学习者了。

我是程序员,咨询师,培训师,这几个角色都要求我必须学得又快又好。本场 Chat 将分析学习的「趋势,原则,策略」,帮你站在更高的视角看待学习,从「内容,动机,交互,收益,资源」五方面制定策略,解决学习痛点,助你成为高效学习者!

想要免费参与本场 Chat ?很简单,「GitChat技术杂谈」公众号后台回复「高效学习」

这里写图片描述

相关文章
相关标签/搜索