15.Redis系列之事务和锁机制及Redisson分布式锁实现秒杀

1. 事务和锁机制

我们学习redis的乐观锁机制与事务的三大特性

1.1 基本操作

127.0.0.1:6379> set balance 100
OK
# 在shell1中执行watch监听balance
127.0.0.1:6379> watch balance
OK
# 在shell2中执行watch监听balance
127.0.0.1:6379> watch balance
OK
# 在shell1中执行multi开始事务
127.0.0.1:6379> multi
OK
# 在shell2中执行multi开始事务
127.0.0.1:6379> multi
OK
# 在shell1中执行incrby增加余额50
127.0.0.1:6379(TX)> incrby balance 50
QUEUED
# 在shell2中执行incrby减少余额50
127.0.0.1:6379(TX)> decrby balance 50
QUEUED
# 在shell1中执行exec执行事务失败,因为监视到了key被其他命令所改动
127.0.0.1:6379(TX)> exec
(nil)
# 在shell2中执行exec执行事务成功
127.0.0.1:6379(TX)> exec
1) (integer) 50
# unwatch取消对所有key的监视
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> decrby balance 55
QUEUED
# discard方式事务
127.0.0.1:6379(TX)> discard
OK

1.2 redis事务三大特性

  • 单独的隔离操作 事务中的所有命令都会按顺序一一执行。事务执行过程中,不会被其他客户端所打断
  • 没有隔离级别的概念 当事务开启时,事务期间的命令并没有执行,而是加入队列,只有执行EXEC命令时,才会执行
  • 不保证原子性 事务中如果一条命令执行失败,其他的命令仍会执行,不会回滚
127.0.0.1:6379> set a 100
OK
127.0.0.1:6379> set b 100
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby a 50a
QUEUED
# decrby b命令仍会执行,不会回滚
127.0.0.1:6379(TX)> decrby b 50
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) (integer) 50
127.0.0.1:6379> get b
"50"
127.0.0.1:6379> get a
"100"

2. Redisson分布式锁实现秒杀

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

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

2.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.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;
    }
}

2.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);
}

2.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 "服务器繁忙";
    }
}

2.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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法小生Đ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值