面试准备 -- redis实现分布式锁

昨天,我们介绍了 zookeeper 实现分布式锁的原理和具体实现。今天,我们来学习使用 Redis 来做分布式锁。

下面我们来简单实现一个分布式锁:

 public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        //设置锁
        Long result = jedis.setnx("key", "value");
        try {
            //加锁成功,设置锁过期时间
            if (result==1) {
                jedis.expire("key", 60 * 5);
                //模拟业务(为了简单演示先这样做)
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        	//删除锁(释放锁的意思)
            jedis.del("key");
        }
    }

setnx 这个方法是在插入前判断这个键是否已经存在,存在返回 0,不存在返回 1 。但是上述方法存在几个弊端。

弊端一
正常情况下,我们设置了这个键,然后会对这个键设置过期时间,这就算是成功拿到了锁了。
面试准备 -- redis实现分布式锁_第1张图片
假设我们setnx成功了,但是突然间我们程序挂掉了,那锁会一直存在,就算其他实例还活着的情况下,永远也拿不到锁了。
面试准备 -- redis实现分布式锁_第2张图片

弊端二
上述代码中,我们持有锁的时间是 5 分钟,5分钟后锁会自动过期。假设我们的线程 A 执行业务的时间超过了 5 分钟,这时我们的锁失效了。

线程 B 拿到了锁,在执行过程中。恰好线程 A 执行完,然后释放锁,结果把线程 B 的锁给释放掉了。接下来程序都乱套。

线程 A 拿到锁并设置了锁过期时间。
面试准备 -- redis实现分布式锁_第3张图片
结果,由于某种情况,执行时间超过了锁设置的过期时间,锁被自动释放了。
面试准备 -- redis实现分布式锁_第4张图片
这时,线程 B 过来了,拿到锁并设置过期时间。

面试准备 -- redis实现分布式锁_第5张图片
又是那么巧,线程 B 在执行过程中,线程 A 执行完了。就要开始释放锁了。
面试准备 -- redis实现分布式锁_第6张图片
等线程 B 执行完,发现自己的锁没了,这不哭晕在厕所吗?

关于上述情况,也是有解决方案的,比如使用 lua 脚本来保证其原子性。恰好 Redisson 框架底层也是使用 lua 脚本来保证原子性,也就不需要我们自己写脚本啦,下面我们就看看 Redisson 框架。

Redisson 简介
redisson 是 redis 官方推荐的 java 语言实现分布式锁的项目。该项目在基于 netty 框架的基础上提供了一系列分布式特性的工具类。下面我们来使用 Redisson 实现分布式锁。

首先我们需要构建配置文件

#这里只配置地址
redis.address= redis://172.16.81.147:6379
#有兴趣的可以去查看 BaseConfig 中具体配置详情

配置类

/**
 * @author Gentle
 * @date 2019/06/01 : 10:38
 */
@Data
@Component
@ConfigurationProperties(prefix = "redis")
public class RedisConfigInfoProperties {
	
    private String address;
}
/**
 * @author Gentle
 * @date 2019/05/30 : 23:37
 */
@Component
@Configurable
public class RedissonConfig {
    @Autowired
    private RedisConfigInfoProperties redisConfigInfoProperties;
    @Bean
    public RedissonClient redissonClient() {

        Config config = new Config();
        config.useSingleServer()
                .setAddress(redisConfigInfoProperties.getAddress())
                .setConnectionMinimumIdleSize(0)
                .setConnectionPoolSize(3);
        return Redisson.create(config);
    }
}

注解

/**
 * @author Gentle
 * @date 2019/05/31 : 23:10
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLockAnnotation {
    /**
     * 时间
     * @return
     */
    long time() default 20;
    /**
     * 时间类型 时 分 秒
     * @return
     */
    TimeUnit timeunit() default TimeUnit.SECONDS;
    /**
     * 加锁的 key
     * @return
     */
    String key() default "test";
}

AOP 拦截器

/**
 * @author Gentle
 * @date 2019/05/31 : 23:25
 */
@Aspect
@Order(5)
@Component
public class RedisLockAop {
    @Autowired
    private RedissonClient redissonClient;
    
    @Around("@annotation(redisLockAnnotation)")
    public void redisLockHandler(ProceedingJoinPoint joinPoint, RedisLockAnnotation redisLockAnnotation){
        //拿到锁对象
        RLock lock = redissonClient.getLock(redisLockAnnotation.key());
        boolean acquire =false;
        try {
            //尝试加锁
            acquire = lock.tryLock(redisLockAnnotation.time(),redisLockAnnotation.timeunit());
            //成功就执行方法
            if (acquire){
                joinPoint.proceed();
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            //拿到锁才释放
            if (acquire){
                lock.unlock();
            }
        }
    }
}

服务接口及实现

/**
 * @author Gentle
 * @date 2019/05/31 : 23:39
 */
public interface TestService {
    /**
     * 计数服务
     */
    void countNumber();
}
/**
 * @author Gentle
 * @date 2019/05/31 : 23:40
 */
@Service
public class TestServiceImpl implements TestService {
    int a =0;
    @Override
    //redis 分布式锁注解
    @RedisLockAnnotation
    public void countNumber() {
        System.out.println(a++);
    }
}

测试类

import com.gentle.service.TestService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisLockApplicationTests {

    @Autowired
    TestService testService;

    @Test
    public void contextLoads() throws InterruptedException {
        //100个线程
        CountDownLatch countDownLatch = new CountDownLatch(100);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                testService.countNumber();
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
    }
}

测试结果
面试准备 -- redis实现分布式锁_第7张图片

代码已推送至 Gitee

Gitee
https://gitee.com/reway_wen/springboot-learn/tree/master/redis-lock-demo

GitHub 待更新

总结
这次,我们主要学习了 Redis 分布式锁的示例,并针对该示例出现的几个问题进行了剖析。接下来我们
学习了使用 Redisson 框架来实现我们的分布式锁。

有兴趣的同学可以关注公众号,一起学习!
面试准备 -- redis实现分布式锁_第8张图片

你可能感兴趣的:(面试准备)