一,前言

redis 现在已经成为系统缓存的必备组件,针对缓存读取更新操作,通常我们希望当缓存过期之后能够只有一个请求去更新缓存,其它请求依然使用旧的数据。这就需要用到锁,因为应用服务多数以集群方式部署,因此这里的锁就必需要是分布式锁才能符合需求。

二,spring-boot 引入 redis

在 pom 文件中加入如下依赖,spring-boot 的自动注册功能会帮我们准备好,我们直接使用 StringRedisTemplate 就可以了。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

三,redis 分布式锁实现

/**
* @author koma <komazhang@foxmail.com>
* @date 2018-09-19 11:24
*/
@Slf4j
@Service
public class CacheService {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "EX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; @Autowired
private StringRedisTemplate redisTemplate; /**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public Boolean tryLock(String lockKey, String clientId, long seconds) {
redisTemplate.opsForValue().set();
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
} /**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public Boolean releaseLock(String lockKey, String clientId) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}

 

上述代码实现,仅对 redis 单实例架构有效,当面对 redis 集群时就无效了。但是一般情况下,我们的 redis 架构多数会做成“主备”模式,然后再通过 redis 哨兵实现主从切换,这种模式下我们的应用服务器直接面向主机,也可看成是单实例,因此上述代码实现也有效。但是当在主机宕机,从机被升级为主机的一瞬间的时候,如果恰好在这一刻,由于 redis 主从复制的异步性,导致从机中数据没有即时同步,那么上述代码依然会无效,导致同一资源有可能会产生两把锁,违背了分布式锁的原则。

redis 单实例架构示意图

为什么上面的代码可以实现分布式锁,根本原因在于 redis 对 set 命令中的 NX 选项和对 lua 脚本的执行都是原子的,因此当多个客户端去争抢执行上锁或解锁代码时,最终只会有一个客户端执行成功。同时 set 命令还可以指定key的有效期,这样即使当前客户端奔溃,过一段时间锁也会被 redis 自动释放,这就给了其它客户端获取锁的机会。

上述代码不能使用 spring-boot 提供的 redisTemplate.opsForValue().set() 命令是因为 spring-boot 对 jedis 的封装中没有返回 set 命令的返回值,这就导致上层没有办法判断 set 执行的结果,因此需要通过 execute 方法调用 RedisCallback 去拿到底层的 Jedis 对象,来直接调用 set 命令。这个问题主要是在 spring-data-redis 的封装上,了解即可。

 四,分布式锁的原则

  • 独享: 即互斥属性,在同一时刻,一个资源只能有一把锁被一个客户端持有

  • 无死锁: 当持有锁的客户端奔溃后,锁仍然可以被其它客户端获取

  • 容错性: 当部分节点失活之后,其余节点客户端依然可以获取和释放锁

  • 统一性: 即释放锁的客户端只能由获取锁的客户端释放

五,一类常见错误实现和推荐使用方式

if (redisTemplate.opsForValue().setIfAbsent(lockKey, clientId)) {
//这里存在宕机风险,导致设置有效期失败
redisTemplate.expire(lockKey, seconds, TimeUnit.SECONDS);
}

这是一种典型的错误实现,在早期的 redis 分布式锁实践中我们经常可以看到类似的实现,其中 spring-boot 中的 setIfAbsent 方法在底层调用的是 redis 的 setNx 命令,该命令和 set 命令的 NX 选项具有同样的功能,但是 setNx 命令不能够设置 key 的有效期,这也是为什么我们会在获取到锁之后马上去设置锁的有效期,但是恰好这里却隐藏着风险,因为这一整个操作并非是原子的。

if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}

对于解锁代码,也存在同样的风险,因为在执行 delete 的时候,lockKey 现在可能已经被另外一个客户端持有了,那么这里直接删除就是删除了其它客户端的锁,导致的最终结果就是真正应该持有锁的客户端在没有完全执行完之后,锁又被另外的客户端持有了,这样一个资源就产生了两把锁,同样违背了分布式锁的原则。

推荐的使用方式是,当 redis 的架构如上图所示一样是单实例模式时,如果存在主备且可以忍受小概率的锁出错,那么就可以直接使用上述代码,当然最严谨的方式还是使用官方的 Redlock 算法实现。其中 Java 包推荐使用 redisson

六,参考资料

spring-boot 中实现标准 redis 分布式锁的更多相关文章

  1. redis分布式锁-spring boot aop+自定义注解实现分布式锁

    接这这一篇redis分布式锁-java实现末尾,实现aop+自定义注解 实现分布式锁 1.为什么需要 声明式的分布式锁 编程式分布式锁每次实现都要单独实现,但业务量大功能复杂时,使用编程式分布式锁无疑 ...

  2. spring boot redis分布式锁

    随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring b ...

  3. spring boot 利用redisson实现redis的分布式锁

    原文:http://liaoke0123.iteye.com/blog/2375469 利用redis实现分布式锁,网上搜索的大部分是使用java jedis实现的. redis官方推荐的分布式锁实现 ...

  4. spring boot redis分布式锁 (转)

    一. Redis 分布式锁的实现以及存在的问题 锁是针对某个资源,保证其访问的互斥性,在实际使用当中,这个资源一般是一个字符串.使用 Redis 实现锁,主要是将资源放到 Redis 当中,利用其原子 ...

  5. spring boot(九):Spring Boot中Redis的使用

    Redis实战代码 1.引入 spring-boot-starter-redis <dependency> <groupId>org.springframework.boot& ...

  6. spring boot(三):Spring Boot中Redis的使用

    spring boot对常用的数据库支持外,对nosql 数据库也进行了封装自动化. redis介绍 Redis是目前业界使用最广泛的内存数据存储.相比memcached,Redis支持更丰富的数据结 ...

  7. springboot(三):Spring boot中Redis的使用

    spring boot对常用的数据库支持外,对nosql 数据库也进行了封装自动化. redis介绍 Redis是目前业界使用最广泛的内存数据存储.相比memcached,Redis支持更丰富的数据结 ...

  8. Spring Cloud分布式微服务系统中利用redssion实现分布式锁

    在非分布式系统中要实现锁的机制很简单,利用java.util.concurrent.locks包下的Lock和关键字synchronized都可以实现.但是在分布式系统中,如何实现各个单独的微服务需要 ...

  9. Spring Boot(十一)Redis集成从Docker安装到分布式Session共享

    一.简介 Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API,Redis也是技术领域使用最为广泛的存储中间件,它是 ...

随机推荐

  1. Leetcode简单题

    # Title Solution Acceptance Difficulty Frequency     1 Two Sum       44.5% Easy     2 Add Two Number ...

  2. awk--基本操作

    二:awk--将一行分为数个字段处理 PS:awk [option] '条件类型 {动作1} 条件类型{动作2} ...' filename PS: awk 'Pattern {action}' fi ...

  3. JavaSE--jdbc编程

    JDBC全称为:Java Data Base Connectivity (java数据库连接),可以为多种数据库提供统一的访问.JDBC是sun开发的一套数据库访问编程接口,是一种SQL级的API.它 ...

  4. 这38个小技巧告诉你如何快速学习MySQL数据库

    1.如何快速掌握MySQL? ⑴培养兴趣兴趣是最好的老师,不论学习什么知识,兴趣都可以极大地提高学习效率.当然学习MySQL 5.6也不例外.⑵夯实基础计算机领域的技术非常强调基础,刚开始学习可能还认 ...

  5. LintCode 6---合并排序数组 II

    import java.util.Arrays; public class Lint6 { /* * 合并两个排序的整数数组A和B变成一个新的数组.新数组也要有序. */ public static ...

  6. mysql设计与优化以及数据库表设计与表开发规范

    一.设计问题? 1.主键是用自增还是UUID ? Innodb 中的主键是聚簇索引. 如果主键是自增的,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的 ...

  7. Linux查看修改文件句柄数

    SuSE 11SP3 默认句柄数是1024 1.查看linux的文件句柄数 ulimit -a 2.修改文件句柄数 ①ulimit -n 65535②修改linux系统参数.vi /etc/secur ...

  8. 使用TensorFlow玩GTA5

    小白学TensorFlow(一) tensorflow安装 在安装之前,您必须选择以下类型的TensorFlow之一来安装: TensorFlow仅支持CPU支​​持.如果您的系统没有NVIDIA®G ...

  9. optparse:让你轻松地与命令行打交道

    介绍 一个专门用于命令行参数解析的模块 使用 import optparse op = optparse.OptionParser() # 添加选项 op.add_option("--s&q ...

  10. MST-prim ElogV

    #include<bits/stdc++.h> #define ll long long using namespace std; ; ; struct node { int t;int ...