springboot(十二)-分布式锁(redis)
什么是分布式锁?
要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
分布式锁的使用场景
线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
有这样一个情境,线程A和线程B都共享某个变量X。
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。
分布式锁简介
其实Java世界的”半壁江山”——Spring早就提供了分布式锁的实现。早期,分布式锁的相关代码存在于Spring Cloud的子项目Spring Cloud Cluster中,后来被迁到Spring Integration中。
可能有不少童鞋对Spring Integration不是很熟悉,简单介绍一下——官方说法,这是一个 企业集成模式
的实现;通俗地说,Spring Integration的定位是一个轻量级的ESB,尽管它做了很多ESB不做的事情。顺便说一下,Spring Cloud Stream的底层也是Spring Integration。
Spring Integration提供的全局锁目前为如下存储提供了实现:
- Gemfire
- JDBC
- Redis
- Zookeeper
它们使用相同的API抽象——这正是Spring最擅长的。这意味着,不论使用哪种存储,你的编码体验是一样的,有一天想更换实现,只需要修改依赖和配置就可以了,无需修改代码。
编码
新建一个sprinboot项目,然后配置相关内容和测试代码。
1.pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itmuch.cloud</groupId> <artifactId>redisLock</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>redisLock</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- 这个需要为 true 热部署才有效 --> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
这里提一下,我之前都是用的1.5.9.RELEASE版本的springboot。但我发现添加了上面红色字体的三个依赖之后,发现编译不通过,发聩的信息是版本问题,后来我改为2.0.0.RELEASE版本,OK了。
2.application.yml
server:
port: 8080
spring:
redis:
port: 6379
host: localhost
当前这个应用的端口,我们设置为8080,然后Redis服务器的端口当然是默认的6379啦!
3.RedisLockConfiguration.java
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockRegistry(redisConnectionFactory, "spring-cloud");
}
}
这个很好理解,我们要用到锁,当然得先注册一个锁的Bean对象到spring容器中,以便获取使用。
4.testController.java
@RestController @RequestMapping(value = "index") public class testController { private final static Logger log = LoggerFactory.getLogger(testController.class); @Autowired private RedisLockRegistry redisLockRegistry; @GetMapping("test") public void test() throws InterruptedException { Lock lock = redisLockRegistry.obtain("lock"); boolean b1 = lock.tryLock(3, TimeUnit.SECONDS); log.info("b1 is : {}", b1); TimeUnit.SECONDS.sleep(5); boolean b2 = lock.tryLock(3, TimeUnit.SECONDS); log.info("b2 is : {}", b2); lock.unlock(); lock.unlock(); }
这个接口类中的代码如果不太明白,不,不管你明不明白,都建议看一下org.springframework.integration.redis.util.RedisLockRegistry这个类的注释。
/** * Implementation of {@link LockRegistry} providing a distributed lock using Redis. * Locks are stored under the key {@code registryKey:lockKey}. Locks expire after * (default 60) seconds. Threads unlocking an * expired lock will get an {@link IllegalStateException}. This should be * considered as a critical error because it is possible the protected * resources were compromised. * <p> * Locks are reentrant. * <p> * <b>However, locks are scoped by the registry; a lock from a different registry with the * same key (even if the registry uses the same 'registryKey') are different * locks, and the second cannot be acquired by the same thread while the first is * locked.</b> * <p> * <b>Note: This is not intended for low latency applications.</b> It is intended * for resource locking across multiple JVMs. * <p> * {@link Condition}s are not supported.
测试
这样,我们一个工程就开发完了,你可以再复制一份工程,RedisLock2.只要把端口号改了就行,比如改为8081.然后同时启动俩工程。
我一般都是一个放在eclipse中跑,一个在终端通过命令行启动,这样简洁一点。
接下来,你打开两个网页,输好地址,然后快速依次访问两个工程的接口:
http://localhost:8081/index/test
http://localhost:8080/index/test
然后看两个控制台的结果
先看第一个:两个都是true,说明同一个线程是可以获得到锁的,正如上面注释
Locks are reentrant.
再看第二个:第一次是false,因为上一个线程锁住了,还没有释放,所以它是获取不到的。而第二次返回true,说明获得到了,因为第一个工程中跑的线程已经释放了锁。
另外,你如果要看到现象,你开启RedisClient.你访问其中一个工程的接口,快速刷新redis对应的db(我的是db0)
你不停的刷新db0,你会看到这个spring-cloud的锁key,过几秒就消失了,因为被释放了嘛。这也证实了我们的运行结果。
如果你代码中不释放锁,那么这个spring-cloud的锁key过60秒会自动消失,正如上面注释所描述的那样。
代码下载地址:https://gitee.com/fengyuduke/my_open_resources/blob/master/redisLock.zip