最近开发一了个答题抽奖项目,由于部署项目采用了负载均衡策略,分配奖品时必须使用分布式锁,项目开发完成后记录一下利用redisson实现分布式锁的过程
一、springboot项目整合redisson
- redisson pom依赖如下,springboot为2.x,如果是更底版本,redisson可能也需要换成更低的版本。
<!--Redis分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.0</version>
</dependency>
- yml配置文件中的redis配置如下
spring:
redis:
host: localhost
port: 3306
timeout: 6000
database: 5
password: '123456'
lettuce:
pool:
max-active: -1 #最大连接数
max-idle: 8 #最大空闲数
max-wait: 15000 #最大连接阻塞等待时间
min-idle: 0 #最小空闲数
- 配置RedissonClient
创建RedisProperties类,装配redis配置
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private String host;
private int port;
private String password;
private int database;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getDatabase() {
return database;
}
public void setDatabase(int database) {
this.database = database;
}
}
然后创建RedissionConfig类,配置RedissonClient
import org.apache.log4j.Logger;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissionConfig {
@Autowired
private RedisProperties redisProperties;
private Logger logger = Logger.getLogger(this.getClass());
@Bean
public RedissonClient redissonClient() {
RedissonClient redissonClient;
Config config = new Config();
String url = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
config.useSingleServer().setAddress(url)
.setPassword(redisProperties.getPassword())
.setDatabase(redisProperties.getDatabase());
try {
redissonClient = Redisson.create(config);
return redissonClient;
} catch (Exception e) {
logger.error("RedissonClient init redis url:[{}], Exception:" + url);
return null;
}
}
}
到此Redisson就整合完成了
二、利用Redisson实现分布式锁
创建DistributedRedisLock类,代码如下
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @title 分布式redis锁
* @author gavin
* @date 2020年7月10日
*/
@Component
public class DistributedRedisLock {
@Autowired
private RedissonClient redissonClient;
private Logger logger = Logger.getLogger(this.getClass());
// 加锁
public Boolean lock(String lockName) {
try {
if (redissonClient == null) {
logger.info("redissonClient为空");
return false;
}
RLock lock = redissonClient.getLock(lockName);
// 锁30秒后自动释放,防止死锁
lock.lock(30, TimeUnit.SECONDS);
logger.info("线程" + Thread.currentThread().getName() + "加锁" + lockName + "成功");
// 加锁成功
return true;
} catch (Exception e) {
logger.error("加锁异常:" + lockName);
e.printStackTrace();
return false;
}
}
// 释放锁
public Boolean unlock(String lockName) {
try {
if (redissonClient == null) {
logger.info("redissonClient为空");
return false;
}
RLock lock = redissonClient.getLock(lockName);
if (lock.isLocked()) {
if (lock.isHeldByCurrentThread()) {
// 主动释放锁
lock.unlock();
logger.info("线程" + Thread.currentThread().getName() + "解锁" + lockName + "成功");
return true;
}
}
return true;
} catch (Exception e) {
logger.error(Thread.currentThread().getName() + "解锁异常:" + lockName);
e.printStackTrace();
return false;
}
}
}
DistributedRedisLock 类中有两个方法,lock是加锁方法,unlock是释放锁方法。其中有两个点需要注意
- lock.lock(30, TimeUnit.SECONDS),这行代码的作用是,如果加锁的业务代码运行时间超过了30秒,便会自动释放锁,这是为防止业务代码运行时间过长或者出错导致死锁。
- lock.isHeldByCurrentThread(),在释放锁时,这行判断必须要加上,它会判断当前线程释放的锁是否属于当前线程,防止解锁错乱。
三、测试分布式锁
我采用的测试逻辑是,对redis中一个值为100的变量,使用100线程同时对该变量进行减1操作,看最后变量的值是否为0,如果为0,则分布式锁有效。
创建TestLockController测试类,代码如下
import java.util.concurrent.CountDownLatch;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("lock")
public class TestLockController {
@Autowired
private DistributedRedisLock distributedRedisLock;
// redision分布式锁id
private static String lock = "lock_id";
// 测试数据key
private static String dataKey = "lock_count";
// 测试数据value
private int count = 100;
// 模拟线程数
private int threadNumber = 100;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* @title 在redis中初始化设置lock_count=100,用于测试
* @author gavin
* @date 2020年8月26日
*/
@GetMapping("lockInit")
public Object testInit() {
this.stringRedisTemplate.opsForValue().set(dataKey, String.valueOf(this.count));
return this.stringRedisTemplate.opsForValue().get(dataKey);
}
/**
* @title 测试不加锁
* @author gavin
* @date 2020年8月26日
* @return
* @throws InterruptedException
*/
@GetMapping("/testNoLock")
public Object testNoLock() throws InterruptedException {
// 多线程计数器
CountDownLatch latch = new CountDownLatch(this.threadNumber);
for(int i=0;i<this.threadNumber;i++) {
new Thread(new Runnable() {
@Override
public void run() {
String valueStr = stringRedisTemplate.opsForValue().get(dataKey);
int value = Integer.valueOf(valueStr);
value = value - 1;
stringRedisTemplate.opsForValue().set(dataKey, String.valueOf(value));
// 每个线程执行完毕,计数器减1
latch.countDown();
}
}).start();
}
// 在此阻塞,等待所有线程结束
latch.await();
System.out.println(this.threadNumber + "个线程全部执行完毕");
// 返回最终的dataKey
return stringRedisTemplate.opsForValue().get(dataKey);
}
/**
* @title 测试分布式锁
* @author gavin
* @date 2020年8月26日
* @return
* @throws InterruptedException
*/
@GetMapping("/testUseLock")
public Object testLock() throws InterruptedException {
// 多线程计数器
CountDownLatch latch = new CountDownLatch(this.threadNumber);
for(int i=0;i<this.threadNumber;i++) {
new Thread(new Runnable() {
@Override
public void run() {
boolean lockFlag = distributedRedisLock.lock(lock);
// 加分布式锁
if(lockFlag) {
String valueStr = stringRedisTemplate.opsForValue().get(dataKey);
int value = Integer.valueOf(valueStr);
value = value - 1;
stringRedisTemplate.opsForValue().set(dataKey, String.valueOf(value));
// 加锁
distributedRedisLock.unlock(lock);
// 每个线程执行完毕,计数器减1
latch.countDown();
}
}
}).start();
}
// 在此阻塞,等待所有线程结束
latch.await();
System.out.println(this.threadNumber + "个线程全部执行完毕");
// 返回最终的dataKey
return stringRedisTemplate.opsForValue().get(dataKey);
}
}
测试类中有三个方法,分别是
- lockInit方法,向redis中存储一个key为lock_count,值为100的变量
- testNoLock方法,不加分布式锁进行测试
- testLock方法,使用分布式锁进行测试
启动项目,打开postman,首先执行lockInit,在redis中初始化变量lock_count=100
然后我们先测试一下不加分布式锁的testNoLock方法,运行结果如下图
可以看到,lock_count最终的值为98,说明100个线程运行过程中出现了并发。
再次执行lockInit方法,使lock_count值为100,然后再运行testLock方法,运行结果如下图
可以看到,lock_count的值为0,说明没有出现并发,分布式锁生效了。