总体思路
服务端
客户端
org.example.seckill.SeckKillTest2
package org.example.seckill;
import io.lettuce.core.*;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
public class SeckKillTest2 {
private static final Logger logger = LoggerFactory.getLogger(SeckKillTest2.class);
public static class ProductPutReq {
private int id;
private int num;
public ProductPutReq(int id, int num) {
this.id = id;
this.num = num;
}
public int getId() {
return id;
}
public int getNum() {
return num;
}
}
public static class SeckKillReq {
private int productId;
private int userId;
public SeckKillReq(int userId, int productId) {
this.userId = userId;
this.productId = productId;
}
public int getProductId() {
return productId;
}
public int getUserId() {
return userId;
}
}
private static final String luaProductPut =
"local exists = redis.call('EXISTS', KEYS[1]) \n" +
"if exists == 1 then \n" +
" return false \n" +
"else \n" +
" redis.call('HSET', KEYS[1], 'product_id', ARGV[1] ) \n" +
" redis.call('HSET', KEYS[1], 'num', ARGV[2] ) \n" +
" redis.call('HSET', KEYS[1], 'init_num', ARGV[3] ) \n" +
" return true \n" +
"end";
//定义 lua脚步
private static final String luaSeckKill = // 如果已经抢过商品,直接返回
"local order_id = redis.call('HGET', KEYS[2], ARGV[2]) \n " +
"if order_id then \n" +
" return '-1' \n" +
"end \r" +
"local num = tonumber(redis.call('HGET',KEYS[1],'num')) \n" +
"if num == nil then \n" +
" return '-2' \n" +
"elseif num > 0 then \n" +
" redis.call('HSET', KEYS[1], 'num', num-1) \n" +
" local order_id = KEYS[1]..'_'..num \n" +
" local res = order_id..string.rep('0', 13 - #order_id) \n" +
" redis.call('HSET', KEYS[2], ARGV[2], res) \n " +
" return res \n" +
"else \n" +
" return '0' \n" + //商品抢完,直接返回 0
"end ";
private static final Random random = new Random();
private static final int base = 100000000;
private static final int LIMIT = (int)(Runtime.getRuntime().freeMemory() / 1024 / 4 ) ;
static {
// logger.info("LIMIT: {}", LIMIT);
System.out.println("LIMIT: " + LIMIT);
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
int times = args.length > 0 ? Integer.parseInt(args[0]) : 1;
RedisClient redisClient = RedisClient.create("redis://192.168.253.176:6379");
SocketOptions socketOptions = SocketOptions.builder()
.tcpNoDelay(false)
.build();
ClientOptions options = ClientOptions.builder()
.socketOptions(socketOptions)
.build();
redisClient.setOptions(options);
StatefulRedisConnection<String, String> connection1 = redisClient.connect();
RedisAsyncCommands<String, String> asyncCommands1 = connection1.async();
RedisAsyncCommands<String, String>[] commands = new RedisAsyncCommands[]{
asyncCommands1
};
//清理上次数据
logger.info("清理上次数据");
String prefix = "2";
RedisFuture<List<String>> delKeys = asyncCommands1.keys(prefix + "*");
delKeys.get().forEach(asyncCommands1::del);
asyncCommands1.del(delKeys.get().toArray(new String[0])).get();
System.out.println("清理上次数据完毕");
int counter = 0;
AtomicInteger latchCount = new AtomicInteger(0);
long begin = System.currentTimeMillis();
for(int c = 0 ; c < times; c++) {
int id = base * 2 + random.nextInt(base);
int num = 1 + random.nextInt(100);
//商品入库
ProductPutReq productPutReq = new ProductPutReq( id, num);
String key = String.join("_",
String.valueOf(productPutReq.getId()));
String seckKillKey = String.join("_",
String.valueOf(productPutReq.getId()),
"kill");
String[] keys = new String[]{key, seckKillKey};
{
final String numStr = String.valueOf(productPutReq.getNum());
RedisFuture<Boolean> future = commands[0].eval(luaProductPut,
ScriptOutputType.BOOLEAN, keys, key, numStr, numStr);
counter++;
future.whenComplete((r, e) -> {
latchCount.incrementAndGet();
print(r, e, key, numStr, asyncCommands1);
});
}
//秒杀商品
int userNum = num + random.nextInt(100);
for (int i = 0; i < userNum; i++) {
SeckKillReq seckKillReq = new SeckKillReq(base + random.nextInt(base), id);
{
RedisFuture<String> result1 = commands[0]
.eval(luaSeckKill, ScriptOutputType.VALUE, keys, "", seckKillReq.getUserId() + "");
counter++;
result1.whenComplete((r, e) -> {
latchCount.incrementAndGet();
printResult(r, e,key, seckKillReq);
});
}
}
while ( counter - latchCount.get() > LIMIT )
{
Thread.yield();
}
}
while (latchCount.get() < counter)
{
Thread.yield();
}
long end = System.currentTimeMillis();
long delta = end - begin;
Thread.sleep(2000);
System.out.printf("总计 %s 次请求 %n", counter);
System.out.printf("总计耗时 %s ms %n", delta);
System.out.printf("平均耗时 %s ms %n", delta / counter);
System.out.printf("TPS: %s 次/秒 %n", counter * 1000L / delta);
System.exit(0);
}
private static void print(Boolean r, Throwable e, String key, String numStr, RedisAsyncCommands<String, String> asyncCommands1) {
if (e != null) {
logger.info("商品入库失败, key为 {}, 商品数量为 {}, 异常:{}", key, numStr, e.getMessage() );
}
else
{
if (r) {
logger.info("商品入库成功, key为 {}, 商品数量为 {}", key, numStr);
} else {
try {
String initNumStr = asyncCommands1.hget(key, "init_num").get();
logger.info("商品已经存在, key为 {}, 商品数量为 {}", key, initNumStr);
} catch (InterruptedException | ExecutionException ex) {
logger.info("商品已经存在, key为 {}, 异常:{}", key, ex.getMessage());
}
}
}
}
private static void printResult(String r, Throwable e,String key, SeckKillReq seckKillReq) {
if (e != null) {
logger.error("用户 {} 抢商品{}, error", seckKillReq.getUserId(), key, e);
} else {
if ("0".equals(r)) {
logger.info("用户 {} 抢商品 {} 失败,商品已抢完", seckKillReq.getUserId(), key);
} else if ("-1".equals(r))
{
logger.info("用户 {} 抢商品 {} 失败,已经抢过商品", seckKillReq.getUserId(), key);
} else if ("-2".equals(r)) {
//乱序处理
logger.info("用户 {} 抢商品 {} 失败,商品不存在", seckKillReq.getUserId(), key);
} else
{
logger.info("用户 {} 抢商品 {} 订单号: {}", seckKillReq.getUserId(), key, r);
}
}
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>RedisTemplateTest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.23.1</version>
<!-- <scope>test</scope>-->
</dependency>
<!-- https://mvnrepository.com/artifact/com.conversantmedia/disruptor -->
<dependency>
<groupId>com.conversantmedia</groupId>
<artifactId>disruptor</artifactId>
<version>1.2.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>3.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.lettuce/lettuce-core -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="%style{%d{HH:mm:ss.SSS}}{Magenta} %style{|-}{White}%highlight{%-5p} [%t] %style{%40.40c}{Cyan}:%style{%-3L}{Blue} %style{-|}{White} %m%n%rEx{filters(jdk.internal.reflect,java.lang.reflect,sun.reflect)}" disableAnsi="false" charset="UTF-8"/>
</Console>
<RollingFile name="RollingFile" fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd-HH}.log" immediateFlush="false" bufferSize="1048576">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="200MB"/>
</Policies>
</RollingFile>
<Async name="AsyncAppender">
<AppenderRef ref="RollingFile"/>
<blocking>false</blocking>
<LinkedTransferQueue/>
<!-- <DisruptorBlockingQueue/>-->
</Async>
</Appenders>
<Loggers>
<Root level="info">
<!-- <AppenderRef ref="Console"/>-->
<AppenderRef ref="AsyncAppender"/>
</Root>
</Loggers>
</Configuration>
压测
运行参数
压测结果