Redis分布式锁

在高并发情况下,经常会出现数据问题,以下展示了redis分布式锁的演进过程。

1.使用synchronize关键字

使用synchronize进行并发控制,在单体架构(单机环境)中可以正常运行,但是分布式应用中,就会出现多个请求同时分发到不同的应用实例(tomcat),各实例并发执行减库存操作,导致数据不一致问题。假设服务实例1和2查询到当前库存为100,两个实例同时执行减一操作,并将库存设置为99,这时库存数量就不正确了。

@RestController("/redis")
class RedisTest {
    
    @RequestMapping("/reduceStock")
    public String reduceStock {
        synchronized(this) {
            return reduce();
        }
    }
    
    private String reduce() {
        int stock = Integer.parseInt(jedis.get("stock"));
        if(stock > 0) {
            stock--;
            jedis.set("stock", stock);
            return "剩余库存:" + stock;
        } else {
            return "库存不足!";
        }
    }
}

 2.使用setnx命令加锁

setnx key value 只有在key不存在的时候,才会将key的值设置为value

使用setnx获取锁成功后减库存,之后再释放锁。这种虽然可以使用try捕获程序中出现的异常,但是如果在执行finally之前整个服务器挂了,还是会造成锁没有释放,最终导致后面的请求都无法再减库存。

public String reduceStock() {
    final String LOCK_KEY = "lockKey";
    Boolean locked = jedis.setIfAbsent(LOCK_KEY, true);
    if(!locked) {
        return "获取分布式锁失败!";
    }
    try {
        reduce();
    } finally {
        jedis.delete(LOCK_KEY);
    }
}

3. setnx加锁时设置过期时间

在setnx获取锁时,给LOCK_KEY加上过期时间
Boolean locked = jedis.setIfAbsent(LOCK_KEY, true, 10, TimeUnit.SECOND);

注意上面这句不能替换成下面两句,因为这两句不是原子操作,有可能在执行setnx之后失败了,导致后面无法设置过期时间,造成永远无法获取该锁的问题。
Boolean locked = jedis.setIfAbsent(LOCK_KEY, true);
jedis.expire(LOCK_KEY, 10, TimeUnit.SECOND);

4. 解决锁提前过期问题

但是上面简单地设置过期时间还是会有问题,可能还没执行完业务代码,锁就过期了,这时有新的请求过来就能拿到锁了。比如锁10s过期,这时虽然该线程还没执行完,但锁已经失效了,这时其他线程又可以获取该锁了。
a. 第0s:假设线程1持有锁,并设置锁过期时间为10s
b. 第10s:锁失效了,这时候有新的请求过来,线程2能拿到了锁
c. 第15s:线程1处理完业务逻辑,准备释放锁,这时删除的锁是线程2加的锁,就出现了本线程加的锁被其他线程释放掉。

下面的方式可以确保本线程不会删除其他线程的锁,但是还是会存在线程1还没执行完,线程2又拿到锁也并发执行的问题。所以这种方式没有彻底解决高并发问题。

public String reduceStock() {
    final String LOCK_KEY = "lockKey";
    String threadId = UUID.randomUUID().toString();
    Boolean locked = jedis.setIfAbsent(LOCK_KEY, threadId, 10, TimeUnit.SECOND);
    if(!locked) {
        return "获取分布式锁失败!";
    }
    try {
        reduce();
    } finally {
        if(threadId.equals(jedis.get(LOCK_KEY))) {
            jedis.delete(LOCK_KEY);
        }
    }
}

5. 使用timer定时器更新过期时间

假设设置LOCK_KEY的过期时间为30s,可以每隔10s重新更新LOCK_KEY的过期时间为30s,延长锁的持有时间

注意:过期时间时间不能设置太长,否则其他线程阻塞的时间就越长;刷新的时间间隔一般为过期时间的1/3。

6. 使用redisson

redisson已经有一套完善的加锁机制,可以直接用它的api进行加锁。它相当于帮我们实现了第5种方案,获取锁时设置锁过期时间为30s,然后启动一个后台线程,每隔10s重新设置LOCK_KEY的过期时间为30s直至该线程结束。

 

public String reduceStock() {
    RLock redissonLock = redisson.getLock(LOCK_KEY);
    try {
        redissonLock.lock();
        reduce();
    } finally {
        redissonLock.unlock();
    }
}

存在问题:

1. 如果redis是集群架构,比如主从架构,假设线程A在master上加了锁,结果master挂了,而且锁没有及时同步到slave上。当slave升级为master后,其他线程检测到新的master上没有加锁,便会获取锁,从而造成并发执行的问题。

2. 分布式锁从底层原理来说就是将并发请求处理成串行队列,虽然redis处理速度快,但想要追求更高并发的性能,还要另寻他法。

posted @ 2020-08-22 17:33  安小  阅读(171)  评论(0编辑  收藏  举报