GitChat 做者:拿客_三产
原文:Redis 快速提升系统性能的银弹
关注微信公众号:GitChat 技术杂谈 ,一本正经的讲技术html
【不要错过文末彩蛋】java
说明:阅读该文章须要必定 Web 开发经验,最好对 Redis 有一个基本的认知,文章最后的附录也会为你们提供一些相关的文章,本文章只是为了让那些对 Redis 的应用仅仅局限于 缓存 的开发人员了解到 Redis 更多可能的应用场景,因为篇幅限制,文中不少场景只是阐述了实现的思想及部分原理,仅仅提供了部分功能的具体实现。git
现代系统随着功能的复杂化,各类各样需求层出不穷,面对越发复杂话的业务系统、愈来愈庞大的用户群体,以及用户对体验的要求愈来愈高,性能就变得更加剧要。程序员
抛开代码逻辑、服务器性能的相关问题外,提升性能的方式有如下几种:github
咱们来分析一下负载均衡、分布式、集群化涉及的问题:web
另外针对不一样部分系统中的一些特定问题又有其余的一些特殊业务需求:redis
诚然,以上各类问题都有花样繁多的解决方法,例如:算法
配置中心可使用 Zookpeer、Redis 等实现。数据库
Session 丢失可使用 Session 同步、客户端 token、Session 共享等解决,其中 Session 共享又能够细分不一样实现方式。apache
面对层出不穷的概念,以及各类新兴的技术,咱们每每会显得力不从心,那么有没有一个银弹能够解决这些问题呢?
我这里为你们推荐的就是 Redis ,虽然它离真正意义的银弹仍是有些距离,可是他是为数很少的接近银弹的解决方案:
接下来咱们就来讲说怎么使用 Redis 解决以前提到的问题:
Redis 自己就是内存 K/V 数据库,支持 哈希、集合、列表等五种数据结构,从而配置信息的存储、读取速度都可以获得知足,Redis 还提供订阅/发布功能从而能够在配置发生改变时通知不一样服务器来进行更新相关配置。
使用 Redis 的 SETNX 命令或者 SET 命令配合 NX 选项的方式以及过时时间等功能能够很方便的实现一个性能优越的分布式锁。
Redis 支持多种过时淘汰机制,自己性能的优点也使 Redis 在缓存方面获得普遍使用。
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并开放源代码。Redis 支持 Lua 脚本的运行,从而能够扩展 Redis 中的命令实现不少复杂功能。
Redis 支持使用 Lua 脚原本实现一些组合命令逻辑处理,从而可使用 Redis 作为限流、分布式惟一 ID 相关技术的实现。
位图(bitmap)是一种很是经常使用的结构,在索引,数据压缩等方面有普遍应用,能同时保证存储空间和速度最优化(而没必要空间换时间)。
使用 Redis 的 BitMaps 作为用户登陆记录统计,不只统计速度极快,并且内存占用极低。
Redis HyperLogLog是一种使用随机化的算法,以少许内存提供集合中惟一元素数量的近似值。
HyperLogLog 能够接受多个元素做为输入,并给出输入元素的基数估算值:
HyperLogLog 的优势是,即便输入元素的数量或者体积很是很是大,计算基数所需的空间老是固定的、而且是很小的。
在 Redis 里面,每一个 HyperLogLog 键只须要花费 12 KB 内存,就能够计算接近 2^64 个不一样元素的基数。这和计算基数时,元素越多耗费内存就越多的集合造成鲜明对比。使用 HyperLogLog 算法,咱们能够垂手可得的实现 IP 统计等对数据允许些许偏差的统计功能。
咱们可使用基于 Redis 来实现地理位置相关管理,附近的人、两地理位置间距离计算等功能变得极为容易实现。
Redis 列表 + 发布/订阅功能能够很方便的实现一个简单的消息队列,将消息存入 Redis 列表中,经过 发布/订阅功能通知指定成员,成员获取到通知后能够根据通知内容进行对应处理。
Redis 官方团队开发了 RediSearch 模块,能够实现使用 Redis 来作全文检索的功能。
分布式惟一ID
Redis 的设计使其能够避免并发的多种问题,使其命令都是原子执行,这些特性都天生匹配分布式惟一ID生成器的要求。 并且经过与 Lua 脚本的结合使用更是能生成复杂的有某些规律的惟一ID。
下面咱们以 Java代码做为演示(编程语言实现方式原理相似只是具体实现方式有些许差异而已)讲解几个功能的实现:
原理:将不一样 Web 服务器的 Session 信息统一存储在 Redis 中,而且获取 Session 也是从 Redis 中获取
实现方法:
方法一:基于 Tomcat 实现 Sessioin 共享:
Tomcat 配置步骤(相关代码资源能够从 https://gitee.com/coderknock/... 获取):
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 有具体测试用例
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/...
本文只是问了发散你们的思惟,如对具体功能实现由兴趣能够在以后的交流中共同探讨。
因为我的的局限性,文中可能存在错误表述,你们能够在评论区中提出共同探讨。
在线体验: http://try.redis.io/
Windows版本: https://github.com/MSOpenTech...
Linux安装: https://www.coderknock.com/bl...
https://www.coderknock.com/bl...
实录:《拿客_三产:解析 Redis 如何快速提升系统性能》
重磅 Chat 分享:
《高效学习,快速变现:不走弯路的五大学习策略》
分享人:
一名会在 B 站直播写代码,会玩杂耍球、弹 Ukulele、极限健身、跑步、写段子、画画、翻译、写做、演讲、培训的程序员。喜欢用编程实现本身的想法,在 Android 市场上赚过钱,有屡次创业经历。擅长学习,习惯养成,时间管理。身体力行地影响他人作出积极的改变!目前就任于 ThoughtWorks,致力于传播快乐高效的编程理念。业余创立软件匠艺社区 CodingStyle.cn,组织超过30场技术活动。Chat简介:
说到学习呀,真是头大哟:碎片化,没有较长的连续时间来学习难专一,捧起书,手机却在召唤:来呀,快活呀~ 反正有,大把时光~作不到,看了不少书,生活中却作不到然并卵,学了方法和工具,找不到使用场景效率低,学习速度跟不上知识产生的速度记不牢,学习速度赶不上遗忘速度在这个知识泛滥、跨界竞争的年代,学习能力才是核心竞争力。你想一想,过去一周,有没有哪一件工做是不须要学习就能完成的?尽管如此重要,大部分人却没研究过学习这件事,觉得上下班路上打开「获得」听本书,就是碎片时间终身学习者了。我是程序员,咨询师,培训师,这几个角色都要求我必须学得又快又好。本场 Chat 将分析学习的「趋势,原则,策略」,帮你站在更高的视角看待学习,从「内容,动机,交互,收益,资源」五方面制定策略,解决学习痛点,助你成为高效学习者!
想要免费参与本场 Chat ?很简单,「GitChat技术杂谈」公众号后台回复「高效学习」