第三部分
从这部分开始,我就要做秒杀的功能优化啦。这篇简单讲一下压测实战,需要结合之前的博文JMeter压测(1)、(2)一起看。这边先展示下在当前基础功能的初步实现下,在高并发情况下系统的性能问题和出现的业务逻辑问题。
1. 生成测试用例
我们先写个函数,在数据库中自动创建5000个用户信息,在Redis中自动创建用户的token信息,方便之后模拟高并发的情况。下面先贴代码。
public class UserUtil {
private static void createUser(int count) throws Exception {
List<MiaoshaUser> users = new ArrayList<MiaoshaUser>(count);
//生成用户
for (int i = 0; i < count; i++) {
MiaoshaUser user = new MiaoshaUser();
user.setId(13000000000L + i);
user.setLoginCount(1);
user.setNickname("user" + i);
user.setRegisterDate(new Date());
user.setSalt("123456");
user.setHead("111");
user.setLastLoginDate(new Date());
user.setPassword(MD5Util.inputPassToDbPass(String.valueOf(user.getId()), user.getSalt()));
users.add(user);
}
System.out.println("create user");
/*//插入数据库
Connection conn = DBUtil.getConn();
String sql = "insert into user(login_count, nickname, register_date, salt, password, id,head,last_login_date)values(?,?,?,?,?,?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for(int i=0;i<users.size();i++) {
MiaoshaUser user = users.get(i);
pstmt.setInt(1, user.getLoginCount());
pstmt.setString(2, user.getNickname());
pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
pstmt.setString(4, user.getSalt());
pstmt.setString(5, user.getPassword());
pstmt.setLong(6, user.getId());
pstmt.setString(7,user.getHead());
pstmt.setTimestamp(8,new Timestamp(user.getLastLoginDate().getTime()));
pstmt.addBatch();
}
pstmt.executeBatch();
pstmt.close();
conn.close();
System.out.println("insert to db");*/
//登录,生成token
String urlString = "http://localhost:8080/login/do_login";
File file = new File("E:/miaosha_imooc/yace/config.txt");
if (file.exists()) {
file.delete();
}
RandomAccessFile raf = new RandomAccessFile(file, "rw");
file.createNewFile();
raf.seek(0);
for (int i = 0; i < users.size(); i++) {
MiaoshaUser user = users.get(i);
URL url = new URL(urlString);
HttpURLConnection co = (HttpURLConnection) url.openConnection();
co.setRequestMethod("POST");
co.setDoOutput(true);
OutputStream out = co.getOutputStream();
String params = "mobile=" + user.getId() + "&password=" + MD5Util.inputPassToFormPass(String.valueOf(user.getId()));
out.write(params.getBytes());
out.flush();
InputStream inputStream = co.getInputStream();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len = 0;
while ((len = inputStream.read(buff)) >= 0) {
bout.write(buff, 0, len);
}
inputStream.close();
bout.close();
String response = new String(bout.toByteArray());
JSONObject jo = JSON.parseObject(response);
String token = jo.getString("data");
System.out.println("create token : " + user.getId());
String row = user.getId() + "," + token;
raf.seek(raf.length());
raf.write(row.getBytes());
raf.write("\r\n".getBytes());
System.out.println("write to file : " + user.getId());
}
raf.close();
System.out.println("over");
}
public static void main(String[] args) throws Exception {
createUser(5000);
}
}
可以看到,这里主要是分四步:1.先创建用户;2.再插入数据库;3.插入到Redis中;4.将用户和token对存放到text文件中,这个文件供压测使用(需要读这个文件)。这里要注意的是:数据库插入一次就可以,但redis由于设置了过期时间(我设了2天),如果超时了需要再运行一下程序以插入redis。
2. JMeter基本设置
这部分最基础的部分在之前的博文JMeter(1)、(2)中写过啦,这里就不重复了,主要写一下这次压测相关的步骤。
首先说明一点的是,测试用户全在我的笔记本上访问页面,并且本机同时运行JMeter和Java程序,是不能真正模拟真实的并发的,这里由于设备限制也没有条件(本人比较穷 ),只是大概体验一下高并发,并且之后所有的优化都基于这个条件,主要保持环境条件一致,还是能比较出优化效果的。
- 新建线程组,这里采用5000个线程循环10次
- 设置一下默认配置,之后就不用反复填写了
- 设置配置文件
这个具体功能在JMeter(2)中已介绍了,就是读text文件并且设置变量的作用。
- 设置HTTP 请求
我们这次直接对秒杀功能进行压测,填写的路径如图所示,这个要参见之前的代码。访问这个路径时需要两个变量,其中token是从之前的文本文件中读取的,注意Value的语法(如何写的)。
全部设置好之后,就可以运行了。运行前保证Redie和Tomcat启动。
3. 结果展示
- 第一次运行的结果
- 压测报告
TPS:630.9/sec(不高)
错误率:64.73%(太高了)
错误率太高了,看一下程序,抛异常了。 - Redis异常
很明显,这样的并发下Redis读取超时了。贴出Redis的配置:
#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.timeout=3000ms
spring.redis.jedis.pool.max-wait=3000ms
- 数据库超卖现象
这是秒杀商品表,我们是对商品1进行秒杀的,库存成为负值,问题大了。我之前设置的秒杀商品个数给了9件,现在超卖了22件。
- 第二次运行结果
为了客观表现结果,重新再运行一次。注意把数据库信息清空的清空,恢复的恢复;把JMeter上次结果清空。
- 压测报告
TPS:808.5/sec(不高)
错误率:67.16%(太高了) - Redis异常
和第一次一样,就不贴图了。 - 数据库超卖现象
情况有所好转,还是超卖。
4. 总结
这次实战结果就是:1. Redis读取超时抛异常导致压测的错误率太高;2. TPS不高,说明系统性能不佳;3. 数据库出现超卖现象,严重的业务逻辑错误。
5. 解决异常
程序抛异常,这是不应该产生的,先将抛异常的问题解决。
我一开始先把redis连接池重新配置:
#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.jedis.pool.max-active=1000
spring.redis.jedis.pool.max-idle=500
spring.redis.timeout=5000ms
spring.redis.jedis.pool.max-wait=5000ms
运行后发现程序不抛异常了,但是压测的错误率并没有下降,依然在60%以上,意识到这可能不是简单改动程序就行了。
查看了压测的日志,也并没有报错,如果大家有遇到类似的问题,参考博文:
https://blog.csdn.net/chenyun19890626/article/details/80645740
这个方法我试过了,用了这个方法可以把错误率大大降低,但是如果把并发调大还是会出现错误。
所以我想是不是本身就是我单机性能有限。。可以把并发线程和循环次数适当调小。
如何性能调优,下次再写。