一个短网址服务系统,可通过 RESTful API 来生成新短网址,短网址与原网址的映射存储在 Redis 数据库中,用户请求短网址时会被重定向到原网址。后台使用 Vert.X-Web 和异步编程。短网址生成使用原网址到62进制映射的方案。
短网址服务原理可参考 短网址(short URL)系统的原理及其实现 | 思否
{"shortUrl": "xxx.xx/5Fdx6l", "date": "2020-4-28 16:22", "state": "success|fail"}
[ {"shortUrl": "short url", "srcUrl": "source url", "date": "create date" } ]
shorturl.common.Convertor
是一个工具类,主要用来把十进制整数转62进制,和把一个字符串转62进制。
shorturl.verticle
包中都是各个 verticle
。 RedisVerticle
用来创建和管理 Redis 数据库,以及通过事件机制(异步)相应 Redis 的读写请求;RestVerticle
用来创建 web 服务,提供 RESTful API 管理接口和短域名的路由重定向,它会异步请求读写 Redis。
short.Server
部署和运行 Vert.X 服务。
因为我想应用重启后,之前创建的短域名能再次使用,因此必须使用外置的数据库,放弃 ConcurrentHashMap。因为 Web 层已经使用了 Vert.X,一个异步的、高并发性能的框架(工具套件),再使用低性能的关系型数据库肯定不匹配,因此使用了 NOSQL 数据库 Redis, 另外数据存储内容也很简单。
当然很多,第一件容易改进的就是让日志系统打印出更详细、具体的内容。另一个,有些挑战性的就是,当短域名创建的多时,如何处理62进制短网址的同名冲突,一个简单的思路是生成短域名后,去检测 Redis 里是否已存在相同键(短域名),如果存在,则把短域名做个小修改(比如+1),然后再去 Redis 里检测是否有相同键,直到不存在相同键为止。(欢迎评论区大佬给建议)
首先确保 Redis 数据库已启动
IDE 直接运行 short.Server
的 main 方法;
生成 jar 包,运行 java -jar short-url-**.jar short.Server
。
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);
}
}
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 请求已回复");
}
}
} );
});
}
}
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();
}
}
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