本人讲述的是基于 Spring Cloud 的分布式架构,那么也带来了线程安全问题,比如一个商城系统,下单过程可能由不同的微服务协作完成,在高并发的情况下如果不加锁就会有问题,而传统的加锁方式只针对单一架构,对于分布式架构是不适合的,这时就需要用到分布式锁。
实现分布式锁的方式有很多,结合我的实际项目和目前的技术趋势,通过实例实现几种较为流行的分布式锁方案,最后会对不同的方案进行比较。
基于 Redis 的分布式锁
利用 SETNX 和 SETEX
基本命令主要有:
- SETNX(SET If Not Exists):当且仅当 Key 不存在时,则可以设置,否则不做任何动作。
- SETEX:可以设置超时时间
其原理为:通过 SETNX 设置 Key-Value 来获得锁,随即进入死循环,每次循环判断,如果存在 Key 则继续循环,如果不存在 Key,则跳出循环,当前任务执行完成后,删除 Key 以释放锁。
这种方式可能会导致死锁,为了避免这种情况,需要设置超时时间。
下面,请看具体的实现步骤。
1.创建一个 Maven 工程并在 pom.xml 加入以下依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 开启web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2.创建启动类 Application.java:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
3.添加配置文件 application.yml:
server:
port: 8080
spring:
redis:
host: localhost
port: 6379
4.创建全局锁类 Lock.java:
/**
* 全局锁,包括锁的名称
*/
public class Lock {
private String name;
private String value;
public Lock(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
5.创建分布式锁类 DistributedLockHandler.java:
@Component
public class DistributedLockHandler {
private static final Logger logger = LoggerFactory.getLogger(DistributedLockHandler.class);
private final static long LOCK_EXPIRE = 30 * 1000L;//单个业务持有锁的时间30s,防止死锁
private final static long LOCK_TRY_INTERVAL = 30L;//默认30ms尝试一次
private final static long LOCK_TRY_TIMEOUT = 20 * 1000L;//默认尝试20s
@Autowired
private StringRedisTemplate template;
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock) {
return getLock(lock, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, LOCK_EXPIRE);
}
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取超时时间 单位ms
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock, long timeout) {
return getLock(lock, timeout, LOCK_TRY_INTERVAL, LOCK_EXPIRE);
}
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取锁的超时时间
* @param tryInterval 多少毫秒尝试获取一次
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock, long timeout, long tryInterval) {
return getLock(lock, timeout, tryInterval, LOCK_EXPIRE);
}
/**
* 尝试获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取锁的超时时间
* @param tryInterval 多少毫秒尝试获取一次
* @param lockExpireTime 锁的过期
* @return true 获取成功,false获取失败
*/
public boolean tryLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {
return getLock(lock, timeout, tryInterval, lockExpireTime);
}
/**
* 操作redis获取全局锁
*
* @param lock 锁的名称
* @param timeout 获取的超时时间
* @param tryInterval 多少ms尝试一次
* @param lockExpireTime 获取成功后锁的过期时间
* @return true 获取成功,false获取失败
*/
public boolean getLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {
try {
if (StringUtils.isEmpty(lock.getName()) || StringUtils.isEmpty(lock.getValue())) {
return false;
}
long startTime = System.currentTimeMillis();
do{
if (!template.hasKey(lock.getName())) {
ValueOperations<String, String> ops = template.opsForValue();
ops.set(lock.getName(), lock.getValue(), lockExpireTime, TimeUnit.MILLISECONDS);
return true;
} else {//存在锁
logger.debug("lock is exist!!!");
}
if (System.currentTimeMillis() - startTime > timeout) {//尝试超过了设定值之后直接跳出循环
return false;
}
Thread.sleep(tryInterval);
}
while (template.hasKey(lock.getName())) ;
} catch (InterruptedException e) {
logger.error(e.getMessage());
return false;
}
return false;
}
/**
* 释放锁
*/
public void releaseLock(Lock lock) {
if (!StringUtils.isEmpty(lock.getName())) {
template.delete(lock.getName());
}
}
}
6.最后创建 HelloController 来测试分布式锁。
@RestController
public class HelloController {
@Autowired
private DistributedLockHandler distributedLockHandler;
@RequestMapping("index")
public String index(){
Lock lock=new Lock("lynn","min");
if(distributedLockHandler.tryLock(lock)){
try {
//为了演示锁的效果,这里睡眠5000毫秒
System.out.println("执行方法");
Thread.sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
distributedLockHandler.releaseLock(lock);
}
return "hello world!";
}
}
7.测试。
启动 Application.java,连续访问两次浏览器:http://localhost:8080/index,控制台可以发现先打印了一次“执行方法”,说明后面一个线程被锁住了,5秒后又再次打印了“执行方法”,说明锁被成功释放。
通过这种方式创建的分布式锁存在以下问题:
- 高并发的情况下,如果两个线程同时进入循环,可能导致加锁失败。
- SETNX 是一个耗时操作,因为它需要判断 Key 是否存在,因为会存在性能问题。
因此,Redis 官方推荐 Redlock 来实现分布式锁。
利用 Redlock
通过 Redlock 实现分布式锁比其他算法更加可靠,继续改造上一例的代码。
1.pom.xml 增加以下依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.0</version>
</dependency>
2.增加以下几个类:
/**
* 获取锁后需要处理的逻辑
*/
public interface AquiredLockWorker<T> {
T invokeAfterLockAquire() throws Exception;
}
/**
* 获取锁管理类
*/
public interface DistributedLocker {
/**
* 获取锁
* @param resourceName 锁的名称
* @param worker 获取锁后的处理类
* @param <T>
* @return 处理完具体的业务逻辑要返回的数据
* @throws UnableToAquireLockException
* @throws Exception
*/
<T> T lock(String resourceName, AquiredLockWorker<T> worker) throws UnableToAquireLockException, Exception;
<T> T lock(String resourceName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception;
}
/**
* 异常类
*/
public class UnableToAquireLockException extends RuntimeException {
public UnableToAquireLockException() {
}
public UnableToAquireLockException(String message) {
super(message);
}
public UnableToAquireLockException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 获取RedissonClient连接类
*/
@Component
public class RedissonConnector {
R