随着公司业务的扩展,单实例逐渐都演变成的多实例,在多实例的情况下,怎么处理并发呢?
单实例加锁:synchronized关键字、Semaphore、ReentrantLock,或者我们也可以基于AQS定制化锁
多实例加锁:?
什么是分布式锁
单机部署的情况下,锁是在多线程之间共享的,但是分布式部署的情况下,是多台机器,应用级锁。也就是说锁是多进程之间共享的。那么分布式锁要保证锁资源的唯一性,可以在多进程之间共享。
分布式锁特性(互斥、可重入)
实现分布式锁的三种方式
数据库
乐观锁:在表结构中,设置一列版本号,每次更新数据,都会校验版本号,不一致则更新失败
场景:写比较少的场景,多读
悲观锁 :select ....... for update , 每次拿数据的时候都会加锁,其他人只能读,不能写
场景:多写场景
zookeeper
实现分布式锁的原理就是多个节点同时在一个指定的节点下面创建临时会话顺序节点,谁创建的节点序号最小,谁就获得了锁,并且其他节点就会监听序号比自己小的节点,一旦序号比自己小的节点被删除了,其他节点就会得到相应的事件,然后查看自己是否为序号最小的节点,如果是,则获取锁。
缺点:
zookeeper分布式锁保证了锁的容错性、一致性。但是会产生空闲节点(/lock-path)。高并发的业务场景下使用zookeeper分布式锁时,会留下很多的空节点。
redis
获取锁时,使用redis的命令setnx、pexpire(提供基于毫秒的过期时间,expire提供基于秒的过期时间)+ lua脚本(保证脚本中的命令被一起执行,不间断)来实现分布式锁。删除锁时,先执行get,如果获取的值是自己设置的,则执行del操作,同时,这两个操作也放在lua脚本中执行,来保证原子性。
RedisLockRegistry
是spring-integration-redis中提供redis分布式锁实现类。主要是通过redis锁+本地锁双重锁的方式实现的一个比较好的锁。
获取锁 的过程也很简单,首先通过本地锁(localLock,对应的是ReentrantLock实例)获取锁,然后通过RedisTemplate获取redis锁。
@Override
public void lock() {
// 获取本地锁
Lock localLock = RedisLockRegistry.this.localRegistry.obtain(this.lockKey);
localLock.lock();
while (true) {
try {
while (!this.obtainLock()) { // 自旋获取 redis
Thread.sleep(100); //NOSONAR
}
break;
} catch (InterruptedException e) {
/*
* This method must be uninterruptible so catch and ignore
* interrupts and only break out of the while loop when
* we get the lock.
*/
} catch (Exception e) {
localLock.unlock();
rethrowAsLockException(e);
}
}
为什么使用本地锁:首先是为了锁的可重入,其次是减轻redis服务压力。
释放锁
@Override
public void unlock() {
if (!Thread.currentThread().equals(this.thread)) {
if (this.thread == null) {
throw new IllegalStateException("Lock is not locked; " + this);
}
throw new IllegalStateException("Lock is owned by " + this.thread.getName() + ";" + this);
}
try {
if (this.reLock-- <= 0) {
try {
this.assertLockInRedisIsUnchanged();
RedisLockRegistry.this.redisTemplate.delete(constructLockKey());
if (logger.isDebugEnabled()) {
logger.debug("Released lock; " + this);
}
} finally {
this.thread = null;
this.reLock = 0;
toWeakThreadStorage(this);
}
}
} finally {
Lock localLock = RedisLockRegistry.this.localRegistry.obtain(this.lockKey);
localLock.unlock();
}
}
如果当前线程持有锁的计数 > 1,说明本地锁被当前线程多次获取,这时只释放本地锁(释放之后当前线程持有锁的计数-1)
如果当前线程持有锁的计数 = 1,释放本地锁和redis锁
使用
@Autowired
private RedisLockRegistry redisLockRegistry;
public void testLock(){
Lock obtain = redisLockRegistry.obtain("Key");
if (obtain.tryLock()) {
try {
// todo 加锁操作
} finally {
try {
obtain.unlock();
} catch (Exception e) {
// 释放锁异常
}
}
} else {
// 获取锁失败
}
}
注入 RedisLockRegistry bean
@Configuration
public class RedisLockConfiguration {
@Bean
@ConditionalOnMissingBean(RedisLockConfig.class)
public RedisLockConfig redisLockConfig() {
return new RedisLockConfig();
}
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory, RedisLockConfig redisLockConfig) {
return new RedisLockRegistry(redisConnectionFactory, redisLockConfig.getRegistryKey());
}
}
@ConfigurationProperties(prefix = "redis.lock", ignoreUnknownFields = true)
public class RedisLockConfig {
private String registryKey;
public String getRegistryKey() {
return registryKey;
}
public void setRegistryKey(String registryKey) {
this.registryKey = registryKey;
}
}
Redisson
是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。 充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。
RedisLockRegistry 和 Redisson 区别
RedisLockRegistry通过本地锁(ReentrantLock)和redis锁,双重锁实现,Redission通过Netty Future机制、Semaphore (jdk信号量)、redis锁实现。
RedisLockRegistry和Redssion都是实现的可重入锁。
RedisLockRegistry对锁的刷新没有处理,Redisson通过Netty的TimerTask、Timeout 工具完成锁的定期刷新任务。
RedisLockRegistry仅仅是实现了分布式锁,而Redisson处理分布式锁,还提供了了队列、集合、列表等丰富的API。
少年易学老难成,一寸光阴不可轻。 --朱熹