目前,大多数服务都是用了多实例,集群部署。传统的synchronized,ReentrantLock等锁,只能在单实例内部生效。集群服务的线程安全,需要通过分布式锁实现,当然如果是数据库访问的话,也可以通过数据库锁实现分布式锁(TODO: mysql集群行锁能否在分布式服务中生效?单实例写,肯定是没问题的;多实例写需要再探究下)。本篇文章主要介绍在springboot项目中使用redis实现分布式锁。
1. StringRedisTemplate实现分布式锁
1.1 pom.xml文件引入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.2 在application.yml中配置redis
spring:
redis:
host: 192.168.1.6
port: 6379
1.3 新建redis锁操作类
/**
* redis锁操作类
*/
@Repository
public class RedisLock {
@Autowired
private StringRedisTemplate stringredisTemplate;
public RedisLock(StringRedisTemplate stringredisTemplate) {
this.stringredisTemplate = stringredisTemplate;
}
/**
* 加锁,无阻塞
* 1. 加锁过程必须设置过期时间。如果没有设置过期时间,手动释放锁的操作出现问题,那么就发生死锁,锁永远不能被释放.
* 2. 加锁和设置过期时间过程必须是原子操作。如果加锁后服务宕机或程序崩溃,来不及设置过期时间,同样会发生死锁.
*
*/
public String tryLock(String key, long expire) {
String token = UUID.randomUUID().toString();
//setIfAbsent方法:当key不存在的时候,设置成功并返回true,当key存在的时候,设置失败并返回false
//token是对应的value,expire是缓存过期时间
Boolean isSuccess = stringredisTemplate.opsForValue().setIfAbsent(key, token, expire, TimeUnit.MILLISECONDS);
if (isSuccess) {
return token;
}
return null;
}
/**
* 加锁,有阻塞
*/
public String lock(String name, long expire, long timeout) {
long startTime = System.currentTimeMillis();
String token;
do {
token = tryLock(name, expire);
if (token == null) {
if ((System.currentTimeMillis() - startTime) > timeout) {
break;
}
try {
//try 50 per sec
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
} while (token == null);
return token;
}
/**
* 解锁操作
* 1. 解锁必须是解除自己加上的锁.
* 试想一个这样的场景,服务A加锁,但执行效率非常慢,导致锁失效后还未执行完,但这时候服务B已经拿到锁了,这时候服务A执行完毕了去解锁,
* 把服务B的锁给解掉了,其他服务C、D、E...都可以拿到锁了,这就有问题了.
* 加锁的时候我们可以设置唯一value,解锁时判断是不是自己先前的value就行了.
*
*/
public boolean unlock(String key, String token) {
//解锁时需要先取出key对应的value进行判断是否相等,这也是为什么加锁的时候需要放不重复的值作为value
String value = stringredisTemplate.opsForValue().get(key);
if (StringUtils.equals(value, token)) {
stringredisTemplate.delete(key);
return true;
}
return false;
}
}
1.4 业务操作类,用上RedisLock
@Service
public class ServiceImpl implements Service {
@Autowired
private Mapper mapper;
@Autowired
private RedisLock redisLock;
@Override
public Map<String, Object> test() {
Map<String, Object> resultMap = new HashMap<>();
//加锁操作,在需要的地方加锁,控制锁粒度尽量小
String token = redisLock.lock("key", 3000, 3500);
try {
//获取到了锁,执行正常业务
if (token != null) {
//执行业务
} else {
resultMap.put("code", "401");
resultMap.put("msg", "其他窗口正在操作,请稍后再试");
return resultMap;
}
} finally {
//解锁
if (token != null) {
boolean isSuccess = redisLock.unlock("key", token);
}
}
resultMap.put("code", "200");
resultMap.put("msg", "success");
return resultMap;
}
}
2. Redisson实现分布式锁
2.1 pom依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.5</version>
</dependency>
2.2 RedissonClient配置类
@Configuration
public class RedisRedLock{
@Value("${spring.redis.sentinel.nodesRedis}")
private String[] nodes;
@Value("${spring.redis.sentinel.master}")
private String master;
/**
* 单个服务
* @return
*/
@Bean
public RedissonClient redissonClientSign(){
Config config = new Config();
config.useSingleServer()
.setIdleConnectionTimeout(10000)//如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
.setConnectTimeout(30000)//同任何节点建立连接时的等待超时。时间单位是毫秒。
.setTimeout(3000)//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
.setPingTimeout(30000)
.setReconnectionTimeout(3000)//当与某个节点的连接断开时,等待与其重新建立
.setPassword("root123456abc").setDatabase(0);
return Redisson.create(config);
}
/**
* 哨兵模式
* @return
*/
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSentinelServers().setMasterName(master)
.setFailedSlaveReconnectionInterval(5000)
.addSentinelAddress(nodes)
.setMasterConnectionPoolSize(500)//设置对于master节点的连接池中连接数最大为500
.setSlaveConnectionPoolSize(500)//设置对于slave节点的连接池中连接数最大为500
.setIdleConnectionTimeout(10000)//如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
.setConnectTimeout(30000)//同任何节点建立连接时的等待超时。时间单位是毫秒。
.setTimeout(3000)//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
.setPingTimeout(30000)
.setReconnectionTimeout(3000)//当与某个节点的连接断开时,等待与其重新建立
.setPassword("root123456abc").setDatabase(0);
return Redisson.create(config);
}
}
2.3 锁工具接口以及实现类
public interface RedLockUtils {
void lock(String lockKey);
void lock(String lockKey, long expireTime);
boolean tryLockTimeout(String lockKey, long waitTime, long expireTime) throws InterruptedException;
void unLock(String lockKey);
}
@Component
public class RedLockUtilsImpl implements RedLockUtils{
@Autowired
private RedissonClient redissonClient;
/**
* 可重入!线程不主动解锁将会永远存在! 慎用
*/
public void lock(String lockKey){
RLock lock1 = redissonClient.getLock(lockKey);
redissonClient.getRedLock(lock1).lock();
}
public void lock(String lockKey, long expireTime) {
RLock lock1 = redissonClient.getLock(lockKey);
redissonClient.getRedLock(lock1).lock(expireTime, TimeUnit.MILLISECONDS);
}
public boolean tryLockTimeout(String lockKey,long waitTime, long expireTime) throws InterruptedException {
RLock lock1 = redissonClient.getLock(lockKey);
return redissonClient.getRedLock(lock1).tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
}
public void unLock(String lockKey) {
RLock lock1 = redissonClient.getLock(lockKey);
redissonClient.getRedLock(lock1).unlock();
}
}
2.4 业务类使用示例
@Service
public class ServiceImpl implements Service {
@Autowired
private RedLockUtils redLockUtils;
public void test() {
String lockKey = "key";
boolean isLock = false;
try {
//获得锁 注意锁的力度,只需要锁定需要防止并发的业务,锁的力度越低性能越好!
isLock = redLockUtils.tryLockTimeout(lockKey, 5000, 10000);
//超时未获得锁
if(!isLock ){
logger.warn("操作太频繁,请稍后重试");
return;
}
//执行业务(需要锁定的部分)
} catch (Exception e) {
logger.error("error:", e);
}finally {
//解锁
if(isLock) {
redLockUtils.unLock(lockKey);
}
}
}
}