Skip to content

Instantly share code, notes, and snippets.

@bingoohuang
Last active February 19, 2016 08:25
Show Gist options
  • Save bingoohuang/746fdce9da1fc86d43ab to your computer and use it in GitHub Desktop.
Save bingoohuang/746fdce9da1fc86d43ab to your computer and use it in GitHub Desktop.
seckilling implementation using java springmvc
http {
include mime.types;
default_type text/plain;
lua_package_path ';;$prefix/conf/?.lua;';
keepalive_timeout 65;
server {
listen 9001;
server_name localhost;
# http://localhost:9001/kill?mobile=18551855407&price=2
location /kill {
default_type 'text/plain';
content_by_lua '
local seckill = require "poet.seckill"
seckill.seckill({
mobile = ngx.var.arg_mobile,
price = ngx.var.arg_price
})
';
}
}
}
import com.raiyee.poet.base.utils.Redis;
import org.n3r.eql.Eql;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.regex.Pattern;
@Controller
public class SecKilling {
static final int LEVELNUM = 100000;
static final String PREFIX = "seckill:";
static Pattern mobilePattern = Pattern.compile("^1\\d{10}$");
static Pattern pricePattern = Pattern.compile("^[1-6]?[0-9]$");
@ResponseBody
@RequestMapping("/kill")
public String kill(@RequestParam("mobile") String mobile, @RequestParam("price") String price) {
try {
checkFormat("mobile", mobile, mobilePattern);
checkFormat("price", price, pricePattern);
int intPrice = Integer.parseInt(price);
check(intPrice >= 1 && intPrice <= 60, price + " should be between 1 and 60");
} catch (RuntimeException e) {
return e.getMessage();
}
try {
// prevent concurrent requests from the same mobile
long count = Redis.incr(PREFIX + mobile);
check(count == 1, mobile + " is already in seckilling");
String querySql = "SELECT PRICE FROM MIAO WHERE MOBILE = ##";
Object priceObject = new Eql().params(mobile).limit(1).execute(querySql);
check(priceObject == null, mobile + " already got a favored price");
String currentPrice = Redis.get(PREFIX + "currentPrice", "1");
check(price.equals(currentPrice), price + " does not match then current price " + currentPrice);
long priceCount = Redis.incr(PREFIX + price);
check(priceCount <= LEVELNUM, price + " already sold out");
long bought = Redis.incr(PREFIX + "bought");
if (bought % LEVELNUM == 0) Redis.set(PREFIX + "currentPrice", "" + (bought / LEVELNUM + 1));
// CREATE TABLE MIAO (
// MOBILE BIGINT(20) UNSIGNED NOT NULL,
// PRICE INT(10) UNSIGNED NOT NULL,
// TS TIMESTAMP NOT NULL,
// PRIMARY KEY (MOBILE));
String insertSql = "INSERT INTO MIAO (MOBILE, PRICE, TS) VALUES(##, ##, NOW())";
new Eql().params(mobile, price).execute(insertSql);
} catch (Exception e) {
return e.getMessage();
} finally {
Redis.del(PREFIX + mobile);
}
return "OK";
}
private void check(boolean expr, String message) {
if (expr) return;
throw new RuntimeException(message);
}
private String checkFormat(String name, String value, Pattern pattern) {
if (value == null) throw new RuntimeException(name + " is required");
if (pattern.matcher(value).matches()) return value;
throw new RuntimeException(name + " is invalid with bad format");
}
}
local _M = {
_VERSION = '0.1'
}
local function error(config, msg, httpCode, err)
if config then config.redis:del(config.prefix .. config.mobile) end
ngx.status = httpCode
ngx.say(msg, err or "")
ngx.log(ngx.ERR, msg)
ngx.exit(httpCode)
end
local function connectRedis (config)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(config.redisTimeout or 1000) -- 1 second
local ok, err = red:connect(config.redisHost, config.redisPort)
if not ok then
error(nil, "connecting redis", 500, err)
end
config.redis = red
end
local function connectMySQL (config)
local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
error(config, "failed to instantiate mysql", 500, err)
end
db:set_timeout(config.mysqlTimeout or 1000) -- 1 second
local ok, err, errno, sqlstate = db:connect{
host = config.mysqlHost,
port = config.mysqlPort,
database = config.mysqlDb,
user = config.mysqlUser,
password = config.mysqlPass,
max_packet_size = 1024 * 1024 }
if not ok then
error(config, "failed to connect mysql", 500, err)
end
config.mysql = db
end
local function checkExistence (config)
local count = config.redis:incr(config.prefix .. config.mobile)
if tonumber(count) > 1 then
error(config, config.mobile .. " is already in second killing, please try later", 400)
end
local sql = "select price from miao where mobile = " .. config.mobile
local res, err, errno, sqlstate = config.mysql:query(sql)
if not res then
error(config, "bad result: ", 500, err .. ": " .. errno .. ": " .. sqlstate .. ".", 400)
end
if res[1] then
error(config, config.mobile .. " have got a favored price " .. res[1].price, 400);
end
end
local function checkCurrentPrice(config)
-- 检查是否超过允许价格
local expectedPrice = config.redis:get(config.prefix .. "currentPrice")
if expectedPrice == ngx.null then expectedPrice = 1 end
if config.price ~= tonumber(expectedPrice) then
error(config, "bad price " .. config.price .. " request expected " .. expectedPrice, 400)
end
local count, err = config.redis:incr(config.prefix .. config.price)
if tonumber(count) > config.levelNum then
error(config, "sold out for current price", 400)
end
end
local function checkPriceRange(config)
if config.price < 1 or config.price > 60 then
error(nil, "bad price range value:" .. config.price, 400)
end
end
local function updatePrice(config)
local countStr, err = config.redis:incr(config.prefix .. "bought")
local count = tonumber(countStr)
if count % config.levelNum == 0 then
config.redis:set(config.prefix .. "currentPrice", count / config.levelNum + 1)
end
end
-- CREATE TABLE MIAO (
-- MOBILE BIGINT(20) UNSIGNED NOT NULL,
-- PRICE INT(10) UNSIGNED NOT NULL,
-- TS TIMESTAMP NOT NULL,
-- PRIMARY KEY (MOBILE)
--);
local function saveRecordToMysql(config)
local sql = "insert into miao (mobile, price, ts) "
.. "values (".. config.mobile .. ", ".. config.price .. ", now())"
local res, err, errno, sqlstate = config.mysql:query(sql)
if not res then
error(config, "bad result: ", 500, err)
end
end
local function checkFormat (name, value, format)
if not value then
error(nil, name .. " required", 400)
end
local valueStr = string.match(value, format)
if not valueStr then
error(nil, name .. " in bad format", 400)
end
return value
end
local function cleanUp(config)
config.redis:del(config.prefix .. config.mobile)
-- put it into the connection pool of size 100,
-- with 10 seconds max idle timeout
config.redis:set_keepalive(config.redisMaxIdleTimeout or 10000, config.redisPoolSize or 10)
config.mysql:set_keepalive(config.mysqlMaxIdleTimeout or 10000, config.mysqlPoolSize or 10)
end
local function seckilling(config)
checkPriceRange(config)
connectRedis(config)
connectMySQL(config)
checkExistence(config)
checkCurrentPrice(config)
updatePrice(config)
saveRecordToMysql(config)
end
function _M.seckill (option)
local config = {
prefix = option.prefix or "seckill:",
mobile = checkFormat("mobile", option.mobile, "^1%d%d%d%d%d%d%d%d%d%d$"),
price = tonumber(checkFormat("price", option.price, "^[1-6]?[0-9]$")),
levelNum = option.levelNum or 100000,
redisHost = "127.0.0.1",
redisPort = 6379,
redisTimeout = 3000, -- 3s
redisMaxIdleTimeout = 10000, -- 10s
redisPoolSize = 20,
redis = nil,
mysqlHost = "127.0.0.1",
mysqlPort = 3306,
mysqlDb = "diamond",
mysqlUser = "diamond",
mysqlPass = "diamond",
mysqlTimeout = 3000,
mysqlMaxIdleTimeout = 10000, -- 10s
mysqlPoolSize = 20,
mysql = nil
}
local success, result = pcall(seckilling, config)
cleanUp(config)
if not success then
error(nil, result, 500)
end
ngx.say("OK")
end
return _M
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
public class SecKillingMessure {
static AtomicLong mobile = new AtomicLong(13012340000L);
static HttpClient httpClient;
static MultiThreadedHttpConnectionManager connectionManager;
static {
HttpClientParams params = new HttpClientParams();
HttpConnectionManagerParams httpConnectionManagerParams = new HttpConnectionManagerParams();
httpConnectionManagerParams.setDefaultMaxConnectionsPerHost(10); // 默认2
httpConnectionManagerParams.setMaxTotalConnections(10); // 默认20
connectionManager = new MultiThreadedHttpConnectionManager();
connectionManager.setParams(httpConnectionManagerParams);
httpClient = new HttpClient(params, connectionManager);
}
@Benchmark
public void measureNginx() throws IOException {
mease(9001);
}
@Benchmark
public void measureJava() throws IOException {
mease(8080);
}
private void mease(int port) throws IOException {
long thisMobile = mobile.incrementAndGet();
long thisPrice = (thisMobile - 13012340000L - 1) / 100000 + 1;
String uri = "http://localhost:" + port + "/kill?mobile=" + thisMobile + "&price=" + thisPrice;
GetMethod method = new GetMethod(uri);
httpClient.executeMethod(method);
String response = method.getResponseBodyAsString();
if (!response.trim().equals("OK")) {
System.out.println("thisMobile:" + thisMobile + ",thisPrice:" + thisPrice + ",error:" + response.trim());
}
method.releaseConnection();
}
public static void main(String[] args) throws RunnerException, IOException {
Options opt = new OptionsBuilder()
.include(FlowPackageSecKill.class.getSimpleName())
.warmupIterations(3)
.threads(10)
.measurementIterations(50)
.forks(0)
.build();
new Runner(opt).run();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment