前言
最近由于项目需要,需要增加分布式锁功能,因此,对分布式锁进行了研究。
技术选型
分布式锁有三种实现方式,zookeeper、数据库、redis。由于redis效率高,使用简单,因此,在这里选用了redis实现分布式锁。
配置redis
由于项目是springboot项目,整合了RedisTemplate,直接在利用RedisTemplate实现分布式锁较为简单,无需增加其他配置,配置如下:
spring.redis.host=10.1.1.1
spring.redis.port=6379
spring.redis.password=123456
pom.xml文件如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!--spring boot 配置处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
添加Lock服务
package com.tp.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
@Service
public class RedisLockService {
@Autowired
private RedisTemplate redisTemplate;
private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
//锁超时时间,防止线程在入锁以后,无限的执行等待
private int expireMsecs = 2 * 1000;
//锁等待时间,防止线程饥饿
private int timeoutMsecs = 5 * 1000;
private volatile boolean locked = false;
private String lockKey = "lock-key";
//获取键的值
private String get(final String key) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] data = connection.get(serializer.serialize(key));
connection.close();
if (data == null) {
return null;
}
return serializer.deserialize(data);
}
});
} catch (Exception e) {
System.err.println("get redis error, key : " + key + "," + e.getMessage());
}
return obj != null ? obj.toString() : null;
}
//设置键的值
private boolean setNX(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
//System.out.println(key + ":" + value);
Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
connection.close();
return success;
}
});
} catch (Exception e) {
System.err.println("setNX redis error, key : " + key + "," + e.getMessage());
e.printStackTrace();
}
return obj != null ? (Boolean) obj : false;
}
//获取并设置键的值,并返回旧值
private String getSet(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
connection.close();
return serializer.deserialize(ret);
}
});
} catch (Exception e) {
System.err.println("setNX redis error, key : " + key);
e.printStackTrace();
}
return obj != null ? (String) obj : null;
}
//实现分布式锁
public synchronized boolean lock(String lockValue) {
int timeout = timeoutMsecs;
while (timeout >= 0) {
//long expires = System.currentTimeMillis() + expireMsecs + 1;
//String expiresStr = String.valueOf(expires); //锁到期时间
if (this.setNX(lockKey, lockValue)) {
// 获取锁
locked = true;
return true;
}
//redis系统的时间
String currentValueStr = this.get(lockKey);
//判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
if (currentValueStr != null && Long.parseLong(currentValueStr) <= System.currentTimeMillis()) {
//获取上一个锁到期时间,并设置现在的锁到期时间,
//只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
String oldValueStr = this.getSet(lockKey, lockValue);
//防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
//分布式的情况下:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
// 获取锁
locked = true;
return true;
}
}
timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
//延迟100 毫秒,
try {
Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
}catch (Exception e){
System.err.println(lockKey + e.getMessage());
}
}
return false;
}
//释放锁
public boolean releaseLock(String lockValue) {
// 判断加锁与解锁是不是同一个客户端
boolean flag = false;
try{
String value = get(lockKey);
//System.out.println(lockValue + ":" + value);
if (lockValue.equals(value)){
redisTemplate.delete(lockKey);
flag = true;
}
}catch (Exception e){
System.err.println("删除锁出错" + e.getMessage());
}
return flag;
}
}
使用demo
package com.tp.service;
import org.omg.Messaging.SYNC_WITH_TRANSPORT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class LockDemo {
@Autowired
private RedisLockService redisLockService;
@PostConstruct
public void lockDemo(){
int i = 100;
while (i > 0){
i--;
new Thread(){
@Override
public void run(){
String lockKey = String.valueOf(Thread.currentThread().getId());
String lockValue = String.valueOf(System.currentTimeMillis() + 2000 + 1);
Boolean result = redisLockService.lock(lockValue);
if (result){
System.out.println(lockKey + ":获取锁");
}else{
System.out.println(lockKey + ":未能获取锁");
}
try{
Thread.sleep(10);
}catch (Exception e){
System.err.println(lockKey + e.getMessage());
}
boolean release = redisLockService.releaseLock(lockValue);
if (release){
System.out.println(lockKey + ":释放锁成功");
}else {
System.out.println(lockKey + ":未释放锁");
}
}
}.start();
}
}
}
结果
总结
使用redis实现分布式锁的方式有很多种,有的使用jedis的setex方法,直接设置过期时间,因为这个方法是原子性的,避免线程死锁发生。这里呢,使用的是redis的setnx,该方法当key不存在时就不能set进去,使用了其他逻辑来避免发生死锁。