实现分布式锁

使用场景

前一篇文章说过在分布式场景下,高并发的情况下可能出现缓存击穿等缓存失效问题,解决该问题就是给业务加上锁。

提到锁可能第一印象会想到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();
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值