原文:http://www.myexception.cn/software-architecture-design/1815507.html

Shiro通过Redis管理会话实现集群

写在前面

1.在上一篇帖子 Shiro一些补充 中提到过Shiro可以使用Shiro自己的Session或者自定义的Session来代替HttpSession

2.Redis/Jedis参考我写的 http://sgq0085.iteye.com/category/317384 一系列内容

一. SessionDao

配置在sessionManager中,可选项,如果不修改默认使用MemorySessionDAO,即在本机内存中操作。

如果想通过Redis管理Session,从这里入手。只需要实现类似DAO接口的CRUD即可。

经过1:最开始通过继承AbstractSessionDAO实现,发现doReadSession方法调用过于频繁,所以改为通过集成CachingSessionDAO来实现。

注意,本地缓存通过EhCache实现,失效时间一定要远小于Redis失效时间,这样本地失效后,会访问Redis读取,并重新设置Redis上会话数据的过期时间。

因为Jedis API KEY和Value相同,同为String或同为byte[]为了方便扩展下面的方法

package com.gqshao.authentication.utils;

import com.google.common.collect.Lists;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session; import java.io.Serializable;
import java.util.Collection;
import java.util.List; public class SerializeUtils extends SerializationUtils { public static String serializeToString(Serializable obj) {
try {
byte[] value = serialize(obj);
return Base64.encodeToString(value);
} catch (Exception e) {
throw new RuntimeException("serialize session error", e);
}
} public static Session deserializeFromString(String base64) {
try {
byte[] objectData = Base64.decode(base64);
return deserialize(objectData);
} catch (Exception e) {
throw new RuntimeException("deserialize session error", e);
}
} public static <T> Collection<T> deserializeFromStringController(Collection<String> base64s) {
try {
List<T> list = Lists.newLinkedList();
for (String base64 : base64s) {
byte[] objectData = Base64.decode(base64);
T t = deserialize(objectData);
list.add(t);
}
return list;
} catch (Exception e) {
throw new RuntimeException("deserialize session error", e);
}
}
}

我的Dao实现,ShiroSession是我自己实现的,原因在后面说明,默认使用的是SimpleSession

package com.gqshao.authentication.dao;

import com.gqshao.authentication.session.ShiroSession;
import com.gqshao.authentication.utils.SerializeUtils;
import com.gqshao.redis.component.JedisUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction; import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Set; /**
* 针对自定义的ShiroSession的Redis CRUD操作,通过isChanged标识符,确定是否需要调用Update方法
* 通过配置securityManager在属性cacheManager查找从缓存中查找Session是否存在,如果找不到才调用下面方法
* Shiro内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了CacheManagerAware并自动注入相应的CacheManager。
*/
public class CachingShiroSessionDao extends CachingSessionDAO { private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class); // 保存到Redis中key的前缀 prefix+sessionId
private String prefix = ""; // 设置会话的过期时间
private int seconds = 0; @Autowired
private JedisUtils jedisUtils; /**
* 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读
*/
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session session = getCachedSession(sessionId);
if (session == null
|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
session = this.doReadSession(sessionId);
if (session == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
} else {
// 缓存
cache(session, session.getId());
}
}
return session;
} /**
* 根据会话ID获取会话
*
* @param sessionId 会话ID
* @return ShiroSession
*/
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = null;
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
String key = prefix + sessionId;
String value = jedis.get(key);
if (StringUtils.isNotBlank(value)) {
session = SerializeUtils.deserializeFromString(value);
logger.info("sessionId {} ttl {}: ", sessionId, jedis.ttl(key));
// 重置Redis中缓存过期时间
jedis.expire(key, seconds);
logger.info("sessionId {} name {} 被读取", sessionId, session.getClass().getName());
}
} catch (Exception e) {
logger.warn("读取Session失败", e);
} finally {
jedisUtils.returnResource(jedis);
} return session;
} public Session doReadSessionWithoutExpire(Serializable sessionId) {
Session session = null;
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
String key = prefix + sessionId;
String value = jedis.get(key);
if (StringUtils.isNotBlank(value)) {
session = SerializeUtils.deserializeFromString(value);
}
} catch (Exception e) {
logger.warn("读取Session失败", e);
} finally {
jedisUtils.returnResource(jedis);
} return session;
} /**
* 如DefaultSessionManager在创建完session后会调用该方法;
* 如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;
* 返回会话ID;主要此处返回的ID.equals(session.getId());
*/
@Override
protected Serializable doCreate(Session session) {
// 创建一个Id并设置给Session
Serializable sessionId = this.generateSessionId(session);
assignSessionId(session, sessionId);
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
// session由Redis缓存失效决定,这里只是简单标识
session.setTimeout(seconds);
jedis.setex(prefix + sessionId, seconds, SerializeUtils.serializeToString((ShiroSession) session));
logger.info("sessionId {} name {} 被创建", sessionId, session.getClass().getName());
} catch (Exception e) {
logger.warn("创建Session失败", e);
} finally {
jedisUtils.returnResource(jedis);
}
return sessionId;
} /**
* 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
*/
@Override
protected void doUpdate(Session session) {
//如果会话过期/停止 没必要再更新了
try {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
} catch (Exception e) {
logger.error("ValidatingSession error");
} Jedis jedis = null;
try {
if (session instanceof ShiroSession) {
// 如果没有主要字段(除lastAccessTime以外其他字段)发生改变
ShiroSession ss = (ShiroSession) session;
if (!ss.isChanged()) {
return;
}
Transaction tx = null;
try {
jedis = jedisUtils.getResource();
// 开启事务
tx = jedis.multi();
ss.setChanged(false);
tx.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString(ss));
logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName());
// 执行事务
tx.exec();
} catch (Exception e) {
if (tx != null) {
// 取消执行事务
tx.discard();
}
throw e;
} } else if (session instanceof Serializable) {
jedis = jedisUtils.getResource();
jedis.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString((Serializable) session));
logger.info("sessionId {} name {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());
} else {
logger.warn("sessionId {} name {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());
}
} catch (Exception e) {
logger.warn("更新Session失败", e);
} finally {
jedisUtils.returnResource(jedis);
}
} /**
* 删除会话;当会话过期/会话停止(如用户退出时)会调用
*/
@Override
protected void doDelete(Session session) {
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
jedis.del(prefix + session.getId());
logger.debug("Session {} 被删除", session.getId());
} catch (Exception e) {
logger.warn("修改Session失败", e);
} finally {
jedisUtils.returnResource(jedis);
}
} /**
* 删除cache中缓存的Session
*/
public void uncache(Serializable sessionId) {
Session session = this.readSession(sessionId);
super.uncache(session);
logger.info("取消session {} 的缓存", sessionId);
} /**
* 获取当前所有活跃用户,如果用户量多此方法影响性能
*/
@Override
public Collection<Session> getActiveSessions() {
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
Set<String> keys = jedis.keys(prefix + "*");
if (CollectionUtils.isEmpty(keys)) {
return null;
}
List<String> valueList = jedis.mget(keys.toArray(new String[0]));
return SerializeUtils.deserializeFromStringController(valueList);
} catch (Exception e) {
logger.warn("统计Session信息失败", e);
} finally {
jedisUtils.returnResource(jedis);
}
return null;
} public void setPrefix(String prefix) {
this.prefix = prefix;
} public void setSeconds(int seconds) {
this.seconds = seconds;
} }

二.Session和SessionFactory

步骤2:经过上面的开发已经可以使用的,但发现每次访问都会多次调用SessionDAO的doUpdate方法,来更新Redis上数据,过来发现更新的字段只有LastAccessTime(最后一次访问时间),由于会话失效是由Redis数据过期实现的,这个字段意义不大,为了减少对Redis的访问,降低网络压力,实现自己的Session,在SimpleSession上套一层,增加一个标识位,如果Session除lastAccessTime意外其它字段修改,就标识一下,只有标识为修改的才可以通过doUpdate访问Redis,否则直接返回。这也是上面SessionDao中doUpdate中逻辑判断的意义

package com.gqshao.authentication.session;

import org.apache.shiro.session.mgt.SimpleSession;

import java.io.Serializable;
import java.util.Date;
import java.util.Map; /**
* 由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,
* 增加标识位,如果只是更新lastAccessTime SessionDao update方法直接返回
*/
public class ShiroSession extends SimpleSession implements Serializable {
// 除lastAccessTime以外其他字段发生改变时为true
private boolean isChanged; public ShiroSession() {
super();
this.setChanged(true);
} public ShiroSession(String host) {
super(host);
this.setChanged(true);
} @Override
public void setId(Serializable id) {
super.setId(id);
this.setChanged(true);
} @Override
public void setStopTimestamp(Date stopTimestamp) {
super.setStopTimestamp(stopTimestamp);
this.setChanged(true);
} @Override
public void setExpired(boolean expired) {
super.setExpired(expired);
this.setChanged(true);
} @Override
public void setTimeout(long timeout) {
super.setTimeout(timeout);
this.setChanged(true);
} @Override
public void setHost(String host) {
super.setHost(host);
this.setChanged(true);
} @Override
public void setAttributes(Map<Object, Object> attributes) {
super.setAttributes(attributes);
this.setChanged(true);
} @Override
public void setAttribute(Object key, Object value) {
super.setAttribute(key, value);
this.setChanged(true);
} @Override
public Object removeAttribute(Object key) {
this.setChanged(true);
return super.removeAttribute(key);
} /**
* 停止
*/
@Override
public void stop() {
super.stop();
this.setChanged(true);
} /**
* 设置过期
*/
@Override
protected void expire() {
this.stop();
this.setExpired(true);
} public boolean isChanged() {
return isChanged;
} public void setChanged(boolean isChanged) {
this.isChanged = isChanged;
} @Override
public boolean equals(Object obj) {
return super.equals(obj);
} @Override
protected boolean onEquals(SimpleSession ss) {
return super.onEquals(ss);
} @Override
public int hashCode() {
return super.hashCode();
} @Override
public String toString() {
return super.toString();
}
}
package com.gqshao.authentication.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionFactory; public class ShiroSessionFactory implements SessionFactory { @Override
public Session createSession(SessionContext initData) {
ShiroSession session = new ShiroSession();
return session;
}
}

三.SessionListener

步骤3:发现用户推出后,Session没有从Redis中销毁,虽然当前重新new了一个,但会对统计带来干扰,通过SessionListener解决这个问题

package com.gqshao.authentication.listener;

import com.gqshao.authentication.dao.CachingShiroSessionDao;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; public class ShiroSessionListener implements SessionListener { private static final Logger logger = LoggerFactory.getLogger(ShiroSessionListener.class); @Autowired
private CachingShiroSessionDao sessionDao; @Override
public void onStart(Session session) {
// 会话创建时触发
logger.info("ShiroSessionListener session {} 被创建", session.getId());
} @Override
public void onStop(Session session) {
sessionDao.delete(session);
// 会话被停止时触发
logger.info("ShiroSessionListener session {} 被销毁", session.getId());
} @Override
public void onExpiration(Session session) {
sessionDao.delete(session);
//会话过期时触发
logger.info("ShiroSessionListener session {} 过期", session.getId());
}
}

四.将账号信息放到Session中

修改realm中AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)方法,在返回AuthenticationInfo之前添加下面的代码,把用户信息放到Session中

// 把账号信息放到Session中,并更新缓存,用于会话管理
Subject subject = SecurityUtils.getSubject();
Serializable sessionId = subject.getSession().getId();
ShiroSession session = (ShiroSession) sessionDao.doReadSessionWithoutExpire(sessionId);
session.setAttribute("userId", su.getId());
session.setAttribute("loginName", su.getLoginName());
sessionDao.update(session);

五. 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd"> <description>Shiro安全配置</description> <!-- Shiro's main business-tier object for web-enabled applications -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroDbRealm"/>
<!-- 可选项 最好使用,SessionDao,中 doReadSession 读取过于频繁了-->
<property name="cacheManager" ref="shiroEhcacheManager"/>
<!--可选项 默认使用ServletContainerSessionManager,直接使用容器的HttpSession,可以通过配置sessionManager,使用DefaultWebSessionManager来替代-->
<property name="sessionManager" ref="sessionManager"/>
</bean> <!-- 項目自定义的Realm -->
<bean id="shiroDbRealm" class="com.gqshao.authentication.realm.ShiroDbRealm"/> <!-- Shiro Filter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 指向登陆路径,整合spring时指向控制器方法地址 -->
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/"/>
<!-- 可选配置,通过实现自己的AuthenticatingFilter实现表单的自定义 -->
<property name="filters">
<util:map>
<entry key="authc">
<bean class="com.gqshao.authentication.filter.MyAuthenticationFilter"/>
</entry>
</util:map>
</property> <property name="filterChainDefinitions">
<value>
/login = authc
/logout = logout
/static/** = anon
/** = user
</value>
</property>
</bean> <!-- 用户授权信息Cache, 采用EhCache,本地缓存最长时间应比中央缓存时间短一些,以确保Session中doReadSession方法调用时更新中央缓存过期时间 -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:security/ehcache-shiro.xml"/>
</bean> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 设置全局会话超时时间,默认30分钟(1800000) -->
<property name="globalSessionTimeout" value="1800000"/>
<!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true-->
<property name="deleteInvalidSessions" value="false"/>
<!-- 是否开启会话验证器任务 默认true -->
<property name="sessionValidationSchedulerEnabled" value="false"/>
<!-- 会话验证器调度时间 -->
<property name="sessionValidationInterval" value="1800000"/>
<property name="sessionFactory" ref="sessionFactory"/>
<property name="sessionDAO" ref="sessionDao"/>
<!-- 默认JSESSIONID,同tomcat/jetty在cookie中缓存标识相同,修改用于防止访问404页面时,容器生成的标识把shiro的覆盖掉 -->
<property name="sessionIdCookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="SHRIOSESSIONID"/>
</bean>
</property>
<property name="sessionListeners">
<list>
<bean class="com.gqshao.authentication.listener.ShiroSessionListener"/>
</list>
</property>
</bean> <!-- 自定义Session工厂方法 返回会标识是否修改主要字段的自定义Session-->
<bean id="sessionFactory" class="com.gqshao.authentication.session.ShiroSessionFactory"/> <!-- 普通持久化接口,不会被缓存 每次doReadSession会被反复调用 -->
<!--<bean class="com.gqshao.authentication.dao.RedisSessionDao">-->
<!-- 使用可被缓存的Dao ,本地缓存减轻网络压力 -->
<!--<bean id="sessionDao" class="com.gqshao.authentication.dao.CachingSessionDao">-->
<!-- 可缓存Dao,操作自定义Session,添加标识位,减少doUpdate方法中Redis的连接次数来减轻网络压力 -->
<bean id="sessionDao" class="com.gqshao.authentication.dao.CachingShiroSessionDao">
<property name="prefix" value="ShiroSession_"/>
<!-- 注意中央缓存有效时间要比本地缓存有效时间长-->
<property name="seconds" value="1800"/>
</bean> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- AOP式方法级权限检查 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
<ehcache updateCheck="false" name="shiroCache">
<!--
timeToIdleSeconds 当缓存闲置n秒后销毁 为了保障会调用ShiroSessionDao的doReadSession方法,所以不配置改属性
timeToLiveSeconds 当缓存存活n秒后销毁 必须必Redis中过期时间短
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToLiveSeconds="60"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="10"
/>
</ehcache>

六.测试会话管理

package com.gqshao.authentication.controller;

import com.gqshao.authentication.dao.CachingShiroSessionDao;
import org.apache.shiro.session.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import java.io.Serializable;
import java.util.Collection; @Controller
@RequestMapping("/session")
public class SessionController { @Autowired
private CachingShiroSessionDao sessionDao; @RequestMapping("/active")
@ResponseBody
public Collection<Session> getActiveSessions() {
return sessionDao.getActiveSessions();
} @RequestMapping("/read")
@ResponseBody
public Session readSession(Serializable sessionId) {
return sessionDao.doReadSessionWithoutExpire(sessionId);
}
}

七.集群情况下的改造

1.问题上面启用了Redis中央缓存、EhCache本地JVM缓存,AuthorizingRealm的doGetAuthenticationInfo登陆认证方法返回的AuthenticationInfo,默认情况下会被保存到Session的Attribute下面两个字段中

org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY 保存 principal
org.apache.shiro.subject.support.DefaultSubjectContext.AUTHENTICATED_SESSION_KEY 保存 boolean是否登陆

然后在每次请求过程中,在ShiroFilter中组装Subject时,读取Session中这两个字段

现在的问题是Session被缓存到本地JVM堆中,也就是说服务器A登陆,无法修改服务器B的EhCache中Session属性,导致服务器B没有登陆。

处理方法有很多思路,比如重写CachingSessionDAO,readSession如果没有这两个属性就不缓存(没登陆就不缓存),或者cache的session没有这两个属性就调用自己实现的doReadSession方法从Redis中重读一下。

/**
* 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读
*/
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session session = getCachedSession(sessionId);
if (session == null
|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
session = this.doReadSession(sessionId);
if (session == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
} else {
// 缓存
cache(session, session.getId());
}
}
return session;
}

2.如果需要保持各个服务器Session是完全同步的,可以通过Redis消息订阅/发布功能,再调用SessionDao中实现了删除Session本地缓存的方法

Shiro经过Redis管理会话实现集群(转载)的更多相关文章

  1. Redis核心解读:集群管理工具(Redis-sentinel)

    Redis核心解读:集群管理工具(Redis-sentinel) - Redis - TechTarget数据库 Redis核心解读:集群管理工具(Redis-sentinel)

  2. Redis 3.0 Cluster集群配置

    Redis 3.0 Cluster集群配置 安装环境依赖 安装gcc:yum install gcc 安装zlib:yum install zib 安装ruby:yum install ruby 安装 ...

  3. 分布式缓存技术redis学习系列(四)——redis高级应用(集群搭建、集群分区原理、集群操作)

    本文是redis学习系列的第四篇,前面我们学习了redis的数据结构和一些高级特性,点击下面链接可回看 <详细讲解redis数据结构(内存模型)以及常用命令> <redis高级应用( ...

  4. redis + 主从 + 持久化 + 分片 + 集群 + spring集成

    Redis是一个基于内存的数据库,其不仅读写速度快,每秒可以执行大约110000的写操作,81000的读取操作,而且其支持存储字符串,哈希结构,链表,集合丰富的数据类型.所以得到很多开发者的青睐.加之 ...

  5. 分布式缓存技术redis学习(四)——redis高级应用(集群搭建、集群分区原理、集群操作)

    本文是redis学习系列的第四篇,前面我们学习了redis的数据结构和一些高级特性,点击下面链接可回看 <详细讲解redis数据结构(内存模型)以及常用命令> <redis高级应用( ...

  6. Redis Cluster高可用集群在线迁移操作记录【转】

    之前介绍了redis cluster的结构及高可用集群部署过程,今天这里简单说下redis集群的迁移.由于之前的redis cluster集群环境部署的服务器性能有限,需要迁移到高配置的服务器上.考虑 ...

  7. 分布式缓存技术redis系列(四)——redis高级应用(集群搭建、集群分区原理、集群操作)

    本文是redis学习系列的第四篇,前面我们学习了redis的数据结构和一些高级特性,点击下面链接可回看 <详细讲解redis数据结构(内存模型)以及常用命令> <redis高级应用( ...

  8. 转:Redis 3.2.1集群搭建

    Redis 3.2.1集群搭建   一.概述 Redis3.0版本之后支持Cluster. 1.1.redis cluster的现状 目前redis支持的cluster特性: 1):节点自动发现 2) ...

  9. Gravitational Teleport 开源的通过ssh && kubernetes api 管理linux 服务器集群的网关

    Gravitational Teleport 是一个开源的通过ssh && kubernetes api 管理linux 服务器集群的网关 支持以下功能: 基于证书的身份认证 ssh ...

随机推荐

  1. 手把手教你免费把网站IP换成1.1.1.1/1.0.0.1

    近日,Cloudflare官方发文,与APNIC官方合作打算用IP1.1.1.1推出速度更快.私密性更强的DNS Cloudflare 运行全球规模最大.速度最快的网络之一. APNIC 是一个非营利 ...

  2. H3C S5024P交换机 vlan实验

    H3C S5024P交换机第二次vlan实验 实验1 与交换机端口G0/1和G0/2相连的PC1与PC2属于VLAN 1,与G0/3和G0/4相连的PC3和PC4属于VLAN 2,PC1.PC2.PC ...

  3. android和IOS长连接区别

    http://blog.csdn.net/zhangzeyuaaa/article/details/39028369 首先我们必须知道,所有的推送功能必须有一个客户端和服务器的长连接,因为推送是由服务 ...

  4. ubuntu命令行转换图片像素大小

    convert -resize 512x256 00433.png 00001.png 1.512和256之间是x(就是字母那个x),用' * '反而会报错 2.这个命令会按照原图的比例进行转换 3. ...

  5. Linux下scp报Permission denied错误的解决方法

    sudo vim /etc/ssh/sshd_config 把PermitRootLogin no改成PermitRootLogin yes如果原来没有这行或被注释掉,就直接加上PermitRootL ...

  6. SQL Server连接不上本地服务器

    昨天星期一,到公司,如常打开电脑后,上个厕所,吃个早餐,电脑才完全醒来.打开项目后台,发现登不上,用户名或密码错误,认真输入几遍,还是错误,打开本地数据库,sql server连接不上,提示错误: 我 ...

  7. Intel CPU参数查询网站

    链接:https://ark.intel.com/#@Processors

  8. python基础(一)—— 核心数据类型

    Hello World程序 [root@mysql ~]# python3 Python 3.6.5 (default, Jul  8 2018, 11:41:23) [GCC 4.4.7 20120 ...

  9. win7下qt error: undefined reference to `_imp__getnameinfo@28'解决

    _imp__getnameinfo@28对应着winsock2.h的getnameinfo函数 首先需要导入对应的头文件 #ifndef WIN32 #include <sys/socket.h ...

  10. 【HIHOCODER 1049】 后序遍历

    描述 在参与过了美食节之后,小Hi和小Ho在别的地方又玩耍了一阵子,在这个过程中,小Ho得到了一个非常有意思的玩具--一棵由小球和木棍连接起来的二叉树! 小Ho对这棵二叉树爱不释手,于是给它的每一个节 ...