一、前言
上一篇分享了redis+lua
实现分布式限流的功能,今天分享一下实现订单消费的功能,这个也是当前工作中正在开发的一个医药相关的项目,由于代码在公司内网没有办法分享出来,所有我们今天分享一个订单秒杀的功能,类似的实现原理,也是通过redis+lua
实现的,主要也是为了解决多个命令的原子性
问题。
上一篇:【技术应用】springboot+redis+lua实现分布式限流
二、方案分析
开发环境:springboot
+ redis
+ lua
思路:
1)使用redis的decr命令
进行扣减订单数量,decr
同样具有原子性;
2)在扣减订单信息的同时,也要把订单信息新增到用户的个人订单里;
三、关键代码
1、lua脚本文件
1)sismember
命令判断用户信息是否存在;
2)get
命令获取当前库存订单数量;
3)decr
命令扣减库存数量;
4)sadd
命令新增订单信息至用户信息中;
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid..":user";
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;--秒杀成功
2、初始化redisConfig
也初始化加载了ms.lua
脚本
package com.miaosha.miaosha.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import java.time.Duration;
import java.util.List;
@EnableCaching//开启缓存
@Configuration
//自定义缓存读写机制
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
//使用JackSon的redis序列化机制
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//即将废弃的方法,被下面的方法替代
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashMap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory){
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//解决查询缓存转换异常
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
//配置序列化(解决乱码问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
/**
* 载入lua脚本
* lua脚本放到了resources的根目录下
* @return
*/
@Bean
public DefaultRedisScript<List> defaultRedisScript() {
DefaultRedisScript<List> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(List.class);
defaultRedisScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("ms.lua"))
);
return defaultRedisScript;
}
}
3、java扣减业务代码
package com.miaosha.miaosha.seckill;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class SecKillRedis {
private RedisTemplate<String,Object> redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate<String,Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
private DefaultRedisScript<List> defaultRedisScript;
@Autowired
public void setDefaultRedisScript(DefaultRedisScript<List> defaultRedisScript) {
this.defaultRedisScript = defaultRedisScript;
}
/**
* lua实现秒杀
* 解决库存遗留问题,同时也解决了库存超卖问题
* @param userid
* @param prodid
* @return
*/
public Integer doSecKill(String userid, String prodid) {
//1 uid 和 proid非空判断
if (null == userid || null == prodid) {
System.out.println("系统错误");
return 1;//系统错误
}
//设置lua脚本中的keys[1]和keys[2]变量,list是追加的看好变量的顺序
List<String> keyList = new ArrayList();
keyList.add(userid);
keyList.add(prodid);
//执行lua脚本
List execute = redisTemplate.execute(defaultRedisScript, keyList);
Long res = (Long) execute.get(0);
if(res==0){
System.out.println("没有库存,秒杀已结束");
return 4;
}else if(res==1){
System.out.println("秒杀成功");
return 0;
}else if(res==2){
System.out.println("已经秒杀过");
return 3;
}else{
System.out.println("出现异常");
return 1;
}
}
}
4、Controller层代码
package com.miaosha.miaosha.controller;
import com.miaosha.miaosha.seckill.SecKillRedis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Random;
@Controller
public class GoodsController {
private SecKillRedis secKillRedis;
@Autowired
public void setSecKillRedis(SecKillRedis secKillRedis) {
this.secKillRedis = secKillRedis;
}
/**
* 提交秒杀请求
* @param prodid 商品id
* @return
*/
@PostMapping("/doseckill")
public @ResponseBody Integer doseckill(String prodid){
//用户id
String userid = new Random().nextInt(50000)+"";
return secKillRedis.doSecKill(userid,prodid);
}
}
5、事务
涉及到订单或者支付的功能,建议新增redis事务
的相关内容,这个示例没有事务的代码,后续会分享关于redis事务
的文章