一、秒杀代码
//秒杀过程
@PostMapping("/doSecKill/{uid}/{prodid}")
public boolean doSecKill(@PathVariable("uid") String uid,@PathVariable("prodid") String prodid){
//1 uid和prodid非空判断
if( uid == null || prodid == null ){
return false;
}
//2 连接redis
Jedis jedis = new Jedis("192.168.xxx.163", 6379);
//3 拼接key
//3.1 库存key
String kcKey = "sk:"+prodid+":qt";
//3.2 秒杀成功用户key
String userKey = "sk:"+uid+":user";
//4 获取库存,如果库存为null,秒杀还没有开始
String kc = jedis.get(kcKey);
if( kc == null ){
System.out.println("秒杀还没开始,请等待");
jedis.close();
return false;
}
//5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey,uid)){
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc) <= 0){
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7 秒杀过程
//7.1 库存-1
jedis.decr(kcKey);
//7.1 把秒杀成功的用户添加到清单里面
jedis.sadd(userKey,uid);
System.out.println("秒杀成功了。。。");
jedis.close();
return true;
}
二、Redis事务--秒杀并发模拟
1、使用工具ab模拟测试,
centos6默认安装,centos7需要手动安装, 联网:yum install httpd-tools
2、测试
ab -n 1000 -c 100 -p /home/du/postfile -T application/x-www-form-urlencoded http://192.168.xx.1:9000/redisTest/doSecKill
3、结果
出现了超卖
127.0.0.1:6379> get sk:0101:qt
"-4"
三、超卖问题和超时问题解决
1、超时问题
使用连接池代替客户端连接
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil(){
}
public static JedisPool getJedisPoolInstance(){
if(jedisPool == null){
synchronized (JedisPoolUtil.class){
if(jedisPool == null){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(300);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*100);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "192.168.xxx.166", 6379, 60000);
}
}
}
return jedisPool;
}
public static void release(JedisPool jedispool,Jedis jedis) {
if(null!=jedis) {
jedis.close();
}
}
}
//2 连接redis
//Jedis jedis = new Jedis("192.168.xxx.166", 6379);
//使用连接池
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
2、超卖问题
使用Redis乐观锁+Redis事务
//秒杀过程
@PostMapping("/doSecKill")
public boolean doSecKill(@RequestParam(value = "uid",required = false) String uid,@RequestParam("prodid") String prodid){
uid = String.valueOf(new Random().nextInt(10000));
//1 uid和prodid非空判断
if( uid == null || prodid == null ){
return false;
}
//2 连接redis
//Jedis jedis = new Jedis("192.168.xxx.166", 6379);
//使用连接池
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3 拼接key
//3.1 库存key
String kcKey = "sk:"+prodid+":qt";
//3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":user";
//监视库存
jedis.watch(kcKey);
//4 获取库存,如果库存为null,秒杀还没有开始
String kc = jedis.get(kcKey);
if( kc == null ){
System.out.println("秒杀还没开始,请等待");
jedis.close();
return false;
}
//5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey,uid)){
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc) <= 0){
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7 秒杀过程
//创建事务
Transaction multi = jedis.multi();
//添加组队
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List<Object> results = multi.exec();
//判断结果
if(results == null){
System.out.println("秒杀失败了。。。");
jedis.close();
return true;
}
7.1 库存-1
//jedis.decr(kcKey);
7.1 把秒杀成功的用户添加到清单里面
//jedis.sadd(userKey,uid);
System.out.println("秒杀成功了。。。");
jedis.close();
return true;
}
结果
127.0.0.1:6379> get sk:0101:qt
"0"
四、库存遗留问题
1、原因
乐观锁版本不一致,导致购买失败
2、解决
使用lua嵌入式脚本(2.6以上版本)
lua脚本优势:将复杂或者多步的Redis操作,写为一个脚本,一次提交给Redis执行,减少反复连接redis次数,提升性能;(有一定原子性,不会被其他命令插队,可以完成一些redis事务性的操作)
public class SecKill_redisByScript {
public static void main(String[] args) {
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
System.out.println(jedis.ping());
HashSet<HostAndPort> set = new HashSet<>();
doSecKill("201","0101");
}
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;";
static String secKillScript2 = "local userExists = redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n"
+"return 1";
public static boolean doSecKill(String userid,String prodid){
Jedis jedis = null;
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
try{
jedis = jedisPool.getResource();
}catch (RuntimeException e){
if(jedis != null){
JedisPoolUtil.release(jedisPool,jedis);
}
}
String sha1 = jedis.scriptLoad(secKillScript);
Object result = jedis.evalsha(sha1,2,userid,prodid);
System.out.println(result);
String reString = String.valueOf(result);
if("0".equals(reString)){
System.out.println("已抢空!!");
}else if("1".equals(reString)){
System.out.println("抢购成功!!!!!");
}else if("2".equals(reString)){
System.out.println("该用户已抢过");
}else{
System.out.println("抢购异常!!");
}
JedisPoolUtil.release(jedisPool,jedis);
return true;
}
}
3、测试
ab -n 2000 -c 300 -p /home/du/postfile -T application/x-www-form-urlencoded http://192.168.xxx.1:9000/redisTest/doSecKill
4、结果
127.0.0.1:6379> get sk:0101:qt
"500"
127.0.0.1:6379> get sk:0101:qt
"0"