三个版本代码百度网盘链接:
链接:https://pan.baidu.com/s/1KpkKpp4F3-7j-WVheGg4Yg
提取码:4unw
1、秒杀逻辑
秒杀:解决计数器和人员记录的事务操作
- uid和proid非空判断
- 连接redis
- 拼接key
- 获取库存,如果库存为null,秒杀还没开始
- 判断用户是否重复秒杀操作
- 判断商品数量,库存数量小于1,秒杀结束
- 秒杀过程
2、存在问题
2.1、连接超时
原因:由于大量创建连接,十分消耗性能,并且有时获取连接不及时,出现连接超时的情况
2.2、超卖
在并发的情况下发生的,就是在输出没有库存(秒杀结束)后还有商品售出导致库存数量为负数。
2.3、库存遗留
使用乐观锁解决问题2之后,出现问题3
如果库存数量相对并发更多,由于使用乐观锁,第一个用户秒杀成功后会修改库存键的版本号,其他抢到的用户会因为版本号不同导致无法继续购买,就会有库存遗留问题
3、解决
3.1、连接超时
使用连接池,工具类如下:
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);
jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 60000);
}
}
}
return jedisPool;
}
}
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
springBoot版本(pom.xml引入,application.yml配置,然后注入对象即可)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 1800000
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 5
min-idle: 0
@Autowired
private RedisTemplate redisTemplate;
3.2、超卖问题
使用Redis事务,乐观锁 + watch
//监视库存
jedis.watch(kcKey);
//中间代码忽略
//7 秒杀过程
//使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List<Object> results = multi.exec();
if(results == null || results.size()==0) {
System.out.println("秒杀失败了....");
jedis.close();
return false;
}
3.3、乐观锁导致的库存遗留问题
使用Lua嵌入式脚本语言
- 将复杂的或者多步的 Redis 操作,写为一个脚本,一次提交给Redis运行,减少反复连接 reids的次数。提升性能。
- LUA脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成redis事务性的操作
- LUA脚本功能,在Redis 2.6以上的版本才可以使用
- 利用 lua 脚本淘汰用户,解决超卖问题。
- redis 2.6 版本以后,通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
local userid=KEYS[1]; //1、2行定义两个变量,
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt"; //3,4行定义拼接key
local usersKey="sk:"..prodid..":usr";
local userExists=redis.call("sismember",usersKey,userid); //5-8,判断用户是否存在,不存在return 2
if tonumber(userExists)==1 then
return2;
end
local num=redis.call("get",qtkey); //9-11,判断商品是否存在
if tonumber(num)<=0 then
return 0;
else //12-15,用户和商品操作
redis.call("decr",qtkey);
redis.call("sadd",usersKey,userid);
end
return1; //最后一行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();
jedis.select(2);
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;
}
|