分布式锁
分布式系统解决一个方法或属性,同一时间在多机、多线程、高并发场景只能被一个线程执行的方案。
目录
如何实现
- pom.xml 添加Redisson依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.3</version>
</dependency>
- 配置 redisson
LockRedisProperties .java
@Data
@ToString
@ConfigurationProperties(prefix = "lock.redis", ignoreUnknownFields = true)
public class LockRedisProperties {
private int database;
/**
* 等待节点回复命令的时间。该时间从命令发送成功时开始计时
*/
private int timeout;
private String password;
private String mode;
private String nodes;
private String connTimeout;
/**
* 池配置
*/
private LockRedisPoolProperties pool;
/**
* 集群 信息配置
*/
private LockRedisClusterProperties cluster;
}
LockRedisPoolProperties.java
@Data
@ToString
public class LockRedisPoolProperties {
private int maxIdle;
private int minIdle;
private int maxActive;
private int maxWait;
private int connTimeout;
private int soTimeout;
/**
* 池大小
*/
private int size;
}
LockRedisClusterProperties.java
@Data
@ToString
public class LockRedisClusterProperties {
/**
* 集群状态扫描间隔时间,单位是毫秒
*/
private int scanInterval;
/**
* 集群节点
*/
private String nodes;
/**
* 默认值: SLAVE(只在从服务节点里读取)设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。
* MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取
*/
private String readMode;
/**
* (从节点连接池大小) 默认值:64
*/
private int slaveConnectionPoolSize;
/**
* 主节点连接池大小)默认值:64
*/
private int masterConnectionPoolSize;
/**
* (命令失败重试次数) 默认值:3
*/
private int retryAttempts;
/**
*命令重试发送时间间隔,单位:毫秒 默认值:1500
*/
private int retryInterval;
/**
* 执行失败最大次数默认值:3w
*/
private int failedAttempts;
}
- 构建RedissonClient
LockCacheConfiguration .java
@Configuration
@EnableConfigurationProperties(LockRedisProperties.class)
public class LockCacheConfiguration {
private final static Logger LOGGER = LoggerFactory.getLogger(LockCacheConfiguration.class);
@Autowired
private LockRedisProperties redisProperties;
@Configuration
@ConditionalOnClass({Redisson.class})
@ConditionalOnExpression("'${lock.redis.mode}'=='single' or '${lock.redis.mode}'=='cluster' or '${lock.redis.mode}'=='sentinel'")
protected class RedissonSingleClientConfiguration {
/**
* 单机模式 redisson 客户端
*/
@Bean
@ConditionalOnProperty(name = "lock.redis.mode", havingValue = "single")
RedissonClient redissonSingle() {
Config config = new Config();
String node = redisProperties.getNodes();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer();
serverConfig.setAddress(node)
// 命令等待超时,单位:毫秒
.setTimeout(redisProperties.getTimeout())
// 连接空闲超时,单位:毫秒
.setIdleConnectionTimeout(redisProperties.getPool().getSoTimeout())
// 连接超时,单位:毫秒
.setConnectTimeout(redisProperties.getPool().getConnTimeout())
// 连接池大小
.setConnectionPoolSize(redisProperties.getPool().getMaxIdle())
// 最小空闲连接数
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StrUtil.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
/**
* 集群模式的 redisson 客户端
*/
@Bean
@ConditionalOnProperty(name = "lock.redis.mode", havingValue = "cluster")
RedissonClient redissonCluster() {
LOGGER.info("cluster redisProperties:{}", redisProperties.getCluster());
Config config = new Config();
String[] nodes = redisProperties.getNodes().split(",");
List<String> newNodes = new ArrayList<>(nodes.length);
Arrays.stream(nodes).forEach((index) -> newNodes.add(
index.startsWith("redis://") ? index : "redis://" + index));
ClusterServersConfig serverConfig = config.useClusterServers();
serverConfig.addNodeAddress(newNodes.toArray(new String[0]))
.setScanInterval(redisProperties.getCluster().getScanInterval())
// 连接空闲超时,单位:毫秒
.setIdleConnectionTimeout(redisProperties.getPool().getSoTimeout())
// 连接超时,单位:毫秒
.setConnectTimeout(redisProperties.getPool().getConnTimeout())
.setFailedSlaveCheckInterval(redisProperties.getCluster().getFailedAttempts())
// 命令失败重试次数
.setRetryAttempts(redisProperties.getCluster().getRetryAttempts())
// 命令重试发送时间间隔,单位:毫秒
.setRetryInterval(redisProperties.getCluster().getRetryInterval())
// 主节点连接池大小
.setMasterConnectionPoolSize(redisProperties.getCluster().getMasterConnectionPoolSize())
// 从节点连接池大小
.setSlaveConnectionPoolSize(redisProperties.getCluster().getSlaveConnectionPoolSize())
// 命令等待超时,单位:毫秒
.setTimeout(redisProperties.getTimeout());
if (StrUtil.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
}
}
- 添加配置,根据 redis 服务情况合理配置参数
单机:
lock:
redis:
database: 0
mode: single
nodes: 192.168.174.105:32341
password: redis
timeout: 30000
pool:
minIdle: 1
maxIdle: 100
soTimeout: 10000
集群:
lock:
redis:
database: 0
mode: cluster
nodes: 192.168.0.103:6380,192.168.0.103:6381,192.168.0.103:6382,192.168.0.103:6383,192.168.0.103:6384,192.168.0.103:6385
password:
timeout: 30000
pool:
minIdle: 1
maxIdle: 100
soTimeout: 10000
cluster:
scanInterval: 10
masterConnectionPoolSize: 64
slaveConnectionPoolSize: 64
如何使用
LockTest.java
@Slf4j
@Component
@ConditionalOnBean(RedissonClient.class)
public class LockTest implements ApplicationRunner {
@Resource
RedissonClient redissonClient;
public String lock(String lockName) {
lockName = StrUtil.blankToDefault(lockName, "lock");
RLock lock = redissonClient.getLock(lockName);
try {
// true if lock is successfully acquired, otherwise false if lock is already set.
boolean lockStatus = lock.tryLock(10, 100, TimeUnit.SECONDS);
if (lockStatus) {
System.out.println("进程"+System.getProperty("PID")+"加锁成功");
}else{
System.out.println("进程"+System.getProperty("PID")+"获取锁失败");
}
} catch (InterruptedException e) {
log.error("加锁失败",e);
}
finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
System.out.println("解锁完成");
}
}
return System.getProperty("PID");
}
/**
* Callback used to run the bean.
*
* @param args incoming application arguments
* @throws Exception on error
*/
@Override
public void run(ApplicationArguments args) throws Exception {
lock("lock-test");
}
}
注意事项
- 加锁:
- 建议设置合理的重试次数和超时时间,以防止死锁情况的发生;
- 避免使用分布式锁来保证高频率的临界区访问,这样会导致锁的频繁获取和释放,降低系统性能;
- 分布式锁不应该成为系统性能的瓶颈,需要合理评估锁的使用场景和性能影响;
- 加锁使用的 key 需要按业务设置,保证不同业务加锁使用的 key 唯一性。
- 解锁:
- 判断锁是否是当前线程所加,避免其他线程出现误解锁情况,造成业务事故。