短网址服务系统 | Vert.X Web & Redis & 异步编程

短网址服务系统

一个短网址服务系统,可通过 RESTful API 来生成新短网址,短网址与原网址的映射存储在 Redis 数据库中,用户请求短网址时会被重定向到原网址。后台使用 Vert.X-Web 和异步编程。短网址生成使用原网址到62进制映射的方案。

短网址服务原理可参考 短网址(short URL)系统的原理及其实现 | 思否

RESTful API 说明:

  • POST /api/create{‘srcUrl’: ‘source url’} | 返回提交原网址的 短网址
{"shortUrl": "xxx.xx/5Fdx6l", "date": "2020-4-28 16:22", "state": "success|fail"}
  • GET /api/list | 返回已创建的所有短网址
[ {"shortUrl": "short url", "srcUrl": "source url", "date": "create date" } ]
  • GET /:shortUrlKey | 通过短网址,重定向到原网址

代码设计简要说明

  • shorturl.common.Convertor 是一个工具类,主要用来把十进制整数转62进制,和把一个字符串转62进制。

  • shorturl.verticle 包中都是各个 verticleRedisVerticle 用来创建和管理 Redis 数据库,以及通过事件机制(异步)相应 Redis 的读写请求;RestVerticle 用来创建 web 服务,提供 RESTful API 管理接口和短域名的路由重定向,它会异步请求读写 Redis。

  • short.Server 部署和运行 Vert.X 服务。

其他说明

  • 数据存储为什么不用 ConcurrentHashMap 或 MySQL?

因为我想应用重启后,之前创建的短域名能再次使用,因此必须使用外置的数据库,放弃 ConcurrentHashMap。因为 Web 层已经使用了 Vert.X,一个异步的、高并发性能的框架(工具套件),再使用低性能的关系型数据库肯定不匹配,因此使用了 NOSQL 数据库 Redis, 另外数据存储内容也很简单。

  • 有什么需要改进的吗?

当然很多,第一件容易改进的就是让日志系统打印出更详细、具体的内容。另一个,有些挑战性的就是,当短域名创建的多时,如何处理62进制短网址的同名冲突,一个简单的思路是生成短域名后,去检测 Redis 里是否已存在相同键(短域名),如果存在,则把短域名做个小修改(比如+1),然后再去 Redis 里检测是否有相同键,直到不存在相同键为止。(欢迎评论区大佬给建议)

运行

首先确保 Redis 数据库已启动

  • IDE 直接运行 short.Server 的 main 方法;

  • 生成 jar 包,运行 java -jar short-url-**.jar short.Server

源码

包结构

短网址服务系统 | Vert.X Web & Redis & 异步编程_第1张图片

shorturl.common.Convertor

package com.onemsg.shorturl.common;

public class Convertor {

    static final char[] digits = {
        '0' , '1' , '2' , '3' , '4' , '5' ,
        '6' , '7' , '8' , '9' , 'a' , 'b' ,
        'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
        'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
        'o' , 'p' , 'q' , 'r' , 's' , 't' ,
        'u' , 'v' , 'w' , 'x' , 'y' , 'z' ,
        'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 
        'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 
        'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 
        'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 
        'Y' , 'Z'
    };

    /**
     * 十进制数字 转 62 进制
     * @param i
     * @return
     */
    public static String to62String(int i) {

        int radix = 62;
        char[] buf = new char[12]; //Integer.MAX_VALUE 大约是 62 的 11 次幂
        boolean negative = (i < 0);
        int charPos = 11;

        if (!negative) {
            i = -i;
        }

        while (i <= -radix) {
            buf[charPos--] = digits[-(i % radix)];
            i = i / radix;
        }
        buf[charPos] = digits[-i];

        if (negative) {
            buf[--charPos] = '-';
        }

        return String.valueOf(buf, charPos, 12 - charPos);
    }

    /**
     * 给定字符串的 hashCode 转 62 进制
     * @param s
     * @return
     */
    public static String to62String(String s){
        int hash = Math.abs(s.hashCode());
        return to62String(hash);
    }

}

shorturl.verticle.RedisVerticle

package com.onemsg.shorturl.verticle;

import java.time.LocalDateTime;
import java.util.List;

import com.onemsg.shorturl.common.Convertor;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.MessageConsumer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.redis.client.Redis;
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.RedisConnection;
import io.vertx.redis.client.Response;
import io.vertx.redis.client.ResponseType;

/**
 * RedisVerticle
 */
public class RedisVerticle extends AbstractVerticle {

    private static final int MAX_RECONNECT_RETRIES = 16;
    private RedisConnection client;
    private RedisAPI redis;
    Logger logger = LoggerFactory.getLogger(RedisVerticle.class);

    public static final String SHORTURL_CREATE_ADDRESS = "redis.shorturl.create";
    public static final String SHORTURL_GET_ADDRESS = "redis.shorturl.get";
    public static final String SHORTURL_LIST_ADDRESS = "redis.shorturl.list";
    private static final String REDIS_CONNECTION_STRING = "redis://localhost:6379/1";

    @Override
    public void start() throws Exception {

        createRedisClient(onCreate -> {
            if (onCreate.succeeded()) {
                System.out.println("Redis 连接成功!");
                redis = RedisAPI.api(client);
            }
        });

        createCURDService();

    }

    private void createRedisClient(Handler<AsyncResult<RedisConnection>> handler) {
                                    
        Redis.createClient(vertx, REDIS_CONNECTION_STRING).connect(onConnect -> {
            if (onConnect.succeeded()) {
                client = onConnect.result();
                // make sure the client is reconnected on error
                client.exceptionHandler(e -> {
                    // attempt to reconnect
                    attemptReconnect(0);
                });
            }
            // allow further processing
            handler.handle(onConnect);
        });
    }

    private void attemptReconnect(int retry) {
        if (retry > MAX_RECONNECT_RETRIES) {
            // we should stop now, as there's nothing we can do.
        } else {
            // retry with backoff up to 10240 ms
            long backoff = (long) (Math.pow(2, Math.min(retry, 10)) * 10);

            vertx.setTimer(backoff, timer -> createRedisClient(onReconnect -> {
                if (onReconnect.failed()) {
                    attemptReconnect(retry + 1);
                }
            }));
        }
    }

    private void createCURDService() {

        EventBus bus = vertx.eventBus();
    
        MessageConsumer<JsonObject> consumner =  bus.consumer(SHORTURL_CREATE_ADDRESS);
        consumner.handler( msg -> {
            JsonObject json = msg.body();

            String srcUrl = json.getString("srcUrl");
            String shortUrl = Convertor.to62String( srcUrl );

            LocalDateTime date = LocalDateTime.now();

            redis.hmset( List.of(shortUrl, "srcUrl", srcUrl, "date", date.toString() ), res -> {
                if( res.succeeded() ){
                    String result = res.result().toString();
                    if(result.equals("OK")){
                        logger.info("短网址创建完成 " + srcUrl + " -> " + shortUrl);
                        msg.reply( new JsonObject()
                            .put("shortUrl", shortUrl)
                            .put("srcUrl", srcUrl)
                            .put("date", date.toString())
                        );
                    }else{
                        logger.error("短网址创建失败 " + srcUrl + " -> " + shortUrl);
                        msg.reply( new JsonObject());
                    }
                }else{
                    logger.error("短网址创建失败 " + srcUrl + " -> " + shortUrl);
                    msg.reply(new JsonObject());
                }
            });
        } );

        bus.<String>consumer(SHORTURL_GET_ADDRESS, msg -> {

            String shortUrl = msg.body();
            redis.hget(shortUrl, "srcUrl", res -> {
                if (res.succeeded()) {
                    if(res.result().type() == ResponseType.ERROR){
                        msg.reply( null);  
                    }else{
                        String srcUrl = res.result().toString();
                        msg.reply( srcUrl);          
                    }
                }else {
                    msg.reply(null);
                }
            });
        });
        
        bus.<String>consumer(SHORTURL_LIST_ADDRESS, msg -> {

            JsonArray array = new JsonArray();
            redis.keys("*", res -> {
                if(res.succeeded()){

                    if(res.result().type() == ResponseType.MULTI){
                        
                        for(Response r : res.result()){
                            
                            redis.hgetall(r.toString(), res2 -> {
                                Response data = res2.result();
                                JsonObject json = new JsonObject();
                                json.put("shortUrl", r.toString())
                                    .put(data.get(0).toString(), data.get(1).toString())
                                    .put(data.get(2).toString(), data.get(3).toString());
                            
                                logger.info(json.toString());
                                array.add(json);
                            });
                        }

                        msg.reply(array);
                        logger.info("list 请求已回复");
                    }
                }
            } );

        });
    }

}

shorturl.verticle.RestVerticle

package com.onemsg.shorturl.verticle;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;

public class RestVerticle extends AbstractVerticle {

    Logger logger = LoggerFactory.getLogger(RestVerticle.class);

    @Override
    public void start() throws Exception {

        Router router = Router.router(vertx);
        router.route().handler(BodyHandler.create());

        router.get("/:shortUrlKey").handler(this::handleRedirect);
        router.get("/api/list").handler(this::handleListShortUrl);
        router.post("/api/create").handler(this::handleCreateShortUrl);

        vertx.createHttpServer().requestHandler(router).listen(8080);
        logger.info("REST web 服务器启动成功!监听端口 8080");
    }

    private void handleCreateShortUrl(RoutingContext context) {

        JsonObject json = context.getBodyAsJson();
        EventBus bus = vertx.eventBus();
        bus.<JsonObject>request(RedisVerticle.SHORTURL_CREATE_ADDRESS, json, reply -> {
            if( reply.succeeded() ){
                JsonObject data = reply.result().body();
                data.put("shortUrl", context.request().host() + "/" + data.getString("shortUrl") );
                context.response().putHeader("content-type", "application/json").end(data.toString());
            }
        });
    }

    private void handleRedirect(RoutingContext context){

        String shortUrlKey = context.pathParam("shortUrlKey");
        EventBus bus = vertx.eventBus();
        bus.<String>request(RedisVerticle.SHORTURL_GET_ADDRESS, shortUrlKey, reply -> {
            if( reply.succeeded() ){
                String srcUrl = reply.result().body();
                if(srcUrl != null){
                    context.response().putHeader("Location", srcUrl).setStatusCode(302).end();
                }else{
                    sendError(404, context.response());
                }

            }
        });
    }

    private void handleListShortUrl(RoutingContext context) {

        EventBus bus = vertx.eventBus();
        bus.<JsonArray>request(RedisVerticle.SHORTURL_LIST_ADDRESS, "list", reply -> {
            if( reply.succeeded()){
                JsonArray data = reply.result().body();
                context.response().putHeader("content-type", "application/json").end(data.toString());
            }
        });
    }

    private void sendError(int statusCode, HttpServerResponse response ){
        response.setStatusCode(404).end();
    }
}

shorturl.Server

package com.onemsg.shorturl;

import com.onemsg.shorturl.verticle.RedisVerticle;
import com.onemsg.shorturl.verticle.RestVerticle;

import io.vertx.core.DeploymentOptions;
import io.vertx.core.Vertx;


public class Server {

    public static void main(String[] args) {
        
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(RestVerticle.class, new DeploymentOptions().setInstances(4));
        vertx.deployVerticle(RedisVerticle.class, new DeploymentOptions().setWorker(true).setInstances(16));
    }
}

Hey guys, thank u for watching, more communication?@me pls

你可能感兴趣的:(Projects)