基于redis实现分布式锁
程序员文章站
2022-03-05 09:57:20
...
因为项目中需要用到分布式锁,所以研究了下实现方式。碰到很多坑以及误导人的博客。这里写下自己的感受,大家看的时候还是要抱着怀疑的态度来看实现的合理性。
一般分布式锁的实现方式就三种:
- 数据库乐观锁(我上面博客说到过https://blog.csdn.net/lp2388163/article/details/80683383);
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁。
我之所以选择redis是因为redis快,单线程的原因。像使用乐观锁就会导致数据库压力太大,在高并发的情况下不可取。不使用zookeeper是因为我使用的是eureka。
分布式锁必须满足的条件:
- 互斥性:因为redis是单线程的,所以这点很容易做到
- 不会发生死锁:网上很多资料使用setnx和expire做锁其实保证不了原子性,一旦在这两步中间业务代码报错无法执行,就出现了死锁。
- 容错性:部署redis集群
- 锁拥有者唯一标识:我这里没实现这点,因为业务代码中只有在锁被获取到了才能解这把锁,也就默认保证了这点
废话不多说,贴代码:
package com.mozi.common.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* @author lp
* @create: 2018/10/12 14:22
* @description: redis分布式锁, 使用时具体根据业务来设置超时时间和锁的持有时间!!!
* 注意: 锁的key尽可能使用业务变量, 保证锁的细粒度, 避免串行化
*/
@Slf4j
@Component
public class DistributedLockHandler {
private static final long LOCK_TRY_INTERVAL = 50L;// 默认多久尝试获取一次锁, 需考虑redis服务器压力
private static final long LOCK_TRY_TIMEOUT = 200L;// 默认尝试多久, 需考虑并发压力
private static final long DEFAULT_EXPIRE_TIME = 3000L; // 默认key过期时间, 需考虑业务执行时长
private static final String LOCK_SUCCESS = "OK"; // set方法执行成功后的返回值
private static final String SET_IF_NOT_EXIST = "NX"; // SET IF NOT EXIST,key存在,进行set操作。若key已经存在,则不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX"; // 当设置为PX,表示设置一个过期时间
private static final String DEFAULT_VALUE = "v"; // set方法的value字段, 这里默认设置v
@Autowired
private JedisPool jedisPool;
public Jedis getJedis() {
return jedisPool.getResource();
}
/**
* 尝试获取全局锁
*
* @param key 锁名
* @return true 获取成功,false获取失败
*/
public boolean tryLock(String key) {
return getLock(key, DEFAULT_VALUE, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, DEFAULT_EXPIRE_TIME);
}
/**
* 尝试获取全局锁
*
* @param key 锁名
* @param timeout 获取超时时间 单位ms
* @return true 获取成功,false获取失败
*/
public boolean tryLock(String key, long timeout) {
return getLock(key, DEFAULT_VALUE, timeout, LOCK_TRY_INTERVAL, DEFAULT_EXPIRE_TIME);
}
/**
* 尝试获取全局锁
*
* @param key 锁名
* @param timeout 获取锁的超时时间
* @param tryInterval 多少毫秒尝试获取一次
* @return true 获取成功,false获取失败
*/
public boolean tryLock(String key, long timeout, long tryInterval) {
return getLock(key, DEFAULT_VALUE, timeout, tryInterval, DEFAULT_EXPIRE_TIME);
}
/**
* 尝试获取全局锁
*
* @param key 锁名
* @param timeout 获取锁的超时时间
* @param tryInterval 多少毫秒尝试获取一次
* @param lockExpireTime 锁的过期
* @return true 获取成功,false获取失败
*/
public boolean tryLock(String key, long timeout, long tryInterval, long lockExpireTime) {
return getLock(key, DEFAULT_VALUE, timeout, tryInterval, lockExpireTime);
}
/**
* 尝试获取全局锁, 只尝试一次
*
* @param key 锁名
* @return true 获取成功,false获取失败
*/
public boolean onceTryLock(String key) {
return getLock(key, DEFAULT_VALUE, DEFAULT_EXPIRE_TIME);
}
/**
* 尝试获取全局锁, 只尝试一次
*
* @param key 锁名
* @param lockExpireTime 锁的过期
* @return true 获取成功,false获取失败
*/
public boolean onceTryLock(String key, long lockExpireTime) {
return getLock(key, DEFAULT_VALUE, lockExpireTime);
}
/**
* 获取全局锁
*
* @param key 锁名
* @param value 锁value, 如果要保证加锁和解锁是同一个客户端的话, 这个参数用来指定特定客户端
* @param expireTime 锁的超时时间
* @param timeout 获取锁的超时时间
* @param tryInterval 多少ms尝试一次
* @return
*/
public boolean getLock(String key, String value, long timeout, long tryInterval, long expireTime) {
try (Jedis jedis = getJedis()) {
// 锁如果为空, 获取锁失败
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
return false;
}
long startTime = System.currentTimeMillis(); // 开始时间戳
do {
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) { // 返回成功,表示加锁成功
return true;
}
if (System.currentTimeMillis() - startTime > timeout) { // 尝试超过了设定超时时间后直接跳出循环,获取锁失败
log.info("获取锁超时: {}", System.currentTimeMillis() - startTime);
return false;
}
Thread.sleep(tryInterval); // 循环时设置时间差
}
while (true); // 只要锁存在,循环
} catch (InterruptedException e) {
log.error(e.getMessage());
return false;
}
}
/**
* 获取全局锁(无超时后循环重试机制,拿不到直接返回false)
*
* @param key 锁名
* @param value 锁value, 如果要保证加锁和解锁是同一个客户端的话, 这个参数用来指定特定客户端
* @param expireTime 超时时间
* @return
*/
public boolean getLock(String key, String value, long expireTime) {
try (Jedis jedis = getJedis()) {
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
return false;
}
// 参数: key, value, key不存在set操作存在就不做任何操作, 可设置超时时间, 具体超时时间
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) { // 返回成功,表示加锁成功
return true;
}
} catch (Exception e) {
log.error(e.getMessage());
return false;
}
return false;
}
/**
* 释放锁
*
* @param key 锁名
*/
public void releaseLock(String key) {
try (Jedis jedis = getJedis()) {
if (!StringUtils.isEmpty(key)) {
Long del = jedis.del(key);
log.info("锁名:{},是否释放成功:{}", key, del);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
锁的实现主要还是靠jedis的set(x,x,x,x,x)方法,稍微解释下这个方法的参数和返回值:
- 第一个参数key,key用来当锁,业务上注意key的粒度,粒度最好保证到对应数据库中一行数据
- 第二个参数value,在我这篇博客中给的默认值v,这个字段可以用来做加锁和解锁人的唯一标识,比如传入uuid,那么解锁时也必须是这个uuid才能解锁成功。这点其实可以在业务代码中规避,只要在加锁成功或才能解锁就规避了这点
- 第三个参数nxxx,这个参数我默认给nx,意思就是当key不存在时,我们进行set操作;若key已经存在,则不做任何操作
- 第四个参数expx,我默认给的PX,意思表示可以给这个key设置一个过期时间,这样就规避了死锁的发生
- 第五个参数time,与第四个参数对应,表示过期时间时长
- 返回值:加锁成功返回OK,否则null
这篇文章很多地方参考了https://blog.csdn.net/forezp/article/details/68957681
但他这篇博客实现的锁有几个bug !!! 当初改的我都要吐血了, 不信的可以去试探一波