Redis:快速提高系统性能的银弹
来源:原创 时间:2017-11-20 浏览:0 次现代高并发杂乱体系面临的应战
现代体系跟着功用的杂乱化,各式各样需求层出不穷,面临愈加杂乱话的事务体系、越来越巨大的用户集体,以及用户对体会的要求越来越高,功用就变得愈加重要。
抛开代码逻辑、效劳器功用的相关问题外,进步功用的办法有以下几种:
动态别离
负载均衡
分布式
集群化
缓存
限流处理
数据压缩
其他
我们来剖析一下负载均衡、分布式、集群化触及的问题:
装备办理变得杂乱,因而需求设置装备中心来处理该问题。
同一个用户的恳求会转发至不同的 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 处理之前说到的问题:
装备中心
Redis 自身就是内存 K/V 数据库,支撑 哈希、调集、列表等五种数据结构,然后装备信息的存储、读取速度都能够得到满意,Redis 还供给订阅/发布功用然后能够在装备发作改动时告诉不同效劳器来进行更新相关装备。
分布式锁
运用 Redis 的 SETNX 指令或许 SET 指令合作 NX 选项的办法以及过期时刻等功用能够很便利的完成一个功用优越的分布式锁。
缓存
Redis 支撑多种过期筛选机制,自身功用的优势也使 Redis 在缓存方面得到广泛运用。
Lua 脚本
Lua 是一种轻量细巧的脚本言语,用规范C言语编写并开放源代码。Redis 支撑 Lua 脚本的运转,然后能够扩展 Redis 中的指令完成许多杂乱功用。
Redis 支撑运用 Lua 脚正本完成一些组合指令逻辑处理,然后能够运用 Redis 做为限流、分布式仅有 ID 相关技能的完成。
Redis 支撑 BitMaps
位图(bitmap)是一种十分常用的结构,在索引,数据压缩等方面有广泛应用,能一起确保存储空间和速度最优化(而不用空间换时刻)。
运用 Redis 的 BitMaps 做为用户登录记载核算,不只核算速度极快,而且内存占用极低。
Redis 支撑 HyperLogLog 算法
Redis HyperLogLog是一种运用随机化的算法,以少数内存供给调集中仅有元素数量的近似值。
HyperLogLog 能够承受多个元素作为输入,并给出输入元素的基数预算值:
HyperLogLog 的长处是,即便输入元素的数量或许体积十分十分大,核算基数所需的空间总是固定的、而且是很小的。
在 Redis 里边,每个 HyperLogLog 键只需求花费 12 KB 内存,就能够核算挨近 2^64 个不同元素的基数。这和核算基数时,元素越多消耗内存就越多的调集构成鲜明对比。运用 HyperLogLog 算法,我们能够垂手可得的完成 IP 核算等对数据容许少许差错的核算功用。
基数:调集中不同元素的数量。比方 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基数就是3。
预算值:算法给出的基数并不是准确的,可能会比实践略微多一些或许略微少一些,但会控制在合理的规模之内。
Redis 支撑 Geo 功用
我们能够运用根据 Redis 来完成地理位置相关办理,邻近的人、两地理位置间间隔核算等功用变得极为简略完成。
简略音讯行列
Redis 列表 + 发布/订阅功用能够很便利的完成一个简略的音讯行列,将音讯存入 Redis 列表中,经过 发布/订阅功用告诉指定成员,成员获取到告诉后能够根据告诉内容进行对应处理。
全文检索
Redis 官方团队开发了 RediSearch 模块,能够完成运用 Redis 来做全文检索的功用。
分布式仅有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:
XML
......
port="6379"
database="0"
maxInactiveInterval="60"
password="admin123" />
......
办法二:根据 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;
/**
*
*
* @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;
/**
*
*
* @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"