├── src ├── main │ ├── resources │ │ ├── templates │ │ │ └── welcome.html │ │ ├── application.properties │ │ ├── mappers │ │ │ └── ShortUrlMapper.xml │ │ ├── ehcache.xml │ │ └── logback.xml │ └── java │ │ └── com │ │ └── leaf │ │ ├── response │ │ ├── ShortUrlVO.java │ │ └── Response.java │ │ ├── request │ │ └── GenerateShortUrlRequest.java │ │ ├── mapper │ │ └── ShortUrlMapper.java │ │ ├── StartApplication.java │ │ ├── config │ │ ├── SpringContextHolder.java │ │ └── CacheConfiguration.java │ │ ├── listener │ │ └── ServletStartupContextListener.java │ │ ├── service │ │ ├── ShortUrlService.java │ │ └── impl │ │ │ └── ShortUrlServiceImpl.java │ │ ├── domain │ │ └── ShortUrl.java │ │ ├── controller │ │ ├── RedirectController.java │ │ └── IndexController.java │ │ ├── utils │ │ └── MathUtils.java │ │ ├── filter │ │ └── ShortUrlBloomFilter.java │ │ └── manager │ │ └── ShortUrlManager.java └── test │ ├── java │ └── com │ │ └── leaf │ │ ├── HashServiceTest.java │ │ ├── manager │ │ └── ShortUrlManagerTest.java │ │ └── mapper │ │ └── ShortUrlMapperTest.java │ └── resources │ └── mybatisTestConfiguration │ └── ShortUrlMapperTestConfiguration.xml ├── .gitignore ├── README.md ├── docs ├── sql │ └── short_url.sql └── lua │ └── cache.lua └── pom.xml /src/main/resources/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Insert title here 6 | 7 | 8 | Hello Thymeleaf. 9 | 10 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/response/ShortUrlVO.java: -------------------------------------------------------------------------------- 1 | package com.leaf.response; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * @author yefan 8 | */ 9 | @Data 10 | @AllArgsConstructor 11 | public class ShortUrlVO { 12 | private String hashValue; 13 | 14 | private String url; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/request/GenerateShortUrlRequest.java: -------------------------------------------------------------------------------- 1 | package com.leaf.request; 2 | 3 | 4 | import lombok.Data; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | @Data 9 | public class GenerateShortUrlRequest { 10 | /** 11 | * 要生成的短链接地址 12 | */ 13 | @NotNull 14 | private String url; 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/mapper/ShortUrlMapper.java: -------------------------------------------------------------------------------- 1 | package com.leaf.mapper; 2 | import java.util.List; 3 | 4 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 5 | import com.leaf.domain.ShortUrl; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | public interface ShortUrlMapper extends BaseMapper { 9 | 10 | ShortUrl findByHashValue(@Param("hashValue")String hashValue); 11 | 12 | List findAllHashValue(); 13 | 14 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | domain=http://localhost:${server.port}/r/ 3 | 4 | spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver 5 | spring.datasource.url=jdbc:mysql://localhost:3306/short_url?useUnicode=true&characterEncoding=utf-8 6 | spring.datasource.username = root 7 | spring.datasource.password = yefan@813 8 | 9 | #myabtis confi 10 | mybatis-plus.mapper-locations=classpath*:mappers/*.xml 11 | 12 | url.protocol=https -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.nar 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | *.iml 27 | !/.idea/ 28 | *.idea/* 29 | *target* 30 | -------------------------------------------------------------------------------- /src/test/java/com/leaf/HashServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.leaf; 2 | 3 | import com.leaf.utils.MathUtils; 4 | import org.apache.commons.codec.digest.MurmurHash3; 5 | import org.junit.Test; 6 | 7 | public class HashServiceTest { 8 | 9 | @Test 10 | public void generateHash() { 11 | String url = "https://yefan813.github.io/"; 12 | long hash = MurmurHash3.hash32x86(url.getBytes()); 13 | System.out.println(hash); 14 | System.out.println(MathUtils._10_to_62(hash)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 记录如何一步一步实现高性能的短链接服务 2 | 3 | 4 | | 所用技术 | 功能 | 完成度 | 5 | |:------------|:---------------|:------| 6 | | Spring Boot | 框架 | 已完成 | 7 | | MySQL | 数据存储 | 已完成 | 8 | | Redis | 二级缓存 | X | 9 | | OpenResty | 代理 + 一级缓存 | 已完成 | 10 | | EhCache | 三级缓存 | 已完成 | 11 | | Zookeeper | 分布式锁 | X | 12 | | 布隆过滤器 | 判断hash是否存在 | 已完成 | 13 | 14 | # 文章地址[传送门](https://yefan813.github.io/2020/03/23/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%9F%AD%E9%93%BE%E6%8E%A5%E6%9C%8D%E5%8A%A1/) -------------------------------------------------------------------------------- /src/main/java/com/leaf/StartApplication.java: -------------------------------------------------------------------------------- 1 | package com.leaf; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.web.servlet.ServletComponentScan; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | 9 | @SpringBootApplication 10 | @MapperScan("com.leaf.mapper") 11 | @EnableCaching 12 | @ServletComponentScan 13 | public class StartApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(StartApplication.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/config/SpringContextHolder.java: -------------------------------------------------------------------------------- 1 | package com.leaf.config; 2 | 3 | 4 | import org.springframework.beans.BeansException; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.context.ApplicationContextAware; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class SpringContextHolder implements ApplicationContextAware { 11 | 12 | private static ApplicationContext ctx = null; 13 | 14 | @Override 15 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 16 | ctx = applicationContext; 17 | } 18 | 19 | public static ApplicationContext getSpringContext() { 20 | return ctx; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/leaf/listener/ServletStartupContextListener.java: -------------------------------------------------------------------------------- 1 | package com.leaf.listener; 2 | 3 | import com.leaf.filter.ShortUrlBloomFilter; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import javax.servlet.ServletContextEvent; 7 | import javax.servlet.ServletContextListener; 8 | import javax.servlet.annotation.WebListener; 9 | 10 | @Slf4j 11 | @WebListener 12 | public class ServletStartupContextListener implements ServletContextListener { 13 | 14 | @Override 15 | public void contextInitialized(ServletContextEvent sce) { 16 | log.info("=============容器启动==============="); 17 | 18 | } 19 | 20 | @Override 21 | public void contextDestroyed(ServletContextEvent sce) { 22 | log.info("=============容器销毁==============="); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/leaf/manager/ShortUrlManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.leaf.manager; 2 | 3 | import com.leaf.domain.ShortUrl; 4 | import com.leaf.response.Response; 5 | import com.leaf.response.ShortUrlVO; 6 | import com.leaf.service.ShortUrlService; 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | 13 | import java.util.Calendar; 14 | import java.util.GregorianCalendar; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertTrue; 18 | import static org.mockito.Mockito.when; 19 | import static org.mockito.MockitoAnnotations.initMocks; 20 | 21 | public class ShortUrlManagerTest { 22 | 23 | @Test 24 | public void test_isStartWithHttpOrHttps() { 25 | boolean start = ShortUrlManager.isStartWithHttpOrHttps("http://www.yefan813.github.io"); 26 | Assert.assertTrue(start); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/service/ShortUrlService.java: -------------------------------------------------------------------------------- 1 | package com.leaf.service; 2 | 3 | import com.leaf.domain.ShortUrl; 4 | 5 | import java.util.List; 6 | 7 | public interface ShortUrlService { 8 | /** 9 | * 将长链接生成短链接 10 | * 11 | * @param url 12 | * @return 13 | */ 14 | String generateShortUrl(String url); 15 | 16 | /** 17 | * 保存到数据库 18 | * @param shortUrl 19 | * @return 20 | */ 21 | int saveShortUrl(ShortUrl shortUrl); 22 | 23 | 24 | /** 25 | * 从数据库查询 26 | * @param hashValue 27 | * @return 28 | */ 29 | ShortUrl findByHashValueFromDB(String hashValue); 30 | 31 | 32 | /** 33 | * 从本地缓存查询 34 | * @param hashValue 35 | * @return 36 | */ 37 | ShortUrl findByHashValueFromLocalCache(String hashValue); 38 | 39 | /** 40 | * 查询DB所有的HashValue 41 | * @return 42 | */ 43 | List findAllHashValue(); 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/test/resources/mybatisTestConfiguration/ShortUrlMapperTestConfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/sql/short_url.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 50720 7 | Source Host : localhost 8 | Source Database : short_url 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50720 12 | File Encoding : utf-8 13 | 14 | Date: 03/24/2020 17:35:46 PM 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for `tb_short_url` 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `tb_short_url`; 24 | CREATE TABLE `tb_short_url` ( 25 | `id` bigint(11) NOT NULL AUTO_INCREMENT, 26 | `hash_value` varchar(32) NOT NULL, 27 | `url` varchar(2048) NOT NULL, 28 | `created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 29 | `updated` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', 30 | `yn` tinyint(2) DEFAULT '0' COMMENT '是否删除 1 删除 0 未删除', 31 | PRIMARY KEY (`id`), 32 | UNIQUE KEY `unique` (`hash_value`) USING BTREE 33 | ) ENGINE=InnoDB AUTO_INCREMENT=325781 DEFAULT CHARSET=utf8; 34 | 35 | SET FOREIGN_KEY_CHECKS = 1; 36 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/config/CacheConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.leaf.config; 2 | 3 | import org.springframework.cache.annotation.EnableCaching; 4 | import org.springframework.cache.ehcache.EhCacheCacheManager; 5 | import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.io.ClassPathResource; 9 | 10 | /** 11 | * 缓存配置管理类 12 | * 13 | * @author yefan 14 | */ 15 | @Configuration 16 | @EnableCaching 17 | public class CacheConfiguration { 18 | 19 | @Bean 20 | public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() { 21 | EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean(); 22 | ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml")); 23 | ehCacheManagerFactoryBean.setShared(true); 24 | return ehCacheManagerFactoryBean; 25 | } 26 | 27 | @Bean 28 | public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) { 29 | return new EhCacheCacheManager(ehCacheManagerFactoryBean.getObject()); 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/test/java/com/leaf/mapper/ShortUrlMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.leaf.mapper; 2 | 3 | import com.leaf.domain.ShortUrl; 4 | import org.apache.ibatis.session.SqlSessionFactory; 5 | import org.apache.ibatis.session.SqlSessionFactoryBuilder; 6 | import org.junit.BeforeClass; 7 | import org.junit.Test; 8 | 9 | import java.io.FileNotFoundException; 10 | 11 | public class ShortUrlMapperTest { 12 | private static ShortUrlMapper mapper; 13 | 14 | @BeforeClass 15 | public static void setUpMybatisDatabase() { 16 | SqlSessionFactory builder = new SqlSessionFactoryBuilder().build(ShortUrlMapperTest.class.getClassLoader().getResourceAsStream("mybatisTestConfiguration/ShortUrlMapperTestConfiguration.xml")); 17 | //you can use builder.openSession(false) to not commit to database 18 | mapper = builder.getConfiguration().getMapper(ShortUrlMapper.class, builder.openSession(true)); 19 | } 20 | 21 | @Test 22 | public void testFindByHashValue() throws FileNotFoundException { 23 | mapper.findByHashValue("123"); 24 | } 25 | 26 | @Test 27 | public void testSaveShortUrl() { 28 | ShortUrl shortUrl = new ShortUrl(); 29 | shortUrl.setHashValue("111"); 30 | shortUrl.setUrl("wwww.baidu.com"); 31 | mapper.insert(shortUrl); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/mappers/ShortUrlMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | id, hash_value, url, created, updated, yn 18 | 19 | 20 | 21 | 22 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/response/Response.java: -------------------------------------------------------------------------------- 1 | package com.leaf.response; 2 | 3 | import lombok.Data; 4 | 5 | public class Response { 6 | public static final String SUCCESS = "Success"; 7 | public static final String FAILED = "Failed"; 8 | 9 | 10 | private String code; 11 | private String msg; 12 | private T data; 13 | 14 | public Response(T data) { 15 | this.code = Response.SUCCESS; 16 | this.data = data; 17 | } 18 | 19 | public Response(String code, String msg, T data) { 20 | this.code = code; 21 | this.msg = msg; 22 | this.data = data; 23 | } 24 | 25 | public Response(String code, String msg) { 26 | this.code = code; 27 | this.msg = msg; 28 | } 29 | 30 | public static Response success(Object data){ 31 | return new Response(data); 32 | } 33 | 34 | public static Response failed(String code, String msg){ 35 | return new Response(code, msg); 36 | } 37 | 38 | public String getCode() { 39 | return code; 40 | } 41 | 42 | public void setCode(String code) { 43 | this.code = code; 44 | } 45 | 46 | public String getMsg() { 47 | return msg; 48 | } 49 | 50 | public void setMsg(String msg) { 51 | this.msg = msg; 52 | } 53 | 54 | public T getData() { 55 | return data; 56 | } 57 | 58 | public void setData(T data) { 59 | this.data = data; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/domain/ShortUrl.java: -------------------------------------------------------------------------------- 1 | package com.leaf.domain; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import java.util.Date; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @TableName(value = "tb_short_url") 16 | public class ShortUrl { 17 | @TableId(value = "id", type = IdType.AUTO) 18 | private Long id; 19 | 20 | @TableField(value = "hash_value") 21 | private String hashValue; 22 | 23 | @TableField(value = "url") 24 | private String url; 25 | 26 | /** 27 | * 创建时间 28 | */ 29 | @TableField(value = "created") 30 | private Date created; 31 | 32 | /** 33 | * 更新时间 34 | */ 35 | @TableField(value = "updated") 36 | private Date updated; 37 | 38 | /** 39 | * 是否删除 1 删除 0 未删除 40 | */ 41 | @TableField(value = "yn") 42 | private Byte yn; 43 | 44 | public static final String COL_ID = "id"; 45 | 46 | public static final String COL_HASH_VALUE = "hash_value"; 47 | 48 | public static final String COL_URL = "url"; 49 | 50 | public static final String COL_CREATED = "created"; 51 | 52 | public static final String COL_UPDATED = "updated"; 53 | 54 | public static final String COL_YN = "yn"; 55 | } -------------------------------------------------------------------------------- /src/main/java/com/leaf/controller/RedirectController.java: -------------------------------------------------------------------------------- 1 | package com.leaf.controller; 2 | 3 | import com.leaf.manager.ShortUrlManager; 4 | import com.leaf.response.ShortUrlVO; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | 17 | /** 18 | * @author yefan 19 | */ 20 | @RestController 21 | @Slf4j 22 | @RequestMapping("/") 23 | public class RedirectController { 24 | @Autowired 25 | private ShortUrlManager shortUrlManager; 26 | 27 | @GetMapping(value = "/r/{hash}") 28 | public String redirect(HttpServletRequest request, HttpServletResponse response, @PathVariable("hash") String hash) throws IOException { 29 | log.info("====================请求地址:[{}]===============" , request.getRequestURL()); 30 | ShortUrlVO shortUrlVO = shortUrlManager.getRealUrlByHash(hash); 31 | if(null == shortUrlVO){ 32 | log.error("短链接不存在,hash[{}]", hash); 33 | return "短链接不存在!"; 34 | } 35 | response.sendRedirect(shortUrlVO.getUrl()); 36 | return ""; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/lua/cache.lua: -------------------------------------------------------------------------------- 1 | 2 | --需要在nginx.conf 配置 3 | -- lua_shared_dict my_cache 128m; 4 | -- content_by_lua_file /xxxx/xxx/cache.lua 5 | -- lua_package_path "/usr/local/opt/openresty/lualib/?.lua;;"; 6 | -- lua_package_cpath "/usr/local/opt/openresty/lualib/?.so;;"; 7 | 8 | 9 | --需要安装 在路径下/usr/local/opt/openresty/lualib 10 | -- wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http_headers.lua 11 | --wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http.lua 12 | 13 | 14 | local uri = ngx.var.uri; 15 | local hashValue = string.gsub(uri,'/r/',''); 16 | local cache_ngx = ngx.shared.my_cache 17 | 18 | local hashKey = "short_hash_"..hashValue 19 | local shortUrl = cache_ngx:get(hashKey) 20 | 21 | if shortUrl == "" or shortUrl == nil then 22 | local http = require("resty.http") 23 | local httpc = http.new() 24 | 25 | local resp = httpc:request_uri("http://192.168.1.3:8080",{ 26 | method = "GET", 27 | path = "/url/getByHash/"..hashValue 28 | }) 29 | 30 | ngx.log(ngx.ERR,"============resp.body==========:", resp.body) 31 | 32 | local cjson = require("cjson") 33 | local resultJson = cjson.decode(resp.body) 34 | 35 | if resultJson.code == -1 then 36 | ngx.say('url not exist!') 37 | return 38 | end 39 | 40 | if resultJson.code == "Success" then 41 | shortUrl = resultJson.data 42 | cache_ngx:set(hashKey, shortUrl, 10 * 60) 43 | ngx.redirect(shortUrl, 302) 44 | return 45 | end 46 | 47 | end 48 | 49 | ngx.redirect(shortUrl, 302) 50 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.leaf.controller; 2 | 3 | import com.leaf.manager.ShortUrlManager; 4 | import com.leaf.request.GenerateShortUrlRequest; 5 | import com.leaf.response.Response; 6 | import com.leaf.response.ShortUrlVO; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import javax.validation.Valid; 13 | 14 | /** 15 | * 欢迎页 16 | * @author yefan 17 | */ 18 | @Controller 19 | @RequestMapping("/url") 20 | @Slf4j 21 | public class IndexController { 22 | 23 | @Autowired 24 | private ShortUrlManager shortUrlManager; 25 | 26 | @PostMapping("/generateShortUrl") 27 | @ResponseBody 28 | public Response generateShortUrl(@RequestBody @Valid GenerateShortUrlRequest request) { 29 | if(!shortUrlManager.isValidUrl(request.getUrl())){ 30 | log.error("无效的url:[{}]",request.getUrl()); 31 | return Response.failed("-1", "无效的url"); 32 | } 33 | return shortUrlManager.generateShortUrl(request.getUrl()); 34 | } 35 | 36 | @GetMapping("/getByHash/{hashValue}") 37 | @ResponseBody 38 | public Response getByHash(@PathVariable("hashValue") String hash) { 39 | log.info("====================请求hash:[{}]===============" , hash); 40 | ShortUrlVO shortUrlVO = shortUrlManager.getRealUrlByHash(hash); 41 | if(null == shortUrlVO){ 42 | log.error("短链接不存在,hash[{}]", hash); 43 | return Response.failed("-1", "短链接不存在"); 44 | } 45 | return Response.success(shortUrlVO.getUrl()); 46 | } 47 | 48 | 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/service/impl/ShortUrlServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.leaf.service.impl; 2 | 3 | import com.leaf.domain.ShortUrl; 4 | import com.leaf.mapper.ShortUrlMapper; 5 | import com.leaf.service.ShortUrlService; 6 | import com.leaf.utils.MathUtils; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.commons.codec.digest.MurmurHash3; 9 | import org.apache.commons.lang.StringUtils; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.cache.annotation.Cacheable; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.util.List; 15 | 16 | @Service 17 | @Slf4j 18 | public class ShortUrlServiceImpl implements ShortUrlService { 19 | 20 | @Autowired 21 | private ShortUrlMapper shortUrlMapper; 22 | 23 | @Override 24 | public String generateShortUrl(String url) { 25 | if (StringUtils.isBlank(url)) { 26 | throw new RuntimeException("请输入正确url"); 27 | } 28 | long hash = MurmurHash3.hash32x86(url.getBytes()); 29 | return MathUtils._10_to_62(hash); 30 | } 31 | 32 | @Override 33 | public int saveShortUrl(ShortUrl shortUrl) { 34 | return shortUrlMapper.insert(shortUrl); 35 | } 36 | 37 | @Override 38 | public ShortUrl findByHashValueFromDB(String hashValue) { 39 | return shortUrlMapper.findByHashValue(hashValue); 40 | } 41 | 42 | @Override 43 | @Cacheable(cacheNames = "shortUrl", key= "#hashValue") 44 | public ShortUrl findByHashValueFromLocalCache(String hashValue) { 45 | log.info("==================从本地缓存中读取数据:[{}]==================", hashValue); 46 | return shortUrlMapper.findByHashValue(hashValue); 47 | } 48 | 49 | @Override 50 | public List findAllHashValue() { 51 | return shortUrlMapper.findAllHashValue(); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/utils/MathUtils.java: -------------------------------------------------------------------------------- 1 | package com.leaf.utils; 2 | 3 | import java.util.Stack; 4 | 5 | public class MathUtils { 6 | private static char[] charSet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); 7 | 8 | 9 | /** 10 | * 将10进制转化为62进制 11 | * 12 | * @param number 13 | * @return 14 | */ 15 | public static String _10_to_62(long number) { 16 | Long rest = Math.abs(number); 17 | Stack stack = new Stack(); 18 | StringBuilder result = new StringBuilder(0); 19 | while (rest != 0) { 20 | stack.add(charSet[new Long((rest - (rest / 62) * 62)).intValue()]); 21 | rest = rest / 62; 22 | } 23 | for (; !stack.isEmpty(); ) { 24 | result.append(stack.pop()); 25 | } 26 | 27 | return result.toString(); 28 | 29 | } 30 | 31 | 32 | /** 33 | * 将62进制转换成10进制数 34 | * 35 | * @param ident62 36 | * @return 37 | */ 38 | private static String convertBase62ToDecimal(String ident62) { 39 | int decimal = 0; 40 | int base = 62; 41 | int keisu = 0; 42 | int cnt = 0; 43 | 44 | byte ident[] = ident62.getBytes(); 45 | for (int i = ident.length - 1; i >= 0; i--) { 46 | int num = 0; 47 | if (ident[i] > 48 && ident[i] <= 57) { 48 | num = ident[i] - 48; 49 | } else if (ident[i] >= 65 && ident[i] <= 90) { 50 | num = ident[i] - 65 + 10; 51 | } else if (ident[i] >= 97 && ident[i] <= 122) { 52 | num = ident[i] - 97 + 10 + 26; 53 | } 54 | keisu = (int) java.lang.Math.pow((double) base, (double) cnt); 55 | decimal += num * keisu; 56 | cnt++; 57 | } 58 | return String.format("%08d", decimal); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | ${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log 17 | 18 | 30 19 | 20 | 21 | 22 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 23 | 24 | 25 | 26 | 10MB 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/filter/ShortUrlBloomFilter.java: -------------------------------------------------------------------------------- 1 | package com.leaf.filter; 2 | 3 | import com.google.common.hash.BloomFilter; 4 | import com.google.common.hash.Funnels; 5 | import com.leaf.service.ShortUrlService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.beans.factory.InitializingBean; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.ApplicationContextAware; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.nio.charset.Charset; 15 | import java.util.List; 16 | 17 | /** 18 | * 这里使用BloomFilter,每次生成短链接从 BloomFilter 判断是否存在, 19 | * 不存在就插入数据库,减少一次查询数据库的操作 20 | * 1,这里虽然可以减少数据,但是如果数据量大,容器启动时从数据库查找大量数据加载到BloomFilter 21 | * 可能会造成内存问题 22 | * 23 | * @author yefan 24 | */ 25 | @Service 26 | @Slf4j 27 | public class ShortUrlBloomFilter implements InitializingBean{ 28 | private static BloomFilter bloomFilter = BloomFilter.create( 29 | Funnels.stringFunnel(Charset.defaultCharset()), 1000000); 30 | 31 | @Autowired 32 | private ShortUrlService shortUrlService; 33 | 34 | public boolean mightContain(String hashValue) { 35 | return bloomFilter.mightContain(hashValue); 36 | } 37 | 38 | public void put(String hashValue) { 39 | bloomFilter.put(hashValue); 40 | } 41 | 42 | @Override 43 | public void afterPropertiesSet() { 44 | log.info("===============初始化BloomFilter......=============="); 45 | long startTime = System.currentTimeMillis(); 46 | long count = 0; 47 | //从数据加载数据到 BloomFilter 48 | List allHashValue = shortUrlService.findAllHashValue(); 49 | for (String hash : allHashValue) { 50 | bloomFilter.put(hash); 51 | } 52 | count = allHashValue.size(); 53 | long costTime = System.currentTimeMillis() - startTime; 54 | log.info("===============初始化BloomFilter完成,costTime:[{}],Count:[{}]==============", costTime, count); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.leaf 8 | short-url 9 | 1.0-SNAPSHOT 10 | 11 | short url 12 | 短连接实战项目 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-parent 18 | 2.2.2.RELEASE 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | 36 | mysql 37 | mysql-connector-java 38 | 39 | 40 | 41 | com.baomidou 42 | mybatis-plus-boot-starter 43 | 3.2.0 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-redis 49 | 1.3.2.RELEASE 50 | 51 | 52 | 53 | 54 | commons-lang 55 | commons-lang 56 | 2.6 57 | 58 | 59 | 60 | com.alibaba 61 | fastjson 62 | 1.2.62 63 | 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | 1.16.12 69 | 70 | 71 | 72 | 73 | commons-codec 74 | commons-codec 75 | 1.14 76 | 77 | 78 | 79 | junit 80 | junit 81 | 4.13 82 | test 83 | 84 | 85 | org.mockito 86 | mockito-all 87 | 1.10.18 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-starter-cache 93 | 94 | 95 | 96 | 97 | net.sf.ehcache 98 | ehcache 99 | 2.8.3 100 | 101 | 102 | 103 | com.google.guava 104 | guava 105 | 25.1-jre 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-maven-plugin 117 | 118 | true 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/java/com/leaf/manager/ShortUrlManager.java: -------------------------------------------------------------------------------- 1 | package com.leaf.manager; 2 | 3 | import com.leaf.domain.ShortUrl; 4 | import com.leaf.filter.ShortUrlBloomFilter; 5 | import com.leaf.response.Response; 6 | import com.leaf.response.ShortUrlVO; 7 | import com.leaf.service.ShortUrlService; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang.StringUtils; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.util.regex.Matcher; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * @author yefan 20 | */ 21 | @Service 22 | @Slf4j 23 | public class ShortUrlManager { 24 | @Autowired 25 | private ShortUrlService shortUrlService; 26 | 27 | private static final String URL_DUPLICATED = "[DUPLICATED]"; 28 | 29 | @Value("${url.protocol:https}") 30 | private String URL_PREFIX; 31 | 32 | @Value("${domain}") 33 | private String DOMAIN; 34 | 35 | @Autowired 36 | private ShortUrlBloomFilter shortUrlBloomFilter; 37 | 38 | @Transactional(rollbackFor = Exception.class) 39 | public Response generateShortUrl(String url) { 40 | //判断 url 是否是Http https 开头 41 | if(StringUtils.isBlank(url)){ 42 | throw new RuntimeException("参数错误"); 43 | } 44 | url = StringUtils.trim(url).toLowerCase(); 45 | if(!isStartWithHttpOrHttps(url)){ 46 | url = appendHttp2Head(url,URL_PREFIX); 47 | } 48 | 49 | //这里多台机器可能出现并发问题,查询->插入,可能会出现问题,但是有数据库唯一索引保护 50 | String hash = shortUrlService.generateShortUrl(url); 51 | //计算多差次拼接才能生成不重复的 hash value 52 | int count = 0; 53 | 54 | 55 | while(true){ 56 | if(count > 5){ 57 | throw new RuntimeException("重试拼接url 超过限制次数"); 58 | } 59 | //从 BloomFilter 查看是否存在 60 | boolean mightContain = shortUrlBloomFilter.mightContain(hash); 61 | if(!mightContain){ 62 | log.info("============生成短链接,判断短链接不存在,可以生成对应关系!==============="); 63 | break; 64 | } 65 | 66 | //hash 相同且长链接相同 67 | ShortUrl dbShortUrl = shortUrlService.findByHashValueFromLocalCache(hash); 68 | if(dbShortUrl.getUrl().equals(url)){ 69 | log.info("============短链接已存在!==============="); 70 | return Response.success(DOMAIN + hash); 71 | }else{ 72 | log.warn("=======hashValue:[{}],DBUrl:[{}],currentUrl:[{}]", 73 | hash,dbShortUrl.getUrl(),url); 74 | url = url + URL_DUPLICATED; 75 | hash = shortUrlService.generateShortUrl(url); 76 | log.warn("=======重新拼接hash:[{}],currentUrl:[{}]", hash, url); 77 | } 78 | count++; 79 | log.info("===========================url重复拼接字符串,次数:[{}]",count); 80 | } 81 | 82 | try { 83 | //入库 84 | ShortUrl saveBean = new ShortUrl(); 85 | saveBean.setHashValue(hash); 86 | saveBean.setUrl(url); 87 | shortUrlService.saveShortUrl(saveBean); 88 | } catch (Exception e) { 89 | log.error("重复插入问题e:",e); 90 | return Response.failed("-1","请重试!"); 91 | } 92 | return Response.success(DOMAIN + hash); 93 | } 94 | 95 | 96 | public ShortUrlVO getRealUrlByHash(String hash) { 97 | //get Url by hash 98 | ShortUrl shortUrl = shortUrlService.findByHashValueFromLocalCache(hash); 99 | if(null == shortUrl){ 100 | return null; 101 | } 102 | String realUrl = shortUrl.getUrl().replace(URL_DUPLICATED,""); 103 | return new ShortUrlVO(shortUrl.getHashValue(), realUrl); 104 | } 105 | 106 | public static boolean isStartWithHttpOrHttps(String url) { 107 | String regex = "^((https|http)?://)"; 108 | Pattern p = Pattern.compile(regex); 109 | Matcher matcher = p.matcher(url); 110 | return matcher.find(); 111 | } 112 | 113 | /** 114 | * url 开头拼接 http 115 | * @param url 116 | * @return 117 | */ 118 | private String appendHttp2Head(String url, String prefix) { 119 | StringBuilder stringBuilder = new StringBuilder(prefix).append("://"); 120 | stringBuilder.append(url); 121 | return stringBuilder.toString(); 122 | } 123 | 124 | 125 | /** 126 | * 是否是有效的 url 127 | * @param urls 128 | * @return 129 | */ 130 | public boolean isValidUrl(String urls) { 131 | boolean isurl = false; 132 | String regex = "(((https|http)?://)?([a-z0-9]+[.])|(www.))" 133 | + "\\w+[.|\\/]([a-z0-9]{0,})?[[.]([a-z0-9]{0,})]+((/[\\S&&[^,;\u4E00-\u9FA5]]+)+)?([.][a-z0-9]{0,}+|/?)";//设置正则表达式 134 | //对比 135 | Pattern pat = Pattern.compile(regex.trim()); 136 | Matcher mat = pat.matcher(urls.trim()); 137 | //判断是否匹配 138 | isurl = mat.matches(); 139 | if (isurl) { 140 | isurl = true; 141 | } 142 | return isurl; 143 | } 144 | } 145 | --------------------------------------------------------------------------------