# springboot-redis-lua **Repository Path**: lh1293/springboot-redis-lua ## Basic Information - **Project Name**: springboot-redis-lua - **Description**: springboot+redis+lua实现商品秒杀-demo - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 3 - **Created**: 2022-03-18 - **Last Updated**: 2023-11-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Springboot+Redis+Lua ## 介绍 springboot+redis+lua实现商品秒杀demo 经过ab测试,基本解决了超卖问题和库存遗留问题 没有加持久化,只是一个demo,实际项目要复杂的多 ## 项目 > 版本说明 * springboot 2.6.4 * 如果使用的是2.0之前的版本,spring-boot-starter-data-redis使用的jedis,需要加上连接池,要不然会有超时问题 * 2.0之后的版本,spring-boot-starter-data-redis使用的是Lettuce,是基于Netty的 * redis 6.2.6 * 系统 centos7 > 主体代码 下面是完整案例,包括了超卖问题的两种解决方式,如果使用redis事务只解决了超卖,没有解决库存遗留 ==使用lua既可以解决超卖问题,又可以解决库存遗留问题== pom.xml ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.6.4 com.miaosha miaosha 0.0.1-SNAPSHOT miaosha miaosha 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-thymeleaf src/main/resources **/*.* org.springframework.boot spring-boot-maven-plugin ``` application.properties ```properties #设置thymeleaf模板引擎的前后缀 spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html #关闭模板缓存 spring.thymeleaf.cache=false spring.redis.host=192.168.130.100 spring.redis.port=6379 #redis数据库索引 spring.redis.database=0 ``` RedisConfig ```java 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 redisTemplate(RedisConnectionFactory factory){ RedisTemplate template = new RedisTemplate<>(); RedisSerializer redisSerializer = new StringRedisSerializer(); //使用JackSon的redis序列化机制 Jackson2JsonRedisSerializer 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); 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 redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer 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 defaultRedisScript() { DefaultRedisScript defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(List.class); defaultRedisScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("ms.lua")) ); return defaultRedisScript; } } ``` lua脚本 ```lua 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;--秒杀成功 ``` SecKillRedis 秒杀实现类 ```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 redisTemplate; @Autowired public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } private DefaultRedisScript defaultRedisScript; @Autowired public void setDefaultRedisScript(DefaultRedisScript 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 keyList = new ArrayList(); keyList.add(userid); keyList.add(prodid); //执行lua脚本,写的时候想扩充一下,所以返回了list类型,但因为我懒,没写扩充,所以凑活着用吧 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; } } /** * 秒杀 * 开启事务解决库存超卖,但会产生库存遗留问题 * @param userid 用户id * @param prodid 商品id * @return */ public Integer doSecKill_back(String userid, String prodid) { //1 uid 和 proid非空判断 if(null==userid || null==prodid){ System.out.println("系统错误"); return 1;//系统错误 } //拼接相关的key //库存key //秒杀成功用户列表key //sk:0101:qt String kcKey = "sk:"+prodid+":qt"; String userKey = "sk:"+prodid+":user"; //秒杀过程 SessionCallback callback = new SessionCallback() { @Override public Object execute(RedisOperations operations) throws DataAccessException { //监视库存 operations.watch(kcKey); //获取库存,如果库存为null,秒杀没有开始 if(null==redisTemplate.opsForValue().get(kcKey)){ System.out.println("秒杀没有开始"); return 2;//秒杀没有开始 } Integer kc = (Integer) redisTemplate.opsForValue().get(kcKey); //判断用户是否重复做秒杀操作 //判断userid是否在清单集合中 Boolean is = redisTemplate.opsForSet().isMember(userKey, userid); if(Boolean.TRUE.equals(is)){ System.out.println("已经秒杀成功,请勿重复秒杀"); return 3;//已经秒杀成功,请勿重复秒杀 } //判断如果商品的数量,库存数量小于1,秒杀结束 if(kc<1){ System.out.println("秒杀已结束"); return 4;//秒杀已结束 } //使用事务 operations.multi(); //7.1 库存-1 operations.opsForValue().decrement(kcKey); //7.2 秒杀成功的用户加入清单 operations.opsForSet().add(userKey,userid); //提交事务 List exec = operations.exec(); if(exec.size()==0){ System.out.println("秒杀失败了"); return 5; } System.out.println("秒杀成功了"); return 0; } }; Object execute = redisTemplate.execute(callback); return (Integer) execute; } } ``` GoodsController 注意ab测试时使用的是post提交 `ab -n 3000 -c 500 -p /postfile -T application/x-www-form-urlencoded http://192.168.0.102:8080/doseckill` ```java 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; } @RequestMapping("/ms") public String ms(){ return "index"; } /** * 提交秒杀请求 * @param prodid 商品id * @return */ @PostMapping("/doseckill") public @ResponseBody Integer doseckill(String prodid){ //用户id String userid = new Random().nextInt(50000)+""; return secKillRedis.doSecKill(userid,prodid); } } ``` index.html ```html Title

哈哈哈-大汽车 1元秒杀

```