Springboot Redis模拟商品秒杀场景

Springboot Redis模拟商品秒杀场景

一、使用配置

1.1、yml配置

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
    username: root
    password: 123456
  redis:
    database: 2
    host: 127.0.0.1
    port: 6379
    password: 123456
    timeout: 10000
    lettuce:
      pool:
        max-active: 1000
        max-wait: -1
        max-idle: 10
        min-idle: 0

1.2、pom配置

		
            org.springframework.boot
            spring-boot-starter-data-redis
        

        
            org.apache.commons
            commons-pool2
            2.6.0
        

1.3、RedisConfig

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        template.setConnectionFactory(redisConnectionFactory);

        //配置序列化方式
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper obm = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和publi
        obm.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        obm.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(obm);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key 采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash
        template.setHashKeySerializer(stringRedisSerializer);
        //value
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }

}

二、商品秒杀代码

/**
     * 模拟商品秒杀场景
     * 使用乐观锁处理并发
     */
    @PassToken
    @GetMapping("/goodsKill")
    public Boolean goodsKill() {
        // 开启事务支持,在同一个 Connection 中执行命令
        redisTemplate.setEnableTransactionSupport(true);
        // 模拟一千人进行秒杀
        int userId = new Random().nextInt(1000);
        String userKey = "MS:USER";
        // 默认10件商品
        String goodsKey = "MS:GOODS";
        // 监听商品key
        redisTemplate.watch(goodsKey);
        // 判断商品是否上架
        Integer quantity = (Integer) redisTemplate.opsForValue().get(goodsKey);
        if (null == quantity) {
            System.out.println("商品还没上架,不能秒杀");
            return false;
        }
        if (0 >= quantity) {
            System.out.println("商品已经秒杀完");
            return false;
        }
        // 用户已经秒杀
        if (redisTemplate.opsForSet().isMember(userKey, userId)) {
            System.out.println("用户" + userId + ":已经参与秒杀");
            return false;
        }
        // 开启事务
        redisTemplate.multi();
        // 减库存
        redisTemplate.opsForValue().decrement(goodsKey);
        // 添加秒杀用户
        redisTemplate.opsForSet().add(userKey, userId);
        // 提交事务
        List<Object> results = redisTemplate.exec();
        if (null == results || results.size() == 0) {
            System.out.println("并发秒杀,失败了");
            return false;
        }

        System.out.println("用户" + userId + ":秒杀成功");

        return true;
    }

三、使用Lua脚本语言处理秒杀商品剩余场景

3.1、 Lua 商品秒杀脚本

local userId = KEYS[1];
local userKey = "MS:USER";
local goodsKey = "MS:GOODS";
local userExists = redis.call("sismember", userKey, userId);
if tonumber(userExists) == 1 then
    return 2;
end
local num = redis.call("get", goodsKey);
if tonumber(num) <= 0 then
    return 0;
else
    redis.call("decr", goodsKey);
    redis.call("sadd", userKey, userId);
end
return 1;

3.2、RedisTemplate执行Lua脚本

/**
     * 使用Lua脚本解决库存遗留问题
     *
     * @return
     */
    @PassToken
    @GetMapping("/luaGoodsKill")
    public boolean luaGoodsKill() {
        // resource/lua/GoodsKill.lua
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/GoodsKill.lua")));
        redisScript.setResultType(Long.class);
        // 模拟一千人进行秒杀
        Integer userId = new Random().nextInt(1000);
        // redisScript,key列表,arg(可多个),参考redis eval 命令
        Long result = (Long) redisTemplate.execute(redisScript, Arrays.asList(userId.toString()));
        if (0L == result) {
            System.out.println("商品已经秒杀完");
        } else if (1L == result) {
            System.out.println("用户" + userId + ":秒杀成功");
        } else if (2L == result) {
            System.out.println("用户" + userId + ":已经参与秒杀");
        } else {
            System.out.println("秒杀异常!!");
        }
        return true;
    }

3.3、 附源码:Lua 数据返回类型

PS: spring-boot-starter-data-redis 提供的返回类型里面不支持 Integer,使用Long代替。

package org.springframework.data.redis.connection;

import java.util.List;
import org.springframework.lang.Nullable;

public enum ReturnType {
    BOOLEAN,
    INTEGER,
    MULTI,
    STATUS,
    VALUE;

    private ReturnType() {
    }

    public static ReturnType fromJavaType(@Nullable Class<?> javaType) {
        if (javaType == null) {
            return STATUS;
        } else if (javaType.isAssignableFrom(List.class)) {
            return MULTI;
        } else if (javaType.isAssignableFrom(Boolean.class)) {
            return BOOLEAN;
        } else {
            return javaType.isAssignableFrom(Long.class) ? INTEGER : VALUE;
        }
    }
}

你可能感兴趣的:(redis,spring,boot,java)