天天看点

redis分布式锁实战由浅入深解决并发超卖问题

redis分布式锁是为了解决什么问题?

为了解决synchronized在分布式情况下无法实现同步的问题,因为synchronized同步的是一个jvm的方法,多个jvm的话是做不到的。

1. 实现redis 基本业务减库存场景

Integer stock = (Integer)redisTemplate.opsForValue().get("stock");
log.info("还剩"+stock);
if (stock<=0){
    log.info("抢购失败");
}
redisTemplate.opsForValue().set("stock",stock-1);
           

这段代码实现了基本业务场景,当有并发时会发生多个线程获取的stock是一样的,从而导致减1操作重复,导致超卖

2.实现redis分布式锁的基本功能

String key = "lockKey";
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1");    //setnx key 1
//如果加锁失败,说明已经其他线程已经加上锁了
if (!flag){
    log.info("系统异常,请稍后再试");
    return;
}
try{
    Integer stock = (Integer)redisTemplate.opsForValue().get("stock");   //get key

    if (stock<=0){
        log.info("抢购失败");
        return;
    }
    Long stock1 = redisTemplate.opsForValue().decrement("stock");   //decr key
    log.info("还剩"+stock1);
}finally {
    //防止代码错误导致没有删除分布式锁
    redisTemplate.delete(key);              //del key
}
           

这段代码解决了超卖问题,但是如果在业务处理过程中,应用宕机了怎么办,这个锁就锁死了。

3.应用宕机怎么办?

解决方案给redis key加上过期时间,就算宕机了过一段时间redis也会释放分布式锁

String key = "lockKey";
        //写法1
//        redisTemplate.opsForValue().setIfAbsent(key, "1");              //setnx key 1
//        redisTemplate.expire(key,10,TimeUnit.SECONDS);          //expire key 10
        //写法2
        //一条redis命令具有原子性,优于上面这种写法
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key,"1",10, TimeUnit.SECONDS);    //setnx key 1 10

        //如果加锁失败,说明其他线程已经加上锁了
        if (!flag){
            log.info("系统异常,请稍后再试");
            return;
        }
        try{
            Integer stock = (Integer)redisTemplate.opsForValue().get("stock");   //get key

            if (stock<=0){
                log.info("抢购失败");
                return;
            }
            Long stock1 = redisTemplate.opsForValue().decrement("stock");   //decr key
            log.info("还剩"+stock1);
        }finally {
            //防止代码错误导致没有删除分布式锁
            redisTemplate.delete(key);              //del key
        }
           

这时会出现一个新的问题,线程A还没走完业务锁过期释放了,这时线程B会加一把新锁,而线程A走完业务后会释放线程B的锁,这个锁加的就有问题了。

一个解决方案就是锁加久一点,并且只允许删除自己线程加的锁,这完全适用了普通业务场景,但是如果是高并发情况下,比如设置了1分钟过期时间,但加锁后系统宕机了,那么这一分钟这个方法是锁死的,对于系统来说是极其致命的。

高并发场景下设置多久的过期时间都存在问题,这时就需要加一个锁续命功能。当锁到期还没执行完业务的情况下,再给锁设置一个过期时间,直到当前线程业务同步方法执行完毕。

4.使用redisson来实现该功能

该框架已经替我们解决了这些事情,这里只简单使用一下,具体用法还是去官网看看。

引入依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.1</version>
</dependency>
           

配置类:

@Bean
public Redisson redisson() {
    Config config = new Config();
    // 此为单机模式
    config.useSingleServer().setAddress("redis://127.0.0.1:6379s").setDatabase(0);
    return (Redisson) Redisson.create(config);
}
           

同步代码:

String key = "lockKey";
//获取redisson锁
RLock lock = redisson.getLock(key);
try{
    //加锁 默认为非公平锁 加锁时间30s 会自动续命
    lock.lock();
    Integer stock = (Integer)redisTemplate.opsForValue().get("stock");   //get key

    if (stock<=0){
        log.info("抢购失败");
        return;
    }
    Long stock1 = redisTemplate.opsForValue().decrement("stock");   //decr key
    log.info("还剩"+stock1);
}finally {
    lock.unlock();
}
           

这样就使用redis分布式锁解决了并发问题。

继续阅读