使用redis的事务完成秒杀案例,流程图如下:
问题一:连接超时
连接超时问题可以使用redis连接池来进行解决
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 设置连接池中一个pool可以设置多少个jedis实例
poolConfig.setMaxTotal(200);
// 设置一个pool中最多有多少个状态为idle(空闲)
poolConfig.setMaxIdle(32);
// 表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
// 获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
poolConfig.setTestOnBorrow(true); // ping PONG
jedisPool = new JedisPool(poolConfig, "192.168.119.130", 6379, 60000 );
}
}
}
return jedisPool;
}
问题二:超卖问题
超卖问题可以使用redis的事务watch(乐观锁)解决超卖问题
// 创建事务(使用watch乐观锁的时候会有库存遗留的问题)
Transaction multi = jedis.multi();
// 组队操作
// 将商品数量减一
multi.decr(productKey);
// 将秒杀用户key入库
multi.sadd(userKey, uid);
// 执行
List<Object> results = multi.exec();
if (results.size() == 0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
问题三:库存遗留问题
乐观锁导致很多请求都失败,先点的没秒到,后点的可能秒到了,使用LUA脚本解决库存遗留问题。
LUA脚本在Redis中的优势
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
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" ;
最终代码实现如下:
// 定义两段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" +
"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 uid,String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
jedis.select(2);
// 通过jedis的scriptLoad方法加载Lua脚本
String sha1= jedis.scriptLoad(secKillScript);
//通过jedis的evalsha方法调用Lua脚本
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;
}