15.Redis系列之使用Redisson分布式锁实现秒杀

本文学习采用redis官方推荐java客户端Redisson分布式锁实现秒杀,从而避免超卖、库存遗留、连接超时问题

所有代码已提交至https://gitee.com/SJshenjian/blog-code/tree/master/src/main/java/online/shenjian/redis

1. pom.xml新增依赖配置

<dependencies>
	<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.18.0</version>
        </dependency>
</dependencies>

2. 新增RedissonConfig配置

@Configuration
public class RedissonConfig {

    @Bean(name = "stringLongCodec")
    public CompositeCodec stringLongCodec() {
        return new CompositeCodec(new org.redisson.client.codec.StringCodec(), new org.redisson.client.codec.LongCodec());
    }

    @Bean(name = "stringCodec")
    public StringCodec stringCodec() {
        return new org.redisson.client.codec.StringCodec();
    }

    @Bean("redissonClient")
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.setTransportMode(TransportMode.NIO);
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://127.0.0.1:6379");
        singleServerConfig.setPassword("shenjian.online");
        singleServerConfig.setTimeout(2000);
        singleServerConfig.setDatabase(0);
        singleServerConfig.setConnectionPoolSize(1000);
        singleServerConfig.setConnectionMinimumIdleSize(2);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

3. 新增controller与service

@RestController
public class SecKillController {

    private SecKillService secKillService;

    public SecKillController(SecKillService secKillService) {
        this.secKillService = secKillService;
    }

    @PostMapping(value = "/doSecKill", produces = MediaType.APPLICATION_JSON_VALUE)
    public String doSecKill(@RequestParam String userId, @RequestParam String productId) {
        return secKillService.doSecKill(userId, productId);
    }
}

public interface SecKillService {

    String doSecKill(String userId, String productId);
}

4. 秒杀核心代码逻辑

@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService {

    private RedissonClient redissonClient;
    // redis中库中信息
    private static final String PRODUCT_STOCK_KEY = "sk_10001_stock";
    // redis中该商品秒杀到的用户列表
    private static final String PRODUCT_USER_KEY = "sk_10001_user";

    public SecKillServiceImpl(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Override
    public String doSecKill(String userId, String productId) {
        RKeys rKeys = redissonClient.getKeys();
        if (rKeys.countExists(PRODUCT_STOCK_KEY) == 0) {
            return "秒杀未开始";
        }
        RLock rLock = redissonClient.getLock(PRODUCT_STOCK_KEY + "_lock");
        try {
            boolean locked = rLock.tryLock(30, 60, TimeUnit.SECONDS);
            if (locked) {
                RAtomicLong productStock = redissonClient.getAtomicLong(PRODUCT_STOCK_KEY);
                RSet<String> productUserSet = redissonClient.getSet(PRODUCT_USER_KEY);
                if (productUserSet != null && productUserSet.contains(userId)) {
                    return "您已秒杀过,请勿重复秒杀";
                }
                if (productStock.get() <= 0) {
                    return "很遗憾,秒杀已结束";
                }
                // 加入到秒杀集合
                productUserSet.add(userId);
                // 库存减1
                productStock.decrementAndGet();
                return "恭喜您,秒杀成功!";
            }
        } catch (InterruptedException e) {
            log.error("异常:{}", e.getMessage());
        } finally {
            rLock.unlock();
        }
        return "服务器繁忙";
    }
}

5. jmeter压力测试

SecKill.jmx与userId.csv也已上传到代码resource中,可以直接导入jmeter

1

  • 秒杀未开始

当无sk_10001_stock值时,2000个用户均返回秒杀未开始

2

  • 秒杀

在redis中设置库存数为100

127.0.0.1:6379> set sk_10001_stock 100 

再次进行jmeter秒杀

用户数改为1000,错误率为0,吞吐173/sec

3

秒杀结束

4

重复秒杀

5

秒杀成功

6

继续查看redis,发现库存为0,秒杀到的用户数为100,正确, 到此秒杀实现介绍完毕,下一节我们将介绍redisson分布式锁原理及通过原生lua脚本实现秒杀逻辑

127.0.0.1:6379> get sk_10001_stock
"0"
127.0.0.1:6379> scard sk_10001_user
(integer) 100

欢迎关注公众号算法小生获取最新文章


版权声明:本文为SJshenjian原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。