redis
秒杀案例
以上为例
我们创建一个项目
Springbooy : serkill
问题思考
秒杀要解决什么问题
1.超卖
2.连接超时
3.库存遗留
编写秒杀过程:doseckill’方法
public boolean doSecKill(String uid,String prodid)
{
Jedis jedis = new Jedis("120.79.14.203",6379);
jedis.auth("123456");
//1:uid和proid的非空判断
if (uid==null||prodid==null){
return false;
}
System.out.println(uid);
System.out.println(prodid);
//3.1库存key
String kckey = "sk"+prodid+"qt";
//3.2秒杀成功用户key
String userkey = "sk"+prodid+"user";
//4 获取库存本身等于空,秒杀还没有开始
jedis.watch(kckey);
System.out.println(kckey);
String s = jedis.get(kckey);
if (s==null){
System.out.println("秒杀还没有开始,请等待");
return false;
}
//5.用户是否重复秒杀操作
Boolean member = jedis.sismember(userkey, uid);
if (member){
System.out.println("你已经秒杀过了不要再次重复的秒杀");
return false;
}
//6.秒杀的过程
if (Integer.parseInt(s)<=0){
System.out.println("秒杀已经结束了");
return false;
}
//7秒杀过程
Transaction multi = jedis.multi();
//7.1库存-1
multi.decr(kckey);
//7.2把秒杀成功的用户添加到redis
multi.sadd(userkey,uid);
List exec = multi.exec();
System.out.println(exec);
if (exec==null || exec.size()==0){
System.out.println("秒杀失败了");
return false;
}
System.out.println("秒杀成功");
return true;
}
前端写一个简单的表单
之后使用阿帕奇的jmeter来测试
并发测试之后
会发现
有库存遗留,并没有卖完,这里并发的并发问题可以用脚本语言 : lua来解决
简单介绍一下
LUA脚本在Redis中的优势
- 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
- LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
- 但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
利用lua脚本淘汰用户,解决超卖问题. - redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。解决例如 2000用户秒杀 800库存 却还剩下600 并发问题
lua脚本业务类编写
package com.hyc.serkill.config;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class SecKill_redisByScript {
private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;
public static void main(String[] args) {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
System.out.println(jedis.ping());
Set<HostAndPort> set=new HashSet<HostAndPort>();
// doSecKill("201","sk: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";
}
脚本代码解读
大致来为大家读一下这个脚本代码的意思哈,我本人也没有学过lua但是看是可以看懂一些的
//获得参数1
"local userid=KEYS[1];\r\n" +
//获得参数2
"local prodid=KEYS[2];\r\n" +
//生成秒杀库存key
"local qtkey=\'sk\'..prodid..\"qt\";\r\n" +
//生成秒杀库存key
"local usersKey=\'sk\'..prodid..\":usr\"\r\n" +
//判断redis查找set集合中userid的数字
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
//如果返回是1那么表示已经秒杀过了,retrun2:代表抢购过了,方便后续调用判断
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
//获取库存
"local num= redis.call(\"get\" ,qtkey);\r\n" +
//判断如果小于等于0那么返回0 表示已经没有了
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
//要是不等于0执行库存减少操作,将用户的id存入道用户key中,返回1 代表秒杀成功
"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= .secKillScript;
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;
}
恢复库存,重新测试
结果
这样就不会出现之前那种
成功失败穿插的问题了,一个线程再用的时候不会被其他线程插队,抢夺资源,很棒
并发下的库存遗留问题解决了
连接超时问题
最后就是连接问题了
我们用节省每次连接redis服务带来的消耗,把连接好的实例反复利用。
通过参数管理连接的行为
主要用到了 :链接池参数
-
MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
-
maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
-
MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
-
lestOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
jedis工具类业务实现~
package com.hyc.serkill.config;
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);
//最多有30左右的空闲实例
poolConfig.setMaxIdle(32);
//连接超时毫秒数
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
// ping PONG
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "120.79.14.203", 6379, 60000 ,"123456");
}
}
}
return jedisPool;
}
//资源回收
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
总结
我们解决了秒杀并发中的三个比较关键的问题
- 超卖
- 库存剩余(本来该卖出去的却没卖完)
- 连接可能会超时的问题
请发表评论