大型商城:分布式锁Redisson

Redisson介绍

官网:https://github.com/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),从而让使用者能够将精力更集中地放在处理业务逻辑上。

这里仅关注分布式锁的实现,更多请参考官方文档

环境搭建

1.引入依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.17.6</version>
</dependency>  

2.写配置类

@Configuration
public class MyReissonConfig {
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        //1.创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        //2.根据Config创建出RedissonClient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

3.可以一个测试

    @Autowired
    RedissonClient redissonClient;
    @Test
    public void redisson(){
        System.out.println(redissonClient);
    }

 Redisson-lock锁测试.

可重入锁(Reentrant Lock)

如果代码走到finally,程序中断,没有结算,会不会形成死锁?

不会! Redisson-lock有看門狗

.lock方法,阻塞式等待,拿不到锁就一直获取! 默认加锁是30s

锁自动续期,如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动被删除!

加锁的业务只要运行完成,就不会给当前锁续期,及时不收到解锁,锁默认在30s以后自动删除!

Redisson-lock看门狗原理-Redisson如何解决死锁问题

如果给锁规定加锁时间

lock.lock(10, TimeUnit.SECONDS);

加锁以后10秒钟自动解锁,无需调用unlock方法手动解锁看门狗不续命,所以加锁时间要大于业务时间

1.如果我们传递了锁的时间,就会发送给redis执行脚本,进行占锁,默认超时时间就是我们指定的时间

2.如果我们未指定锁的超时时间,就使用30*1000【lockWatchdogTimeout看门狗的默认时间】;

只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,过期时间就是看门狗的默认时间,每个10s就会自动续期】

internallockLeaseTime【看门狗时间】/ 3,10s

最佳实战:

lock.lock(10, TimeUnit.SECONDS); 省掉整个续期操作,手动解锁


// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

 Redisson-读写锁测试

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。改数据加写锁,写数据加写锁

比如:一个线程加写锁,在进行写操作,,另一个线程就无法进行读操作,写锁没释放就必须等待!

读 + 读 : 相当于无锁,并发读,只会在redis中记录,所以当前的读锁,他们会同时加锁成功
写 + 读:等待写锁释放
写 + 写:阻塞方式
读 + 写:有读锁,写也需要等待

如下案例:

访问/write,加锁,写数据,睡眠3秒,期间有写锁,所以,无法访问/read,无法拿到数据,等到写锁释放后,/read可以读到最新数据

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/write")
    @ResponseBody
    public String writeValue(){
        RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.writeLock();
        try {
            //1.改数据加写锁,写数据加写锁
            rLock.lock();
            System.out.println("写锁加锁成功.."+Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("writeValue",s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            rLock.unlock();
            System.out.println("锁释放.."+Thread.currentThread().getId());
        }
        return s;
    }
    @GetMapping("/read")
    @ResponseBody
    public String readValue(){
        RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.readLock();
        rLock.lock();
        try{
            s = (String) redisTemplate.opsForValue().get("writeValue");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rLock.unlock();
        }
        return s;
    }

 Redisson-闭锁

访问锁门方法/lockDoor,不同,door.trySetCount(5),所以要访问5次/gogogo/{id},/lockDoor变通

Redsson-信号量

如以下案例:

访问3次/park就无法访问了, 3 个信号被取光,访问一次/go ,释放一个信号后,/park可再成功访问一次

 /**
     * 车库停车
     * 3个车位  向redis中存入 park 3
     * @return
     * 也可以做服务限流
     */
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = redissonClient.getSemaphore("park");
        park.acquire();  //获取一个信号,获取一个值,占一个车位
        //boolean b = park.tryAcquire();   上面是阻塞,不成功一直等,这个是返回一个布尔值
        return "ok";
    }
    //开走
    @GetMapping("go")
    @ResponseBody
    public String go(){
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();  //释放一个车位
        return "ok";
    }

缓存一致性解决

使用Redisson锁改造之前的catalogJson

    /// Redisson锁
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() throws InterruptedException {
        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock();
        Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                lock.unlock();
            }
            return  dataFromDb;
        }

遇到问题:

修改数据库之后,缓存没有修改,所以需要缓存数据一致性

1.双写模式:修改数据库后,同时修改缓存

2.失效模式:修改数据库后,同时删除缓存redis.del("catalogJSON"); 等待下次主动查询

以上两种模式,在高并下,都会产生脏数据的情况!

解决方案:

  • 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  • 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
  • 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  • 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

总结:

我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
我们不应该过度设计,增加系统的复杂性
遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

 

系统的一致性解决方案:

1.缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新

2.读写数据的时候,加上分布式锁,,,经常读,经常写!!!

 

阅读剩余
THE END