Last active
February 19, 2016 08:25
-
-
Save bingoohuang/746fdce9da1fc86d43ab to your computer and use it in GitHub Desktop.
seckilling implementation using java springmvc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}) | |
'; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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