Tuesday, March 25, 2014

Tiny Url with Redis

As part of work, I was asked to research about Redis, a Distributed Key-Value Persistence Store developed to make caching extremely fast. So far I have found the following by going through documentation, Redis;
  1. An In-memory Key-Value persistence store with off the shelf advanced data structures and algorithms
  2. Supports Hashes (Tables), Lists, Sets, Sorted Sets and of course Key-Value pairs
  3. Enables I/O concurrency but no execution parallelism (Single threaded engine).
  4. Supports command pipelining for lesser network roundtrips.
  5. Supports transactions on demand by queuing commands.
  6. Supports Custom Script Execution (Similar to Stored Procedures)
  7. Supports Publish/Subscribe on topics
Redis stands out from Memcached because of its built-in datastructures and operations such as Lists, Sets, Hashes and Sorting functions. Redis is widely used in alot of applications which requires high performance such Twitter, Instagram, Stackoverflow and many more.

To get some hands on experience on Redis I wrote a simple Tiny Url application using Redis and Spring MVC. Two Hash data structures are used in my application to store both the Url Code to Url Mapping and Url Code to Click Counts. 
  • tinyurls - To store Url Code to Url Mapping
  • tinyurls:clicks - To store Url Code to Click Counts
The controller for the application looks as the following. Where jedisClient is an instance of Jedis, a Java Client for Redis. 

package com.shazin.tinyurl;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.TransactionBlock;
import redis.clients.jedis.exceptions.JedisException;

@Controller
@RequestMapping("/")
public class TinyUrlController {

    private static final String CLICKS = "tinyurls:clicks";

    private static final String TINY_URLS = "tinyurls";

    @Inject
    private Jedis jedisClient;

    @RequestMapping(value = "create", method = RequestMethod.POST)
    public ModelAndView create(@RequestParam("url") String url,
            HttpServletRequest request) {
        ModelMap map = new ModelMap();
        String tinyUrl = null;
        Long timeInMillis = System.currentTimeMillis();
        String urlKey = Long.toHexString(timeInMillis);
        jedisClient.hset(TINY_URLS, urlKey, url);
        jedisClient.hset(CLICKS, urlKey, "0");

        tinyUrl = generateTinyUrl(request, urlKey);

        map.put("tinyurl", tinyUrl);
        return new ModelAndView("home", map);
    }

    public String generateTinyUrl(HttpServletRequest request, String urlKey) {
        String tinyUrl;
        tinyUrl = new StringBuilder()
                .append((request.isSecure() ? "https://" : "http://"))
                .append(request.getServerName()).append(":")
                .append(request.getServerPort())
                .append(request.getContextPath()).append("/u/").append(urlKey)
                .toString();
        return tinyUrl;
    }

    @RequestMapping(value = "u/{urlKey}", method = RequestMethod.GET)
    public ModelAndView forwardUrl(@PathVariable("urlKey") String urlKey, HttpServletResponse response) {
        String destinationUrl = null;
        ModelMap map = new ModelMap();
        String url = jedisClient.hget(TINY_URLS, urlKey);
        if(url != null && url.length() > 0) {
            jedisClient.hincrBy(CLICKS, urlKey, 1);
            map.put("url", url);
            destinationUrl = "redirect";
        } else {
            map.put("errorMsg", "Invalid Code in Url!");
            destinationUrl = "home";
        }
        
        return new ModelAndView(destinationUrl, map);
    }
    
    @RequestMapping(value="/", method=RequestMethod.GET)
    public ModelAndView home() {
        return new ModelAndView("home");
    }
    
    @RequestMapping(value="/clicks", method=RequestMethod.GET)
    public ModelAndView clicks(HttpServletRequest request) {
        ModelMap map = new ModelMap();
        Map<String, String> clickCounts = jedisClient.hgetAll(CLICKS);
        
        Map<String, String> counts = new HashMap<String, String>();
        
        for(Map.Entry<String, String> entry:clickCounts.entrySet()) {
            counts.put(generateTinyUrl(request, entry.getKey()), entry.getValue());
        }
        
        map.put("clickCounts", counts);
        
        return new ModelAndView("clicks", map);
    }
}


And the home.jsp looks as the following.



<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
<title>Tiny Url</title>
<link href="${pageContext.request.contextPath}/resources/main.css" type="text/css" rel="stylesheet"/>
</head>
<body>
    <form id="urlShortenerForm" action="${pageContext.request.contextPath}/create" method="POST">
        <center style="margin-top: 10%">
            <c:if test="${errorMsg != null}">
                <p style="color: red;">
                    <b>${errorMsg}</b>
                </p>
            </c:if>            
            <p>Shorten any url!</p>
            <c:if test="${tinyurl != null}">
                <p>
                    <b>The Tiny Url is <a href="${tinyurl}">${tinyurl}</a></b>
                </p>
            </c:if>
            <p>
                <textarea id="url" name="url" rows="5" cols="50"></textarea>
            </p>
            <p>
                <input type="button" onclick="validateUrl()" value="Shorten" />
            </p>
            <p>
                <a href="${pageContext.request.contextPath}/clicks">Clicks Count</a>
            </p>
        </center>
    </form>
</body>
<script type="text/javascript">
    function validateUrl() {
        var textArea = document.getElementById("url");
        var valid = false;
        var value = textArea.value;
        if(value.length > 0) {
            var httpIndex = value.toLowerCase().indexOf("http");
            var columnIndex = value.indexOf(":");
            var slashIndex = value.indexOf("/");        
            if(httpIndex > -1 && columnIndex > -1 && slashIndex > -1) {
                valid = true;
            }
        }
        if(valid) {
            var urlShortenerForm = document.getElementById("urlShortenerForm");
            urlShortenerForm.submit();
        } else {
            alert("Invalid Url, Please enter a valid Url");
            textArea.value = "";
            textArea.focus();
        }
    }
</script>
</html>

After submitting a URL, this how the Tiny URL will look like.


The clicks.jsp will look like the following


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
<title>Tiny Url Click Counts</title>
<link href="${pageContext.request.contextPath}/resources/main.css" type="text/css" rel="stylesheet"/>
<style type="text/css">
table
{
border-collapse:collapse;
}
table, td
{
border: 1px solid black;
text-align: center;
}
th {
background: aqua;
}
</style>
</head>
<body>
    <center style="margin-top: 10%">
        <p>Click Counts</p>

        <p>
        <table border="1">
            <tr>
                <th>Url</th>
                <th>Click Count</th>
            </tr>
            <c:choose>
                <c:when test="${clickCounts.size() != 0}">
                    <c:forEach items="${clickCounts}" var="clickCount">
                        <tr>
                            <td><a href="${clickCount.key}">${clickCount.key}</a></td>
                            <td>${clickCount.value}</td>
                        </tr>
                    </c:forEach>
                </c:when>
                <c:otherwise>
                    <tr>
                        <td colspan="2">No clicks found</td>
                    </tr>
                </c:otherwise>
            </c:choose>

        </table>
        </p>

        <p>
            <a href="${pageContext.request.contextPath}">Back</a>
        </p>
    </center>
</body>
</html>

Finally the redirect.jsp will use a Javascript to redirect to the destination URL.

<html>
    <script type="text/javascript">
        window.location = "${url}";
    </script>
    <b>Redirecting Please wait...</b>
</html>

So in the backend the Url Code is generated using the Current Time in Milliseconds converted to Hexadecimal, and stored with the URL in tinyurls hash along with the click count which is 0 in tinyurls:clicks hash. And when the generated tiny url is clicked the corresponding url is retrieved from tinyurls hash based on the Url Code and tinyurls:clicks is incremented by one based on Url Code. Finally the user is redirected to Original URL. 

For this to work redis-server must be running!

This is just a basic example of the capabilities of Redis but is a good starting point. Hoping to learn further!


2 comments: