vertx realizes session sharing of redis version

Keywords: Java Session Redis github Spring

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  1. 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)));

Posted by simon13 on Sat, 29 Jun 2019 15:20:17 -0700