1.为什么要使用分布式锁
传统项目中,大部分项目都是部署一个实例就行了,这个时候使用JDK自带的锁基本就可以解决并发问题。但是,一旦项目部署了多个实例,那么使用Java自带的锁,就没有用了;举个栗子,项目A部署了三个实例,有一段代码是通过定时任务定时触发的,比如发送报表,时间是每天早上9点,这个时候,你会发现每天早上9点,同一个人会收到3份一样的报表。但是这并不是我们想要的,正常业务里面是一个人收到一份报告就足够了。那么,我怎么确定让那个实例来定时发送报告呢?换句话说,我怎么在9点的时候,在3个实例中选一个实例来发送报告呢?是不是觉得,原生的Java的锁就做不到了;但是,使用分布式锁就可以解决这个问题,只需要在9点的时候,让三个实例去竞争获取锁,只有持有锁的实例,才可以发送报表。
2.如何实现分布式锁
目前主流分布式锁的实现有三种:
a. 基于redis实现(本文介绍的就是这个)
b.基于zookeeper实现(本文暂不介绍)
c.基于数据库实现(平安面试时,架构师说的,但是实际开发我没有使用过)
3.如何使用Redis实现分布式锁(基于Java实现)
3.1 pom文件添加redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 1.5的版本默认采用的连接池技术是jedis,2.0以上版本默认连接池是lettuce, 因为此次是采用jedis,所以需要排除lettuce的jar-->
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
注意:jedis版本不要随便更新,更换了可能导致方法不支持
3.2 初始化RedisTemplate配置
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer(getClass().getClassLoader());
template.setValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
3.3 redis加锁释放锁工具类
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 锁的后缀
*/
private static final String LOCK_SUFFIX = "_redis_lock";
/**
* 锁超时时间,防止线程在入锁以后,防止阻塞后面的线程无法获取锁
*/
private int expireMsecs = 60 * 1000;
private final RedisTemplate redisTemplate;
/**
* 加锁键
*/
private final String lockKey;
/**
* 加锁客户端唯一标识(采用UUID)
*/
private final String clientId;
public RedisLock(RedisTemplate redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey + LOCK_SUFFIX;
this.clientId = UUID.randomUUID().toString();
}
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
* <p>
* 支持重复,线程安全
*/
public boolean tryLock() {
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireMsecs);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*/
public boolean releaseLock() {
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
4. 工具类使用示例
@EnableScheduling
@Component
public class SmsReportTask {
private Logger logger = LoggerFactory.getLogger(SmsReportTask.class);
@Value("${report.yunpian.enabled}")
private boolean enabled;
private final String ALI_LOCK_KEY = "notify_sync_ali";
private final String YUNPIAN_LOCK_KEY = "notify_sync_yunpian";
private final SmsTemplateLogMapper smsTemplateLogMapper;
private final AccountService accountService;
private final IAcsClient iAcsClient;
private final RedisTemplate redisTemplate;
@Autowired
public SmsReportTask(SmsTemplateLogMapper smsTemplateLogMapper, AccountService accountService,
IAcsClient iAcsClient, RedisTemplate redisTemplate) {
this.smsTemplateLogMapper = smsTemplateLogMapper;
this.accountService = accountService;
this.iAcsClient = iAcsClient;
this.redisTemplate = redisTemplate;
}
@Scheduled(cron = "${report.trigger.aliyun}")
private void refreshAliReport() {
RedisLock lock = new RedisLock(redisTemplate, ALI_LOCK_KEY);
if (lock.tryLock()) {
try {
updateAliReport();
} finally {
lock.releaseLock();
}
}
}
}