通过 redis 的 事务 和 锁机制 完成秒杀案例
redis 事务:串联多个命令执行,防止插队
命令:multi(命令组队)、exec(命令执行)、discard(放弃组队)
事务的错误处理:
(1)组队中某个命令出错,执行时,整个队列会被取消
(2)执行时,队列中某个命令出错,其他指令继续执行,整体不回回滚,即不保证原子性
Redis 乐观锁 -- watch
在执行 multi 之前,可以通过 watch key1 [key2],监视多个 key ,如果在事务执行之前这些 key 被改动,则该事务将被打断
Redis 事务特性
(1) 单独的隔离操作、(2)没有隔离级别概念、(3)不保证原子性
public class SecKill_redis {
public static void main(String[] args) {
}
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1 uid和prodid非空判断
if(uid == null || prodid == null) return false;
//2 连接redis
Jedis jedis = new Jedis("xxx.xxx.xxx.xxx",6379);
//3 拼接key
// 3.1 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":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.2 把秒杀成功用户添加清单里面
jedis.sadd(userKey,uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
}
出现问题 :(1)超卖,即秒杀成功的数量大于库存数量,最总库存呈负值
(2)连接超时,大量连接请求打到 redis 上,需要进行等待,但等待时间过长出现超时
解决方案 : (1)超卖 --> 使用事务
(2)连接超时 --> 使用连接池
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.setMaxActive(1000);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWait(100*1000);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig,"xxx.xxx.xxx.xxx",6379);
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool,Jedis jedis)
{
if(null != jedis)
{
jedisPool.returnResourceObject(jedis);
}
}
}
出现问题 : 库存遗留,即虽然并发秒杀的请求有很多,但由于乐观锁机制,导致大量同时并发的请求失败,最终库存仍有剩余
解决方案 : 使用 LUA 脚本
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid..":usr';
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtkey);
redis.call("sadd",usersKey,userid);
end
return 1;
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" ;
public static boolean doSecKill(String uid,String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
// 加载秒杀脚本
String sha1= jedis.scriptLoad(secKillScript);
// 执行脚本
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;
}