7.2.分布式锁

0.原则

分布式锁 要满足以下原则

1、互斥

在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。

2、防止死锁

在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。

所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

3、性能

对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。

所以在锁的设计时,需要考虑两点。

1、锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。

2、锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。

4、重入

我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。

分布式锁redis的分布式锁 重试 redis分布式锁注意_分布式锁redis的分布式锁 重试

1.redis缓存

启动 应用 的一个实例(使用 StringRedisTemplate, JSONObject), 通过 jmeter分别发两 个请求

请求两次 , 分别 观察 控制台 及 最小/最大 响应时间

@Autowired
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/query")
    public List<MiddleData> query(){
        List<MiddleData> list = new ArrayList<>();
        
        String middleDataList = redisTemplate.opsForValue().get("middleDataList");
        if (StringUtils.isEmpty(middleDataList)) {
            list = middleDataService.list();
            redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
            System.out.println(" 从数据库中取出 存储到redis里 ");
        }else{
            list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
            System.out.println(" 从redis里取出 ");
        }

        return list;
    }

2.并发请求

清空 redis , 将 jmeter 调整成 50 并发

观察 控制台 发现 有多次 “从数据库中取出

解决办法 加 本地锁 synchronized, 这样只有一个请求是从数据库中查询 其它的请求都 是从redis里取

@Autowired
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/query")
    public List<MiddleData> query(){
        List<MiddleData> list = new ArrayList<>();

        synchronized (this) {
        	String middleDataList = redisTemplate.opsForValue().get("middleDataList");
        	if (StringUtils.isEmpty(middleDataList)) {
            	list = middleDataService.list();
            	redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
            	System.out.println(" 从数据库中取出 存储到redis里 ");
        	}else{
            	list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
                System.out.println(" 从redis里取出 ");
        	}
        }

        return list;
    }

3.微服务多实例

为了 模拟 集群 形式

同时启动多个应用实例及通过网关并发请求

清空 redis , 将 jmeter 调整成 50 并发, 并通过 网关进行请求

出现异常

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 second(s)] with root cause

io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 second(s)

更换 redis的引擎, 清空 redis 重新设置

<!--redis 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

                <dependency>
                    <groupId>redis.clients</groupId>
                    <artifactId>jedis</artifactId>
                </dependency>

不再报错 但每一个 应用实例 都 要进行数据库 查询

4.分布式锁

http://redis.cn/commands/set.html

@RequestMapping("/query")
    public List<MiddleData> query(){
        List<MiddleData> list = new ArrayList<>();
		// 加锁 判断
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
        if (lock) {
            //synchronized (this) {
                String middleDataList = redisTemplate.opsForValue().get("middleDataList");
                if (StringUtils.isEmpty(middleDataList)) {
                    list = middleDataService.list();
                    redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
                    System.out.println(" 从数据库中取出 存储到redis里 ");
                }else{
                    list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
                    System.out.println(" 从redis里取出 ");
                }
            //}
            // 解锁
            redisTemplate.delete("lock");
        }else{
            // 自旋 调用
            query();
        }
        return list;
    }

5.追加失效时间解决死锁问题

设置 key-value 与 加锁 要原子执行

@RequestMapping("/query")
    public List<MiddleData> query(){
        List<MiddleData> list = new ArrayList<>();
  		// 追加 失效时间为 30 秒
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 30, TimeUnit.SECONDS);
        if (lock) {
            //synchronized (this) {
                String middleDataList = redisTemplate.opsForValue().get("middleDataList");
                if (StringUtils.isEmpty(middleDataList)) {
                    list = middleDataService.list();
                    redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
                    System.out.println(" 从数据库中取出 存储到redis里 ");
                }else{
                    list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
                    System.out.println(" 从redis里取出 ");
                }
            //}
            redisTemplate.delete("lock");
        }else{
            query();
        }
        return list;
    }

6.业务超时 超过 锁定时间

@RequestMapping("/query")
    public List<MiddleData> query(){
        List<MiddleData> list = new ArrayList<>();
        // 设置一个随机的value值
        String uuid = UUID.randomUUID().toString();
                                                                         // 存储到redis里
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,30, TimeUnit.SECONDS);

        if (lock) {
            //synchronized (this) {
                String middleDataList = redisTemplate.opsForValue().get("middleDataList");
                if (StringUtils.isEmpty(middleDataList)) {
                    list = middleDataService.list();
                    redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
                    System.out.println(" 从数据库中取出 存储到redis里 ");
                }else{
                    list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
                    System.out.println(" 从redis里取出 ");
                }
            //}
            // 取出 lock 对应的value
            String lockValue = redisTemplate.opsForValue().get("lock");
            // 与 uuid 比较  相同 再删除
            if (uuid.equals(lockValue)) {
                redisTemplate.delete("lock");
            }
        }else{
            query();
        }
        return list;
    }

7.取value时 对比 也要是原子性

使用 lua脚本

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
/* 调用 脚本 */
/* 删除操作 返回 int 类型 1 成功  0 不成功*/
/* 这个是赋值给  KEYS[1] 的 */
/* 这个是赋值给 ARGV[1] 的 */
Object lockState = redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid);

System.out.println("lockState = " + lockState);

8.异常处理

@Autowired
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/query")
    public List<MiddleData> query(){
        List<MiddleData> list = new ArrayList<>();
        // 设置一个随机的value值
        String uuid = UUID.randomUUID().toString();
                                                                         // 存储到redis里
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300, TimeUnit.SECONDS);

        if (lock) {
            //synchronized (this) {
            try {
                String middleDataList = redisTemplate.opsForValue().get("middleDataList");
                if (StringUtils.isEmpty(middleDataList)) {
                    list = middleDataService.list();
                    redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
                    System.out.println(" 从数据库中取出 存储到redis里 ");
                }else{
                    list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
                    System.out.println(" 从redis里取出 ");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                 取出 lock 对应的value
                //String lockValue = redisTemplate.opsForValue().get("lock");
                 与 uuid 比较  相同 再删除
                //if (uuid.equals(lockValue)) {
                //    redisTemplate.delete("lock");
                //}

                String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                /* 删除操作 返回 int 类型 1 成功  0 不成功*/
                /* 这个是赋值给  KEYS[1] 的 */
                /* 这个是赋值给 ARGV[1] 的 */
                Long lockState = redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid);

            }
            //}

          
        }else{
            query();
        }
        return list;
    }