前端传一个产品id过来,并且与servlet做ajax交互,前端得到产品库存数量
servlet
String userid = new Random().nextInt(50000) +"" ;
String prodid =request.getParameter("prodid");
boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
response.getWriter().print(isSuccess);
redis
模拟整个秒杀过程,但是是在没有并发情况下
//秒杀过程
public static boolean doSecKill(String uid,String prodid){
//1、uid和proid非空判断
if (uid==null||prodid==null)
return false;
//2、连接redis
Jedis jedis =new Jedis("192.168.111.130",6379);
//3、拼接key
//库存key
String kcKey="sk:"+prodid+":qt";
//秒杀成功用户key
String userKey="sk:"+prodid+":user";
//4、获取库存,如果库存为null,秒杀还没开始
String kcNums=jedis.get(kcKey);
if (kcNums==null) {
System.out.println("秒杀还未开始");
jedis.close();
return false;
}
//5、判断用户是否重复秒杀操作
if (jedis.sismember(userKey,uid)){
System.out.println("不能重复秒杀!!");
jedis.close();
return false;
}
//6、判断商品数量,库存小于1,秒杀结束
kcNums=jedis.get(kcKey);
if (Integer.parseInt(kcNums)<1){
System.out.println("秒杀已经结束了!!");
jedis.close();
return false;
}
//7、秒杀过程:
// 库存-1
jedis.decr(kcKey);
//添加秒杀成功的用户到清单里面
jedis.sadd(userKey,uid);
System.out.println("秒杀成功了");
return true;
}
在Centos7下安装模拟并发的软件
yum install httpd-tools
使用 ab -n 1000 -c 100 -p ~/postfile -T 'application/x-www-form-urlencoded' http://10.16.23.139:8080/Seckill/doseckill 模拟
解释:-n 1000 表示当前请求1000次
-c 当前 请求中的并发次数 就是在1000里面有多少个操作是并发的
-p表示请求方式为Post,~/postfile表示的是请求所需参数放置的当前目录的文件,通过vi postfile创建并填写内容即可(以key=value&key=value方式)
-T如果为Post或Put需设置这个类型
10.16.23.139为本机ip
Seckill/doseckill为自己的请求路径
模拟高并发后出现超卖情况,还有可能出现连接超时问题,因为redis不能同时处理很大量的数据
解决超卖情况和连接超时问题
- 创建连接池解决超时问题
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* 连接池
*/
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); //测试连接 ping PONG
//基本配置 ip redis端口号 连接超时时间
jedisPool = new JedisPool(poolConfig, "192.168.111.130", 6379, 60000 );
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
- 获取Jedis对象,从连接池获取
//2、连接redis
// Jedis jedis =new Jedis("192.168.111.130",6379);
//通过连接池得到Jedis对象 解决连接超时问题
JedisPool jedisPool=JedisPoolUtil.getJedisPoolInstance();
Jedis jedis= jedisPool.getResource();
- 添加乐观锁(事务解决超卖)
结合之前的redis代码
//在进行对库存操作前进行监视库存
jedis.watch(kcKey);
//在秒杀成功时添加事务
Transaction tx=jedis.multi();//获取事务对象
// 组队
tx.decr(kcKey);//库存-1
tx.set(userKey,uid);//添加秒杀成功的用户到清单里面
//执行
List<Object> result=tx.exec();
if (result.size() == 0) {
System.out.println("秒杀失败了");
jedis.close();
return false;
}
System.out.println("秒杀成功了");
jedis.close();
return true;
加了乐观锁和连接池后的整个秒杀代码
这样会存在库存遗留
//秒杀过程
public static boolean doSecKill(String uid,String prodid){
//1、uid和proid非空判断
if (uid==null||prodid==null)
return false;
//2、连接redis
// Jedis jedis =new Jedis("192.168.111.130",6379);
//通过连接池得到Jedis对象 解决连接超时问题
JedisPool jedisPool=JedisPoolUtil.getJedisPoolInstance();
Jedis jedis= jedisPool.getResource();
//3、拼接key
//库存key
String kcKey="sk:"+prodid+":qt";
//秒杀成功用户key
String userKey="sk:"+prodid+":user";
//监视库存
jedis.watch(kcKey);
//4、获取库存,如果库存为null,秒杀还没开始
String kcNums=jedis.get(kcKey);
if (kcNums==null) {
System.out.println("秒杀还未开始");
jedis.close();
return false;
}
//5、判断用户是否重复秒杀操作
if (jedis.sismember(userKey,uid)){
System.out.println("不能重复秒杀!!");
jedis.close();
return false;
}
//6、判断商品数量,库存小于1,秒杀结束
kcNums=jedis.get(kcKey);
if (Integer.parseInt(kcNums)<1){
System.out.println("秒杀已经结束了!!");
jedis.close();
return false;
}
//添加事务
Transaction tx=jedis.multi();
// 组队
tx.decr(kcKey);//库存-1
tx.set(userKey,uid);//添加秒杀成功的用户到清单里面
//执行
List<Object> result=tx.exec();
if (result.size() == 0) {
System.out.println("秒杀失败了");
jedis.close();
return false;
}
//7、秒杀过程:
// 库存-1
// jedis.decr(kcKey);
//添加秒杀成功的用户到清单里面
// jedis.sadd(userKey,uid);
System.out.println("秒杀成功了");
jedis.close();
return true;
}
库存遗留问题
乐观锁会造成库存遗留的问题,但是redis不支持悲观锁,利用LUA来实现。
LUA脚本代码
/**
* 拼接LUA脚本
*/
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" +
//调用redis里面的sismember方法
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
//约定 秒杀过的就返回2
" return 2;\r\n" +
"end\r\n" +
//调用redis里面的get方法获取库存
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
//约定 卖完了就返回0
" 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" ;
悲观锁秒杀过程
/**
* 秒杀过程
* @param uid 用户id
* @param prodid 产品id
* @return 返回是否抢购成功
*/
public static boolean doSecKill(String uid,String prodid) {
//连接池获取Jedis对象
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
//String sha1= .secKillScript;
//通过scriptLoad方法加载脚本 secKillScript
String sha1= jedis.scriptLoad(secKillScript);
//通过evalsha方法加载,固定写法
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}