简单利用redis的LUA脚本功能,一次性操作,实现原子性扣减库存
注释都写得明白,大家凑合着看吧,没有增加库存,直接是初始化一次库存量,后面等过期失效
特别注意一点,就是在集群模式下,需要解决依赖问题
第二个是,序列化的时候,需要把int long类型能转成功
先增加依赖
<!--redis 两种分布式锁依赖包 lettuce 去掉这两个插件,扣库存的分布式连接有问题-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
序列化RedisTemplate
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(genericToStringSerializer);
return redisTemplate;
}
下面是业务的代码块,核心 点还是在扣库存的时候,不能超发,也不能扣到负数,
然后再同步到MYSQL里,初始化库存数量,这个可以从DB里取实际的量,
LUA脚本相对简单点,扣减逻辑都在里面,减少IO并且也是保证原子性问题,
/**
* RedisTemplate 通过 LUA脚本去库存操作
* @author sky
*/
@Component
public class RedisTemplateStockUtil implements IStockCallback {
Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedissonClient redissonClient;
/**
* 库存还未初始化
*/
public static final long UNINITIALIZED_STOCK = -3L;
/**
* Redis 客户端
*/
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
private static final String REDIS_KEY = "redis_key:stock:";
/**
* 执行扣库存的脚本
*/
public static final String STOCK_LUA;
static {
/*
*
* @desc 扣减库存Lua脚本
* 库存(stock)-1:表示不限库存
* 库存(stock)0:表示没有库存
* 库存(stock)大于0:表示剩余库存
*
* @params 库存key
* @return
* -3:库存未初始化
* -2:库存不足
* -1:不限库存
* 大于等于0:剩余库存(扣减之后剩余的库存),直接返回-1
*/
StringBuilder sb = new StringBuilder();
sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
sb.append(" local num = tonumber(ARGV[1]);");
sb.append(" if (stock == -1) then");
sb.append(" return -1;");
sb.append(" end;");
sb.append(" if (stock >= num) then");
sb.append(" return redis.call('incrby', KEYS[1], 0 - num);");
sb.append(" end;");
sb.append(" return -2;");
sb.append("end;");
sb.append("return -3;");
STOCK_LUA = sb.toString();
}
@Override
public long getStock(String batchNo, long expire, int num) {
String key = REDIS_KEY+batchNo;
// redisTemplate.delete(key);
// 初始化库存
long stock = stock(key, num);
// 没有什么作用,可省略
/*boolean hasKey = Boolean.TRUE.equals(redisTemplate.hasKey(key));
if (hasKey){
Integer batchNoLock = (Integer) redisTemplate.opsForValue().get(key);
log.info("========== batchNoLock :{}",batchNoLock);
log.info("======================== hasKey :{}",hasKey);
}*/
if (stock == UNINITIALIZED_STOCK) {
RLock rLock = redissonClient.getLock(REDIS_KEY+":lock");
try {
// 获取锁
if (rLock.tryLock(1, TimeUnit.SECONDS)) {
// 双重验证,避免并发时重复回源到数据库
stock = stock(key, num);
if (stock == UNINITIALIZED_STOCK) {
// 获取初始化库存 initStock = 100 参数
int initStock = initStock(1);
// 将库存设置到redis, 100是初始化库存参数
ValueOperations<String, Integer> valueOperations = redisTemplate.opsForValue();
valueOperations.set(key, 100, expire, TimeUnit.SECONDS);
// 调一次扣库存的操作
stock = stock(key, num);
Object stockNum = redisTemplate.opsForValue().get(key);
Integer batchNoLock = Integer.parseInt((String) stockNum);
log.info("batchNoLock key :{}",batchNoLock);
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (rLock != null && rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
log.info("stock num :{}",stock);
return stock;
}
/**
* 扣库存 这步特别注意,分布式连接有问题,需要依赖包里,去掉lettuce组件
*
* @param key 库存key
* @param num 扣减库存数量
* @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
*/
private Long stock(String key, int num) {
// 脚本里的KEYS参数
List<String> keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List<String> args = new ArrayList<>();
args.add(Integer.toString(num));
return (Long) redisTemplate.execute((RedisCallback<Long>) connection -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
}
return UNINITIALIZED_STOCK;
});
}
/**
* 获取初始的库存
*
* @return
*/
private int initStock(long commodityId) {
// TODO 这里做一些初始化库存的操作
return 10;
}
调用 方法
@RequestMapping(value = "/stock/{batchNo}", method = RequestMethod.GET)
public ResponseEntity<Object> reduceStock(@PathVariable String batchNo) {
long numb = iStockCallback.getStock(batchNo,20,1);
return ResponseEntity.ok("秒杀活动,扣减库存 num:" +numb);
}