Now more and more popular micro-service architecture, mention the micro-service architecture, you can think of spring boot and vertx bar! The former is more listened to than communicated, but today I share with you the latter vertx. For more information, read the vertx website http://vertx.io/docs/vertx-we...
No more nonsense, go straight to the subject. Today we share session sharing in vertx web. In the company, I developed a web platform with vertx, but need to prevent the downtime from continuing to provide services, so deployed two machines, here we begin to involve session sharing. For performance reasons, I wanted to put sessions in redis to achieve my goal, but there was no such implementation on vertx. At that time, I used Hazelcast first. The other day I took time to look at the underlying code, wrapped it up by myself, and put sessions in redis. github address: https://github.com/robin0909/...
Design of native vertx session
The following is the structural relationship between Local Session Store Impl and Clustered Session Store Impl:
LocalSession:
ClusteredSession:
From the above structure, we can find an inheritance implementation relationship. The top-level interface is SessionStore.
And what interface is SessionStore? In vertx, session has a special design. SessionStore here defines the interface specifically for storing session. See what methods are defined in this interface.
public interface SessionStore { //Attributes used primarily for distributed session sharing, retry session time from the store long retryTimeout(); Session createSession(long timeout); //Get Session from the store based on session Id void get(String id, Handler<AsyncResult<@Nullable Session>> resultHandler); //delete void delete(String id, Handler<AsyncResult<Boolean>> resultHandler); //Increase session void put(Session session, Handler<AsyncResult<Boolean>> resultHandler); //empty void clear(Handler<AsyncResult<Boolean>> resultHandler); //store size void size(Handler<AsyncResult<Integer>> resultHandler); //Close, Release Resource Operations void close(); }
Many of the above will use one attribute, session Id (id). In session mechanism, we also need to rely on browser-side cookies. When the server-side session is generated, the server will set a vertx-web.session=4d9db69d-7577-4b17-8a66-4d6a2472cd33 in the cookie to return to the browser. Presumably you can also see that is a uuid code, that is session Id.
Next, we can look at the secondary sub-interface. Secondary sub-interface function, in fact, is very simple, directly on the code, you will understand.
public interface LocalSessionStore extends SessionStore { long DEFAULT_REAPER_INTERVAL = 1000; String DEFAULT_SESSION_MAP_NAME = "vertx-web.sessions"; static LocalSessionStore create(Vertx vertx) { return new LocalSessionStoreImpl(vertx, DEFAULT_SESSION_MAP_NAME, DEFAULT_REAPER_INTERVAL); } static LocalSessionStore create(Vertx vertx, String sessionMapName) { return new LocalSessionStoreImpl(vertx, sessionMapName, DEFAULT_REAPER_INTERVAL); } static LocalSessionStore create(Vertx vertx, String sessionMapName, long reaperInterval) { return new LocalSessionStoreImpl(vertx, sessionMapName, reaperInterval); } }
The main purpose here is to use and construct aspects gracefully, router. route (). handler (SessionHandler. create (LocalSessionStore. create (vertx)); somewhat similar to factory, create objects. In this interface, some proprietary parameters can also be initialized. So there's no difficulty.
We understand the official code very well, so let's start encapsulating our RedisSession Store.
Your own RedisSession Store package
First, we define a RedisSessionStore interface, which inherits the SessionStore interface.
/** * Created by robinyang on 2017/3/13. */ public interface RedisSessionStore extends SessionStore { long DEFAULT_RETRY_TIMEOUT = 2 * 1000; String DEFAULT_SESSION_MAP_NAME = "vertx-web.sessions"; static RedisSessionStore create(Vertx vertx) { return new RedisSessionStoreImpl(vertx, DEFAULT_SESSION_MAP_NAME, DEFAULT_RETRY_TIMEOUT); } static RedisSessionStore create(Vertx vertx, String sessionMapName) { return new RedisSessionStoreImpl(vertx, sessionMapName, DEFAULT_RETRY_TIMEOUT); } static RedisSessionStore create(Vertx vertx, String sessionMapName, long reaperInterval) { return new RedisSessionStoreImpl(vertx, sessionMapName, reaperInterval); } RedisSessionStore host(String host); RedisSessionStore port(int port); RedisSessionStore auth(String pwd); }
Next, I create a RedisSessionStoreImpl class. Here I give a written RedisSessionStoreImpl, which I will explain later.
public class RedisSessionStoreImpl implements RedisSessionStore { private static final Logger logger = LoggerFactory.getLogger(RedisSessionStoreImpl.class); private final Vertx vertx; private final String sessionMapName; private final long retryTimeout; private final LocalMap<String, Session> localMap; //Default values private String host = "localhost"; private int port = 6379; private String auth; RedisClient redisClient; // Clear All Time Use private List<String> localSessionIds; public RedisSessionStoreImpl(Vertx vertx, String defaultSessionMapName, long retryTimeout) { this.vertx = vertx; this.sessionMapName = defaultSessionMapName; this.retryTimeout = retryTimeout; localMap = vertx.sharedData().getLocalMap(sessionMapName); localSessionIds = new Vector<>(); redisManager(); } @Override public long retryTimeout() { return retryTimeout; } @Override public Session createSession(long timeout) { return new SessionImpl(new PRNG(vertx), timeout, DEFAULT_SESSIONID_LENGTH); } @Override public Session createSession(long timeout, int length) { return new SessionImpl(new PRNG(vertx), timeout, length); } @Override public void get(String id, Handler<AsyncResult<Session>> resultHandler) { redisClient.getBinary(id, res->{ if(res.succeeded()) { Buffer buffer = res.result(); if(buffer != null) { SessionImpl session = new SessionImpl(new PRNG(vertx)); session.readFromBuffer(0, buffer); resultHandler.handle(Future.succeededFuture(session)); } else { resultHandler.handle(Future.succeededFuture(localMap.get(id))); } } else { resultHandler.handle(Future.failedFuture(res.cause())); } }); } @Override public void delete(String id, Handler<AsyncResult<Boolean>> resultHandler) { redisClient.del(id, res->{ if (res.succeeded()) { localSessionIds.remove(id); resultHandler.handle(Future.succeededFuture(true)); } else { resultHandler.handle(Future.failedFuture(res.cause())); logger.error("redis Delete sessionId: {} fail", id, res.cause()); } }); } @Override public void put(Session session, Handler<AsyncResult<Boolean>> resultHandler) { //Before put ting, determine whether session exists. If it exists, check it. redisClient.getBinary(session.id(), res1->{ if (res1.succeeded()) { //Existing data if(res1.result()!=null) { Buffer buffer = res1.result(); SessionImpl oldSession = new SessionImpl(new PRNG(vertx)); oldSession.readFromBuffer(0, buffer); SessionImpl newSession = (SessionImpl)session; if(oldSession.version() != newSession.version()) { resultHandler.handle(Future.failedFuture("Version mismatch")); return; } newSession.incrementVersion(); writeSession(session, resultHandler); } else { //No data exists SessionImpl newSession = (SessionImpl)session; newSession.incrementVersion(); writeSession(session, resultHandler); } } else { resultHandler.handle(Future.failedFuture(res1.cause())); } }); } private void writeSession(Session session, Handler<AsyncResult<Boolean>> resultHandler) { Buffer buffer = Buffer.buffer(); SessionImpl sessionImpl = (SessionImpl)session; //Sequencing session s into buffer s sessionImpl.writeToBuffer(buffer); SetOptions setOptions = new SetOptions().setPX(session.timeout()); redisClient.setBinaryWithOptions(session.id(), buffer, setOptions, res->{ if (res.succeeded()) { logger.debug("set key: {} ", session.data()); localSessionIds.add(session.id()); resultHandler.handle(Future.succeededFuture(true)); } else { resultHandler.handle(Future.failedFuture(res.cause())); } }); } @Override public void clear(Handler<AsyncResult<Boolean>> resultHandler) { localSessionIds.stream().forEach(id->{ redisClient.del(id, res->{ //If it exists in localSession Ids, but expiration does not exist in redis, just notify it. localSessionIds.remove(id); }); }); resultHandler.handle(Future.succeededFuture(true)); } @Override public void size(Handler<AsyncResult<Integer>> resultHandler) { resultHandler.handle(Future.succeededFuture(localSessionIds.size())); } @Override public void close() { redisClient.close(res->{ logger.debug("Close redisClient "); }); } private void redisManager() { RedisOptions redisOptions = new RedisOptions(); redisOptions.setHost(host).setPort(port).setAuth(auth); redisClient = RedisClient.create(vertx, redisOptions); } @Override public RedisSessionStore host(String host) { this.host = host; return this; } @Override public RedisSessionStore port(int port) { this.port = port; return this; } @Override public RedisSessionStore auth(String pwd) { this.auth = pwd; return this; } }
First, start with get() and put(), which are the two core methods.
get(), a uuid is generated when cookies are created, and session is fetched with this id. The first time we find that session cannot be fetched, line 56 will generate a session based on this id.
Each time we send a request, we reset the session expiration time, so after each get, there will be a put operation before returning to the browser, that is, updating the data. Put here is a little more complicated. Before putting, we need to get the session from redis according to the id of the session passed in. If you can't get it, it means that session acquired by get before is not the same object, then an exception will occur, which is equivalent to setting a security threshold. When obtained, then compare the two sessions version is consistent, if not, that session was broken, is the second security threshold settings! No problem, you can put the session and reset the time.
Here we rely on redisClient provided by vertx to manipulate data, so we must introduce this dependency: io.vertx:vertx-redis-client:3.4.1.
Next up is the serialization issue. Here I use a serialization of vertx encapsulation to serialize data into Buffer, and the SessiomImpl class has been serialized, from SessionImple serialization to Buffer and Buffer deserialization.
public class SessionImpl implements Session, ClusterSerializable, Shareable { //... @Override public void writeToBuffer(Buffer buff) { byte[] bytes = id.getBytes(UTF8); buff.appendInt(bytes.length).appendBytes(bytes); buff.appendLong(timeout); buff.appendLong(lastAccessed); buff.appendInt(version); Buffer dataBuf = writeDataToBuffer(); buff.appendBuffer(dataBuf); } @Override public int readFromBuffer(int pos, Buffer buffer) { int len = buffer.getInt(pos); pos += 4; byte[] bytes = buffer.getBytes(pos, pos + len); pos += len; id = new String(bytes, UTF8); timeout = buffer.getLong(pos); pos += 8; lastAccessed = buffer.getLong(pos); pos += 8; version = buffer.getInt(pos); pos += 4; pos = readDataFromBuffer(pos, buffer); return pos; } //... }
These are the implementations of serialization and deserialization.
Local Session Ids are mainly used when clearing sessions, because data is mainly stored sessions, local Session Ids save session Id is a supplementary role.
usage
The usage is simple, just one line of code.
router.route().handler(SessionHandler.create(RedisSessionStore.create(vertx).host("127.0.0.1").port(6349)));