├── 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 |
--------------------------------------------------------------------------------