redis 单机锁 分布式锁

  • Post author:
  • Post category:其他



单机锁:

多线程访问redis缓存时, 要保证数据同步,可以给redis里的数据加锁。

加锁有两种方式, 一种是使用java 的关键字 synchronized , 一种是使用ReentratLock类。

synchronized加锁手段非常强硬,一旦锁住,除非块内程序执行完成,否则不开锁。其他线程想要访问就得一直等待。

而RenntraLock类的加锁方式比较温和。 一个线程使用RenntraLock去抢锁, 抢到了就执行代码, 抢不到可以在else中处理抢不到锁的代码。 还可以设置超时时间,抢不到的话,在指定的时间内继续抢,时间一到,则不再抢锁。 和synchronized不同,synchronized抢不到会一直抢,直到抢到才罢休。

RenntraLock代码示例:

        Lock lock = new ReentrantLock();
        if(lock.tryLock())
        {
            lock.lock();
            //业务逻辑
        }
        else
        {
            // 没有抢到线程的业务逻辑--放弃执行
            lock.unlock();
        }

synchronized代码示例:

        synchronized(this)
        {

        }


分布式锁:

synchronized和ReentratLock只适用于单例服务,并不适用于微服务模式。 synchronized 和 ReentratLock 加锁的作用域只是当前服务。 在当前服务内的多进程下可以保证数据唯一性。但是当在分布式环境下,有多个相同的服务部署在不同服务器时, 使用redis会出现风险。 synchronized不可能锁住好几个服务的数据。 此时需要使用redis自带的set方法来完成锁数据的操作。

setIfAbsent方法提供在分布式环境下对数据的加锁处理。
       /**
         * stringRedisTemplate 提供setIfAbsent方法对指定的内容加锁。
         * 适用于分布式架构
         * 按照唯一键lockKey加锁, 返回boolean类型的加锁结果
         * setIfAbsent表示如果已经存在lockKey的锁,则不会继续加锁,返回false表示加锁失败
         * 如果不存在lockKey的锁,则对value进行加锁,然后执行业务代码
         */
        String lockKey = UUID.randomUUID().toString()+ "serverName";
        String value = "被加锁的内容";
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey , value);
        if(!flag)
        {
            // 加锁失败
        }
        else
        {
            //指定业务代码后,释放锁
            stringRedisTemplate.delete(lockKey);
        }

上述代码可以完成分布式加锁,但存在一个问题。 如果程序执行到业务代码中时,抛出异常了怎么办? 应为解锁代码在业务代码之后,也就是多要在业务代码执行完成后在解锁。 但是如果业务代码指定一半是抛出了异常,那么最后的解锁代码也执行不到。

为了解决这个问题, 需要对从加锁开始到执行业务代码 进行 try-catch处理。 在finally语句中释放锁。 这样不论业务代码有没有抛出异常,最后都会解锁。

  /**
         * stringRedisTemplate 提供setIfAbsent方法对指定的内容加锁。
         * 适用于分布式架构
         * 按照唯一键lockKey加锁, 返回boolean类型的加锁结果
         * setIfAbsent表示如果已经存在lockKey的锁,则不会继续加锁,返回false表示加锁失败
         * 如果不存在lockKey的锁,则对value进行加锁,然后执行业务代码
         */
        String lockKey = UUID.randomUUID().toString()+ "serverName";
        String value = "被加锁的内容";
        try
        {
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey , value);
            if(!flag)
            {
                // 加锁失败
            }
            else
            {
                //执行业务代码

            }

        }
        catch (Exception e)
        {

        }
        finally {
            stringRedisTemplate.delete(lockKey);
        }
        return ResponseResult.ok(message);
    }

在解决了程序因为抛异常而无法执行到解锁语句的问题后, 还有一个问题没有解决。 那就是: 如果程序执行到业务代码,但是整个宕机了,整个服务挂掉了,可能是谁杀了服务的进行。这时候哪怕是有finally语句,因为进程被杀了,也无法执行到。 但是redis还正常运行。 被加锁的数据因此无法解锁。 这时候该怎么办? 所以,为了因对这种情况, 在对数据加锁时, 需要对数据指定超时时间。 指定超时时间后,即时服务宕机无法执行到解锁语句,在redis中加锁的数据也有一个超时时间,超时后对自动删除。

// 给被加锁数据设置一个超时时间
stringRedisTemplate.expire(lockKey , 10, TimeUnit.SECONDS);


// 要保证加锁和给所一个时间 是一个原子操作
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey , value , 10 , TimeUnit.SECONDS);



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