短网址生成-nodejs实现

该项目使用mysql+nodejs实现短网址服务;
支持自定义短网址;
完美解决了自定义短网址与自增id生产网址冲突问题。

以下所有文件都放在项目根目录下
init.txt

安装nodejs之后
在项目的根目录下执行下列命令
npm install mysql
npm install winston
npm install uuid
mysql模块,用于连接mysql数据库,执行curd操作。
winston模块,用于代替console.log打印日志。
uuid模块,用于支持配置winston模块的日志id。

db.js
封装了对数据库的curd操作,支持连接池,支持事务,解决了批量插入并且处理了占位符(?)的可读性问题。
封装的另一个目的,是将回调风格改为promise风格,调用者可以使用async-await将异步代码同步化。

const mysql = require('mysql');
const log = require('./log');
const logger = log.defaultLogger;//使用默认日志记录器
const db = {};
var pool  = mysql.createPool({  //创建连接池
  connectionLimit : 10,  
  host            : 'localhost',  
  user            : 'root',  
  password        : '',  
  database        : 'test',
  charset		  : 'utf8'
});
/*  
const conn = mysql.createConnection({
	host:'localhost',
	user:'root',
	password:'',
	database:'jyd'
});
conn.connect();
*/


//获取连接  
db.conn = async function(){
	return new Promise((resolve,reject)=>{
		pool.getConnection(function(err, conn) {  
			if (err) {  
				reject(err);  
			}else{
				resolve(conn);
			}			 
		});  
	});
};
//在事务中执行
db.doInTx = function(cb){
	return new Promise((resolve,reject)=>{
		db.conn().then(conn=>{
			conn.beginTransaction(async(err)=>{
				if(err){
					conn.release();
					await cb(err,null);
					return;
				}
				try{
					const res = await cb(null,conn);
					conn.commit((err)=>{
						logger.info('commit');
						conn.release(); 
						if(err){
							logger.error(err);
							reject(err);
							return;
						}
						resolve(res);
					});
				}catch(e){
					conn.rollback(err=>{
						logger.info('rollback');
						conn.release();
						if(err){
							logger.error(err);
							reject(err);
							return;
						}
						reject(e);
					});
				}
			});
		},async err=>{
			await cb(err,null);
		});
	});
	
};
//批量插入。这里其实还可以优化一下,可以支持设置批量数,数据太多分多次批量操作插入。
db.insert = async function({conn,tablename,columns,values}){
	let {sql,params} = genInsertSql({tablename,columns,values});
	//logger.info(sql);
	//logger.info(JSON.stringify(params,null,2));
	try{
		let rows = await query({conn,sql,params});
		return rows;
	}catch(e){
		if(e.code == 'PARSER_JS_PRECISION_RANGE_EXCEEDED'){
			logger.error(JSON.stringify(e));
		}else{
			throw e;
		}
	}
	return;
};
//生成批量插入的sql  
function genInsertSql({tablename,columns,values}){
	const columnnames = columns.join(',');
	const sql = `insert into ${tablename}(${columnnames})values `;
	const params = [];
	const placeholders = [];
	values.forEach((item,index)=>{
		let holder = [];
		for(let i=0;i<columns.length;i++){
			holder.push('?');
			params.push(item[columns[i]]);
		}
		placeholders.push('('+holder.join(',')+')');
	});
	const ret = {
		sql:sql+placeholders.join(',')+';',
		params:params
	};
	//logger.info(ret);
	return ret;
}
//查询操作
async function query({conn,sql,params}){
	return new Promise((resolve,reject)=>{
		conn.query(sql,params,(err,res)=>{
			if(err){
				reject(err);
				return;
			}
			//logger.info(res);
			//logger.info(JSON.stringify(res,null,2));
			resolve(res);
		});
	});
}
db.query = query;
db.pool = pool;
module.exports = db;
//query方法和insert方法,其实还可以优化。可以去掉conn参数,每次获取连接的时候,生成一个标识,将连接保存到一个对象,然后查询和插入的时候,根据标识从中获取它,释放连接的时候,根据标识将其从对象中删除。

log.js

//npm install winston
//npm install uuid
const uuid = require('uuid');
const winston = require('winston');

const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format;

const myFormat = printf(info => {
  return `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`;
});

function newLogger(loglabel = uuid.v4()){
    const logger = createLogger({
      format: combine(
        label({ label: loglabel}),
        timestamp(),
        myFormat
      ),
      transports: [new transports.Console()]
    });
    return logger;
}
const defaultLogger = newLogger();//提供一个默认的日志。该日志是全局唯一的,任何地方使用它,都是相同的日志id。
const log = {
	newLogger,
	defaultLogger
};
/*
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    //
    // - Write to all logs with level `info` and below to `combined.log` 
    // - Write all logs error (and below) to `error.log`.
    //
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});
*/
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
// 
/*
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}*/

module.exports = log;

tiny.js
短网址模块
一般短网址实现有两个思路

  • tinyUrl=md5(longUrl).substr(0,16)
    这种方法,需要解决tinyUrl重复的问题。
  • 将自增id转为62进制对应的字符串。tinyUrl=genTinyUrl(id)
    这种方法,自定义tinyUrl会使自增id突然跳到一个比较大的值,中间的id被浪费,而且不可预估浪费多少。

我们的实现结合上面的两个方案,通过记录自定义tinyUrl对自增id的占用关系,解决了tinyUrl重复的问题和id浪费的问题。
保存自定义tinyUrl和longUrl的关联的时候,我们也使用自增id,而不是设置id为parseId(tinyUrl)。

提供两个接口

  • longUrl关联到自动生成的tinyUrl
    如果longUrl已关联过tinyUrl,则直接返回。
    否则获取下一个自增id,转成tinyUrl。
    如果tinyUrl已被id’占用,假设tinyUrl’=genTinyUrl(id’),则将longUrl关联到tinyUrl’。
    否则关联longUrl到tinyUrl。
  • longUrl关联到自定义的tinyUrl
    如果tinyUrl已关联到一个longUrl’,并且longUrl’!=longUrl则提示用户该tinyUrl已被使用。
    如果tinyUrl已关联到一个longUrl’,并且longUrl’==longUrl则返回关联成功。
    否则,获取下一个自增id,假设为id’,保存tinyUrl和longUrl的关联。并将tinyUrl对id’的占用,记录到另一张表。

假设
tinyUrl1=genTinyUrl(id1)
tinyUrl2=genTinyUrl(id2)
tinyUrl3=genTinyUrl(id3)

如果出现tinyUrl1占用id2,tinyUrl2占用id3的情况,则将tinyUrl1占用的id更新为id3,即现在tinyUrl1占用了id3,tinyUrl2一借一贷,逻辑上已经不占用别人的id,它对应的id也不被其他tinyUrl占用。

const db = require('./db');
const log = require('./log');
const logger = log.defaultLogger;
//作为短网址的字符(一共62个,我们的短网址最多6位,所以可以生成62的6次方个短网址)
const chars = `0123456789abcdegfhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`.split(new RegExp('','igm'));

//模拟业务
//给定长网址,生成短网址。这里还有工作没完成,只是生成了短网址的一部分,没有处理网址协议部分
async function generateTinyUrl({longUrl}){
	if(!longUrl){
		throw new Error(`invalid longUrl => ${longUrl}`);
	}
	return await db.doInTx(async (err,conn)=>{
		if(err){
			logger.error(err);
			return;
		}
		//如果长网址已经存在,则直接返回
		let req = {
			conn,
			sql:'select * from t_tiny_url where long_url = ?',
			params:[longUrl]
		};
		let rows = await db.query(req);
		if(rows.length > 0){
			logger.info(`this longUrl is exsits => ` + JSON.stringify(rows));
			return rows;
		}
		//获取下一个自增id
		let tablename = 't_tiny_url';
		let columns = ['long_url'];
		let values = [{
			long_url:longUrl
		}];
		rows = await db.insert({conn,tablename,columns,values});
		//logger.info(rows);
		let id = rows.insertId;
		//计算短网址
		//logger.info(id);
		let tinyUrl = toTinyUrl(id);
		//判断短网址是否存在
		req.sql = 'select * from t_tiny_url where tiny_url = ?';
		req.params = [tinyUrl];
		rows = await db.query(req);
		//如果短网址已经存在,就使用占用它id的 短网址对于的id 作为生成此短网址的数值。意思是谁占了我的坑,我就要用它的坑。如果它的坑被别人占了,我就把它们作为一个整体,当成它。这样只要坑被占了,就一定能找到一个未被占用的坑。
		if(rows.length > 0){
			//寻找占用了我的坑的家伙
			req.sql = 'select * from t_tiny_id_provide where tiny_url = ?';
			req.params = [tinyUrl];
			rows = await db.query(req);
			const _tinyUrl = tinyUrl;
			const _id = rows[0].provide_id;
			tinyUrl = toTinyUrl(_id);
			logger.info(`id=>${id},tinyUrl=>${tinyUrl}`);
			//req.sql = 'update t_tiny_id_provide set provide_id = ? where tiny_url = ?';
			//req.params = [id,_tinyUrl];
            //别人占了我的坑,我用了它(它们)的坑,扯平了,这些信息不需要了。
			req.sql = 'delete from t_tiny_id_provide where provide_id = ?';
			req.params = [_id];
			rows = await db.query(req);
		}
		//logger.info(tinyUrl);
		//logger.info(db);
		//保存短网址
		req.sql = 'update t_tiny_url set tiny_url = ? where id = ?';
		req.params = [tinyUrl,id];
		rows = await db.query(req);
		return rows;
	});
}
//自定义短码
async function customTinyUrl({tinyUrl,longUrl}){
	if(!longUrl){
		throw new Error(`invalid longUrl => ${longUrl}`);
	}
	if(!tinyUrl || !/[0-9a-zA-Z]{1,6}/.test(tinyUrl)){
		throw new Error(`invalid tinyUrl => ${tinyUrl}`);
	}
	return await db.doInTx(async (err,conn)=>{
		if(err){
			logger.error(err);
			return;
		}
		//如果短网址或者长网址已经存在,就直接返回
		let req = {
			conn,
			sql:'select * from t_tiny_url where tiny_url = ? or long_url = ?',
			params:[tinyUrl,longUrl]
		};
		let rows = await db.query(req);
		if(rows.length > 0){
			logger.info(`this tinyUrl/longUrl is exsits => ` + JSON.stringify(rows));
			return rows;
		}
		let tablename = 't_tiny_url';
		let columns = ['tiny_url','long_url'];
		let values = [{
			long_url:longUrl,tiny_url:tinyUrl
		}];
		//获取下一个自增id
		rows = await db.insert({conn,tablename,columns,values});
		//logger.info(rows);
		let id = rows.insertId;
		//logger.info(id);
		//计算短网址对应的数值
		let provideId = fromTinyUrl(tinyUrl);
		//如果我的坑被别人占了,那就是声明别人占了你的坑。我不欠谁的坑,谁也不欠我的坑。
		req.sql = 'update t_tiny_id_provide set provide_id = ? where provide_id = ?';
		req.params = [id,provideId];
		logger.info(req.params);
		rows = await db.query(req);
		logger.info('=>'+JSON.stringify(rows));
		//如果我的坑没被别人占,那就声明一下,我占了某人的坑。
		if(rows.affectedRows == 0){
			tablename = 't_tiny_id_provide';
			columns = ['tiny_url','provide_id'];
			values = [{
				tiny_url:tinyUrl,
				provide_id:id
			}];
			//
			rows = await db.insert({conn,tablename,columns,values});
		}
		return rows;
	});
}
//根据数值计算短网址,当然这里并没有处理协议部分
function toTinyUrl(id){
	let n = id;
	let code = '';
	let size = chars.length;
	while(n > 0){
		code += chars[n%size];
		n = 0|(n/size);
	}
	return [...code].reverse().join('');
}
//根据短网址,计算数值
function fromTinyUrl(tinyUrl){
	const chs = [...tinyUrl];
	//logger.info(`chs=>${chs}`);
	//logger.info(Array.isArray(chs));
	let res = chs.map(ch=>chars.indexOf(ch));
	logger.info(`res=>${res}`);
	res = res.reduce((prev,curr)=>{
		return prev*chars.length+curr;
	},0);
	logger.info(`res=>${res}`);
	return res;
}
//测试
let test = async function(){
	//await generateTinyUrl('http://www.test.com/4');
	//await generateTinyUrl('http://www.test.com/1');
	//await generateTinyUrl('http://www.test.com/2');
	//await generateTinyUrl('http://www.test.com/x');

	await customTinyUrl('4','http://www.test.com/1');
	await customTinyUrl('1','http://www.test.com/2');
	await customTinyUrl('2','http://www.test.com/3');
	//await customTinyUrl('1','http://www.test.com/4');
	await generateTinyUrl('http://www.test.com/4');

}
test = async function(){
	await customTinyUrl('5','http://www.test.com/1');
	await customTinyUrl('1','http://www.test.com/2');
	await customTinyUrl('2','http://www.test.com/3');
	//await customTinyUrl('1','http://www.test.com/4');
	await generateTinyUrl('http://www.test.com/4');
	await generateTinyUrl('http://www.test.com/5');

}
test = function(){
	const id = fromTinyUrl('abc');
	logger.info(id);
}
//test();
/*
test().then(data=>{
	logger.info(data);
},err=>{
	logger.error(err);
});*/
module.exports = {
	generateTinyUrl,customTinyUrl
};

init.js
初始化模块
用于创建表

const db = require('./db');
const log = require('./log');
const logger = log.defaultLogger;

async function creatTables(err,conn){
	if(err){
		logger.error(err);
		return;
	}
	//清除数据
	let sqls = [
		'drop table if exists t_tiny_url;',
		'drop table if exists t_tiny_id_provide;',

		`create table if not exists t_tiny_url(
			id bigint primary key auto_increment,
			tiny_url varchar(32) unique,
			long_url varchar(767) unique,
			key t_tiny_url_idx_tiny_url(tiny_url),
			key t_tiny_url_idx_long_url(long_url)
		);`,
		`create table if not exists t_tiny_id_provide(
			tiny_url varchar(32) unique,
			provide_id bigint unique,
			key t_tiny_id_provide_idx_tiny_url(tiny_url),
			key t_tiny_id_provide_idx_provide_id(provide_id)
		);`
	];
	//logger.info(sqls);
	let params = [];
	let defers = [];
	sqls.forEach(sql=>{
		defers.push(db.query({conn,sql,params}));
	});
	await Promise.all(defers);
}
let test = async function(){
	await db.doInTx(creatTables);
}
test().then(data=>{
	logger.info(data);
	db.pool.end();
},err=>{
	logger.error(err);
	db.pool.end();
});

server.js
http服务器模块

const http = require('http');
const url = require('url');
const querystring = require('querystring');

const tiny = require('./tiny');
// 创建一个 HTTP 服务器
const server = http.createServer( async(req, res) => {
	try{
		const reqUrl = url.parse(req.url);
		//这里没有用到框架,因为我们的需求非常简单
		const handler = {
			'/gen-tiny-url':'generateTinyUrl',
			'/cust-tiny-url':'customTinyUrl'
		};
		const pathname = reqUrl.pathname;
		//TODO 添加一个处理,访问短网址的时候,重定向到长网址
		//TODO 添加一个处理,查询短网址对应的长网址
		//TODO 添加一个处理,查询长网址对应的短网址,如果不存在,返回空
		if(!handler[pathname]){
			res.writeHead(404, { 'Content-Type': 'text/plain' });
			res.end('404');
			return;
		};
		const arg = querystring.parse(reqUrl.query);
		console.info(arg);
		res.writeHead(200, { 'Content-Type': 'text/plain' });
		const rows = await tiny[handler[pathname]](arg);
		//console.info(rows);
		res.write(JSON.stringify(rows));
		res.end();
	}catch(e){
		console.error(e);
		res.writeHead(500, { 'Content-Type': 'text/plain' });
		res.end('500');
	}
	
});
server.on('clientError', (err, socket) => {
	socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
// 服务器正在运行
server.listen(1337);

package.json

{
  "name": "tinyurl",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "mysql": "^2.16.0",
    "uuid": "^3.3.2",
    "winston": "^3.0.0"
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值