Redis秒杀案例分析

Redis数据库中记录商品库存和秒杀成功人员名单

prodid: 商品号

高并发下超卖问题

如果不加锁的情况下 多个用户在同一时间拿到的库存量是一样的 然后他们都去减少这个库存 最终导致库存数超卖 为负值


利用Redis默认的乐观锁淘汰用户解决超卖问题

//增加乐观锁
jedis.watch(qtkey);

//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null || "".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}

int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}

//增加事务
Transaction multi = jedis.multi();

//4.减少库存
//jedis.decr(qtkey);
multi.decr(qtkey);

//5.加人
//jedis.sadd(usrkey, uid);
multi.sadd(usrkey, uid);

//执行事务
List<Object> list = multi.exec();

//判断事务提交是否失败
if(list==null || list.size()==0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.err.println("秒杀成功");
jedis.close();


`已经秒杀完 但仍然有库存的情况`

已经秒光,可是还有库存。原因,就是乐观锁导致很多请求都失败。先点的没秒到,后点的可能秒到了

使用Lua脚本

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

利用lua脚本淘汰用户,解决超卖问题。

redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题

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;


SpringBoot配置Redis连接池

连接超时问题 – 通过连接池解决

#首先是redis各个参数的配置,在 application-dev.properties中配置如下:
#redis配置
redis.server.host=192.168.50.162
redis.server.port=6379
redis.server.password=password
redis.server.timeOut=5000
redis.server.maxIdle=50
redis.server.maxWaitMillis=5000
redis.server.maxTotal=500

对 redis 配置参数进行读取和绑定,配置属性注入到 JedisProperties

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = JedisProperties.JEDIS_PREFIX)
public class JedisProperties {

public static final String JEDIS_PREFIX = "redis.server";

private String host;

private int port;

private String password;

private int maxTotal;

private int maxIdle;

private int maxWaitMillis;

private int timeOut;

public int getTimeOut() {
return timeOut;
}

public void setTimeOut(int timeOut) {
this.timeOut = timeOut;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}

public int getPort() {
return port;
}

public void setPort(int port) {
this.port = port;
}

public int getMaxTotal() {
return maxTotal;
}

public void setMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
}

public int getMaxIdle() {
return maxIdle;
}

public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}

public int getMaxWaitMillis() {
return maxWaitMillis;
}

public void setMaxWaitMillis(int maxWaitMillis) {
this.maxWaitMillis = maxWaitMillis;
}

}

配置了 Redis 连接池之后,将 Redis 连接池 注入到 RedisClient 中,并生成 RedisClient Bean

import com.mljr.auth.config.properties.JedisProperties;
import com.mljr.auth.util.RedisClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
@EnableConfigurationProperties(JedisProperties.class)
@ConditionalOnClass(RedisClient.class)
public class JedisConfig {

private Logger logger = LoggerFactory.getLogger(JedisConfig.class);

@Autowired
private JedisProperties prop;

@Bean(name = "jedisPool")
public JedisPool jedisPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(prop.getMaxTotal());
config.setMaxIdle(prop.getMaxIdle());
config.setMaxWaitMillis(prop.getMaxWaitMillis());
return new JedisPool(config, prop.getHost(), prop.getPort(), prop.getTimeOut(), prop.getPassword());
}

@Bean
@ConditionalOnMissingBean(RedisClient.class)
public RedisClient redisClient(@Qualifier("jedisPool") JedisPool pool) {
logger.info("初始化……Redis Client==Host={},Port={}", prop.getHost(), prop.getPort());
RedisClient redisClient = new RedisClient();
redisClient.setJedisPool(pool);
return redisClient;
}

}

配置一些常用的 redis 的操作:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class RedisClient {

private JedisPool jedisPool;

public void set(String key, Object value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set(key, value.toString());
}catch (Exception e){
e.printStackTrace();
}finally {
jedis.close();
}
}

/**
* 设置过期时间
* @param key
* @param value
* @param exptime
* @throws Exception
*/
public void setWithExpireTime(String key, String value, int exptime) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set(key, value, "NX", "EX", 300);
} catch (Exception e){
e.printStackTrace();
}finally {
jedis.close();
}
}

public String get(String key) {

Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.get(key);
} catch (Exception e){
e.printStackTrace();
}finally {
if (jedis != null)
jedis.close();
}
return null;
}
public JedisPool getJedisPool() {
return jedisPool;
}

public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}

}

大功告成,使用注解的方式引入就可以使用了

@Autowired
private RedisClient redisClient;
....
....
....