秒杀项目用户访问量大
考虑:秒杀商品库存的存储、秒杀成功用户列表的存储(数据可以存在Redis中)
sk:商品id:qt 库存的数量 string[incr decr]
sk:商品id:usr 用户id列表 set
【
Redis中的数据:一个用户会对应多条数据需要存储,需要保证存储的数据键的唯一性,见名知意。
用户登录成功的信息
用户的会话数据
用户获取的验证码
用户获取验证码的数量
】
实现:
1、创建项目,准备秒杀的页面和后台处理秒杀的Servlet。
在pom中引入依赖
<dependency>
<groupId>javax.servletgroupId>
<artifactId>jsp-apiartifactId>
<version>2.0version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>servlet-apiartifactId>
<version>2.5version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
dependencies>
在webapp下删除自带的index.jsp,自己创建index页面。
<h2>一元秒杀糖果</h2>
<form action="${pageContext.request.contextPath}/doSeckill" method="post">
<input type="hidden" name="prodid" value="1001"/>
<input type="submit" value="点我秒杀"/>
</form>
修改mavenweb项目的目录:需要有main/java main/resources test/java
在java下创建com.atguigu.sk.DoSeckillServlet处理秒杀的请求:
public class DoSecKillServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理秒杀的业务
System.out.println(req.getParameter("prodid"));
}
}
在项目的web.xml中注册servlet:
<servlet>
<servlet-name>DoSeckillServletservlet-name>
<servlet-class>com.atguigu.sk.DoSeckillServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>DoSeckillServletservlet-name>
<url-pattern>/doSeckillurl-pattern>
servlet-mapping>
2、修改秒杀的请求为异步方式提交:
拷贝众筹项目的static目录到项目的webapp目录下:jquery
修改inde页面为异步提交
<h2>一元秒杀糖果</h2>
<form action="${pageContext.request.contextPath}/doSeckill" method="post">
<input type="hidden" name="prodid" value="1001"/>
<input type="submit" value="点我秒杀"/>
</form>
<script type="text/javascript">
$("form input:last").click(function () {
$.ajax({
"type":"post",
"url":$("form").prop("action"),
"data":$("form").serialize(),
"success":function (result) {
if(result=="200"){
alert("秒杀成功");
}else if(result=="1001"){
alert("请勿重复秒杀");
}else if(result=="1002"){
alert("秒杀还未开始");
}else if(result=="1003"){
alert("库存不足。。")
}else{
alert("服务器繁忙,请稍后重试");
}
}
});
});
</script>
3、修改Servlet处理秒杀的业务:
public class DoSeckillServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理秒杀业务
//System.out.println(req.getParameter("prodid"));
//1、获取请求参数 秒杀id
String prodid = req.getParameter("prodid");
//随机生成用户id
String userid = (int)(10000*Math.random())+"";
//拼接redis中存储数据的键
String prodKey = "sk:"+prodid+":qt";
String usersKey = "sk:"+prodid+":usr";
//2、秒杀业务
Jedis jedis = new Jedis("192.168.200.130", 6379);
// 2.1 判断该用户是否秒杀成功过(在Redis的秒杀用户列表set中判断)
Boolean sismember = jedis.sismember(usersKey, userid);
if(sismember){
//用户已经秒杀过
System.err.println(userid+"用户重复秒杀。。。");
resp.getWriter().write("1001");
jedis.close();
return;
}
//2、2判断库存是否充足
String qtStr = jedis.get(prodKey);
if(qtStr==null||qtStr==""){
//秒杀尚未开始
System.err.println("秒杀尚未开始");
resp.getWriter().write("1002");
jedis.close();
return;
}
int qt=Integer.parseInt(qtStr);
if(qt<=0){
//库存不足
System.err.println("库存不足");
resp.getWriter().write("1003");
jedis.close();
return;
}
//库存足够用户可以秒杀
//减少库存将用户添加到秒杀成功的列表中
System.out.println("秒杀成功。。。"+qt);
jedis.decr(prodKey);
jedis.sadd(usersKey,userid);
resp.getWriter().write("200");
}
}
4、测试:
在redis中存入库存: set sk:1001:qt 10
重启项目访问
以后java程序员需要使用的性能测试工具:
apache ab :linux系统中可以安装使用,用来做高并发测试
apache jemeter: java程序,用来做高并发的测试,功能比ab多
postman : 测试java程序的接口是否可以访问,参数返回值是否正常,可以做持续的压力测试
ab工具的安装使用
安装:
yum install -y httpd-tools
使用ab测试秒杀:
初始化redis中的商品库存:
set sk:1001:qt 100
在linux中使用ab进行测试:
ab -n 2000 -c 200 -p postfile.txt -T application/x-www-form-urlencoded -r http://192.168.1.46:8080/SecondKill/doSeckill
【注意:这里的ip地址不可以写成localhost或者127.0.0.1,因为此时在虚拟机中,虚拟机访问】
-n 请求总数量
-c 并发提交的请求数量
-p post方式提交请求的请求参数
postfile.txt内容:
vim postfile.txt
prodid=1001&
-T post方式提交的请求体数据的处理方式
-r 出错不会退出
最后是要访问的项目地址[由于秒杀项目在idea主机中运行,ip地址需要些主机的ip]
public class DoSeckillServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理秒杀业务
//System.out.println(req.getParameter("prodid"));
//1、获取请求参数 秒杀id
String prodid = req.getParameter("prodid");
//随机生成用户id
String userid = (int)(10000*Math.random())+"";
//拼接redis中存储数据的键
String prodKey = "sk:"+prodid+":qt";
String usersKey = "sk:"+prodid+":usr";
//2、秒杀业务
Jedis jedis = new Jedis("192.168.200.130", 6379);
// 2.1 判断该用户是否秒杀成功过(在Redis的秒杀用户列表set中判断)
Boolean sismember = jedis.sismember(usersKey, userid);
if(sismember){
//用户已经秒杀过
System.err.println(userid+"用户重复秒杀。。。");
resp.getWriter().write("1001");
jedis.close();
return;
}
//2、2判断库存是否充足
//给库存添加乐观锁
jedis.watch(prodKey);
String qtStr = jedis.get(prodKey);
if(qtStr==null||qtStr==""){
//秒杀尚未开始
System.err.println("秒杀尚未开始");
resp.getWriter().write("1002");
jedis.close();
return;
}
int qt=Integer.parseInt(qtStr);
if(qt<=0){
//库存不足
System.err.println("库存不足");
resp.getWriter().write("1003");
jedis.close();
return;
}
//库存足够用户可以秒杀
//减少库存将用户添加到秒杀成功的列表中
System.out.println("秒杀成功。。。"+qt);
Transaction multi = jedis.multi();
multi.decr(prodKey);
multi.sadd(usersKey,userid);
//执行队列
multi.exec();//只要执行队列的watch会自动删除
// jedis.unwatch();
jedis.close();
resp.getWriter().write("200");
}
}
乐观锁机制,如果请求不能更新数据则直接执行结束
多个并发的请求如果获取数据时的版本号不一样,最多只有一个请求给更新成功,其他全部失败。
redis从2.6版本后开始支持LUA脚本,内置了LUA的解释器
LUA脚本也是一个轻量级的高级语言,一般不会单独编程,一般在其他的语言中当做脚本语言使用。
在java代码中可以将操作redis的逻辑和命令封装为lua脚本,将lua脚本一次性提交给redis,redis可以通过Lua脚本解析 逻辑判断+命令并执行
public class DoSeckillServlet extends HttpServlet {
static String secKillScript = "local userid=KEYS[1];\r\n"
+ "local prodid=KEYS[2];\r\n"
+ "local qtkey='sk:'..prodid..\":qt\";\r\n"
+ "local usersKey='sk:'..prodid..\":usr\";\r\n"
+ "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n"
+ "if tonumber(userExists)==1 then \r\n"
+ " return 2;\r\n" + "end\r\n"
+ "local num= redis.call(\"get\" ,qtkey);\r\n"
+ "if tonumber(num)<=0 then \r\n"
+ " return 0;\r\n" + "else \r\n"
+ " redis.call(\"decr\",qtkey);\r\n"
+ " redis.call(\"sadd\",usersKey,userid);\r\n" + "end\r\n" + "return 1";
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理秒杀业务
//System.out.println(req.getParameter("prodid"));
//1、获取请求参数 秒杀id
String prodid = req.getParameter("prodid");
//随机生成用户id
String userid = (int) (10000 * Math.random()) + "";
//拼接redis中存储数据的键
String prodKey = "sk:" + prodid + ":qt";
String usersKey = "sk:" + prodid + ":usr";
//2、秒杀业务
Jedis jedis = new Jedis("192.168.200.130", 6379);
//通过lua脚本封装业务和命令一次性交给Redis执行,就可以强行将所有的请求进行排序按照提交顺序执行
String str = jedis.scriptLoad(secKillScript);
//jedis执行LUA脚本并传入参数:参数1:lua脚本的加密字符串,参数2:lua脚本参数的数量,参数3.。。:参数列表
Object evalsha = jedis.evalsha(str, 2, userid, prodid);
int result =(int)((long)evalsha);
if(result==1){
resp.getWriter().write("200");
}else if(result==2){
resp.getWriter().write("1001");
}else if(result==3){
resp.getWriter().write("1003");
}
jedis.close();
}
}
之前jedis的连接都是在service方法中直接创建关闭,比较耗时。
1、在项目的pom文件中引入commons-pool2依赖
org.apache.commons
commons-pool2
2.7.0
2、拷贝jedis的连接池工具类
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100 * 1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "192.168.1.168", 6379, 60000);
}
}
}
return jedisPool;
}
}
3、修改代码中获取jedis连接的方式
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();