实现分布式锁
使用场景
前一篇文章说过在分布式场景下,高并发的情况下可能出现缓存击穿等缓存失效问题,解决该问题就是给业务加上锁。
提到锁可能第一印象会想到synchronized,但是synchronized只能解决本地服务的线程问题,面对分布式下的集群(一个服务在多台服务器运行),本地锁明显是解决不了问题的,这就引出了分布式锁的概念。
原始解决方法
- 结合redis的set命令,set命令后面可以加NX,EX,XX等参数,下面我们运用到的主要是NX和EX,分别是不存在和存活时间(过期时间)。
- 在Java中redis的set带参数命令相当于setIfAbsent方法。
- 具体过程如下
public Object getDataFromDBWithRedisLock() {
//加锁,防高并发下缓存失效问题
String uuid = UUID.randomUUID().toString();
//该代码创建了一个名为lock的锁,并给该锁设置30秒的过期时间;倘若锁不存在,创建成功,返回true,否则返回false
//设置过期时间的意义在于,若在还没执行到解锁的过程中,出了问题导致程序中断,这时候会根据过期时间自动删除锁,不会让锁霸占着位子不放导致死锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (lock){
System.out.println("获取分布式锁成功");
try {
//业务代码
} finally {
//lua脚本解锁,删除锁,保持在值对比和删除锁的过程中保持原子性
//使用lua脚本解锁,保证不会在删锁的过程中,正在删的锁刚刚好过期,导致新的进程进入,这时候删除的锁就不是原来的锁,而是刚刚进入的新锁,导致错误
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
//删除锁
Long execute = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return new Object();
}else {//如果加锁不成功,则是因为锁被其他线程抢占,此时就会自旋,等待占用中的锁释放后抢占锁
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDataFromDBWithRedisLock();//自旋
}
}
整合Redis的客户端Redisson
Redisson
- Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson的分布式锁和同步器
可重入锁
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();//阻塞式等待,默认加锁30s(看门狗时间)
//如果业务处理时间过长,该方式会给锁自动续期,每10s就给过期时间续满30s,直到业务处理完成,则不再续期,即使不手动解锁,默认30s后自动删除。
try {
//业务
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
// 加锁以后35秒钟自动解锁
// 无需调用unlock方法手动解锁
//此处的过期时间必须大于业务时间,这里不会自动续期过期时间
lock.lock(35, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
- 推荐使用第二种,不会有锁的续期时间,节省时间,但是注意设置时间一定要大于业务处理时间。
读写锁
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
- 读写锁保证一定能获取到最新的数据,在修改期间,写锁是一个排他锁(互斥锁,独享锁),读锁是一个共享锁。
- 当读锁、写锁两者同时进行时,只要有写锁,就得等待。
- 读+读:相当于无锁,可以同时获取数据,同时加锁成功。
- 读+写:写锁得等待读锁完成。
- 写+读:读锁得等待写锁完成。
- 写+写:阻塞等待写锁。
闭锁
//接口1
//创建一个名为anyCountDownLatch的闭锁
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//设置闭锁的次数
latch.trySetCount(5);
//阻塞,等待闭锁完成
latch.await();
//接口2
// 创建一个名字跟上述一样的闭锁
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
//执行完成业务后,闭锁完成数-1,即计数减1
latch.countDown();
信号量
RSemaphore semaphore = redisson.getSemaphore("semaphore");
//获取占位
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//释放占位
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
- 信号量可以用作分布式限流
使用Redisson加上分布锁
配置和引入依赖
- 引入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson 作为分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
- 编写配置类
@Configuration
public class MyRedissonConfig {
/**
* 所有redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
具体实现
使用了redisson的好处,相对于原始的分布式锁,少了过期时间的设置,还有删除缓存的代码编写,使代码更加整洁。
@Autowired
private RedissonClient redissonClient;
public Object getDataFromDBWithRedissonLock() {
RLock lock = redissonClient.getLock("Catalog_lock");
lock.lock();
try {
//业务代码
} finally {
lock.unlock();
}
return new Object();
}
使用spring cache解决
配置和引入依赖
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 编写配置类
@Configuration
@EnableConfigurationProperties(CacheProperties.class)//允许加入spring cache的默认配置
public class MyCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration (CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//自定义配置,让存储的值以json格式存储,兼容性高
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//读取默认配置 ,下列配置都是复制源码的默认配置
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
- 配置启动类,给启动类加上注解
@EnableCaching
- 配置文件
spring.cache.type=redis//使用的缓存类型
spring.cache.redis.time-to-live= 360000//设置缓存过期时间
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
具体实现
//sync是加锁,防止缓存击穿。
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)
public Object getDataFromDBWithCache() {
//业务代码
return new Object();
}