xmtrock
发布于 2021-12-24 / 223 阅读
0

Redisson分布式锁

<!-- org.redisson/redisson 分布式专用-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.5.3</version>
</dependency>
@Bean
public Redisson redisson(){
    // 单机模式
    Config config = new Config();
    config.useSingleServer().setAddress("redis://xxxxx:6604").setPassword("xxx").setDatabase(0);
    return (Redisson) Redisson.create(config);
}
@RestController
public class IndexController {

    @Resource
    private Redisson redisson;

    @Resource
    private StringRedisTemplate stringRedisTemplate; // 居然根据名字来找的,不是根据类型。。。

    /**
     * 代码有什么问题?
     *      1.线程安全问题,多个线程同时访问就可能出现错误,用synchronized可以解决但是性能不好
     *          synchronized在高并发情况下还是有bug出现,会出现超卖,可以用jmeter压测
     *      2.设置redis锁解决分布式场景之后,超时时间设置10s合理吗?适合场景问题?如果10s中之内没有处理完,处理到一半呢?
     *          15s--10s后释放锁--还需要5s,5s后释放了其他人的锁
     *          8s--5s后我的锁被人释放了,其他线程又来了
     *          循环下去,锁的是别人....这不就完全乱套了,这锁完全没用啊
     *        解决方法:你不是可能存在释放别人的锁的情况吗?那就设置识别号,识别到只能是自己的才能被释放
     *        这只是解决了释放别人的锁的问题,你自己没有执行完就已经超时的问题呢?
     *        答案:开启子线程定时器来延长超时时间咯,子线程每隔一段时间就去查看是否完成,没完成就加时,那这个一段时间要多长呢?
     *             三分之一过期时间,其他人的实践经验。
     *        所以:我们现在又要造轮子了吗?是否有其他人已经考虑过这个问题并做开源了呢?
     *        那肯定有啊,不然我写这个干吗。redisson,比jedis强大,专对分布式
     *
     *      3.redisson
     *          大概阐述一下这个锁的操作:
     *          当一个redisson线程过来获取到锁时,后台会有其他线程去检查是否还持有锁,
     *          还持有说明还没执行结束,就会继续延长锁的时间,大概10s去轮询。(三分之一)
     *          另外一个线程过来,如果没有获取锁成功,就会while自旋尝试加锁。
     *          clientId他在底层实现了。
     *
     *          3.1如果使用的是Redis主从架构呢,主节点宕了,从节点怎么处理?但这是锁还没有被同步过去,其他线程就过来访问了呢?
     *          3.2另外如何提升性能呢?
     *              - 商品库存分段存储,key不一样,每个段的数量越小性能不就越高嘛,而且锁定不同的key值
     *
     * @return
     */
    @RequestMapping("/deduct_stock")
    public String deductStock() {

        // 0.标识号
        String clientID = UUID.randomUUID().toString();

        // 1.这个相当于一把锁,控制只能一个人来
        String lockKey = "product001";
        RLock lock = redisson.getLock(lockKey); // 3.1
        long startMillis = 0;
        long endMillis = 0;
        try{
/*
            // 2.获取锁
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "yjiewei");// jedis.setnx(key, value);
            // 3.如果突发宕机,锁没有释放掉怎么办,key过期处理(10s),但是如果在获取锁之后就出问题呢,这一步也没有成功,大招:二合一
            stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
*/

            // 2-3
/*          Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientID, 10, TimeUnit.SECONDS);
            if (!result){
                return "error";
            }
*/

            // 3.1 解决过期时间内还未完成操作的问题
            lock.lock(30, TimeUnit.SECONDS); // 先拿锁,再设置超时时间
            startMillis = System.currentTimeMillis();

            // 4.真正操作商品库存
            synchronized (this){
                int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock");
                if (stock > 0){
                    int realStock = stock - 1;
                    stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key, value);
                    System.out.println("扣减成功,剩余库存:" + realStock);
                }else {
                    System.out.println("扣减失败,库存不足!");
                }
            }
            endMillis = System.currentTimeMillis();
        }finally {
            lock.unlock(); // 释放锁
/*
            if (clientID.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                // 5.释放锁,放在finally块中防止没有释放锁导致死锁问题
                stringRedisTemplate.delete(lockKey);
            }
*/
            System.out.println("200个库存 同步时,耗时: " + (endMillis - startMillis));
        }
        return "end";
    }
}

start from gulimall

@Configuration
public class MyRedissonConfig {
    /**
     * 所有Redission的使用都是通过RedissionClient对象
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return Redisson.create(config);
    }
}
@GetMapping("/hello/{hello}")
@ResponseBody
public String hello(@PathVariable String hello) {
    //获取一把锁。只要锁名相同就是同一把锁
    RLock lock = redissonClient.getLock("my-lock");
    //加锁,默认加锁时长是30s
    //1)锁的自动续期,如果业务超长,会在运行期间自动给锁续时长,不用担心业务时间超长过期而提前删除
    //2)加锁的业务运行完成,就不会给当前锁自动续期。即使不手动解锁,也会在30s后自动删除
    lock.lock();//阻塞式等待!悲观锁。注意如果指定时间,最好设定锁时间>业务执行时长
    try {
        System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        //解锁 假设解锁代码没有运行,redisson会不会出现死锁?
        System.out.println("释放锁...");
        lock.unlock();
    }
    return hello;
}

读写锁:保证一定能读到最新数据。修改期间,写锁是排他锁,只能独占。读锁是共享锁,可多读非独占

先写后读:等待写释放
写+写:阻塞
先读后写:有读锁,写也需要等待哟
读+读:相当于无锁,并发读同一个锁,都能读

@GetMapping("/write")
@ResponseBody
public String writeValue() {

    RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
    RLock writeLock = lock.writeLock();

    String uuid = null;
    try {
        //改数据加写锁
        writeLock.lock();
        log.warn("写锁成功...{}", Thread.currentThread().getId());
        uuid = UUID.randomUUID().toString();
        Thread.sleep(30000);
        redisTemplate.opsForValue().set("writeValue", uuid);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        writeLock.unlock();
        log.warn("写锁释放...");
    }
    return uuid;
}

@GetMapping("/read")
@ResponseBody
public String readValue() {

    RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
    RLock readLock = lock.readLock();

    //读数据加读锁
    readLock.lock();
    String uuid = null;
    try {
        log.warn("读锁成功...{}", Thread.currentThread().getId());
        uuid = redisTemplate.opsForValue().get("writeValue");
        Thread.sleep(30000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        readLock.unlock();
        log.warn("读锁释放...");
    }
    return uuid;
}

信号量(生产者-消费者)
如下,加入redis中park键的值是3,说明有3个车位.那么park3次后信号量为0,于是第四次park将会失败.
当go,表示park值回增1,这是才有新车位允许park.

@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
    RSemaphore park = redissonClient.getSemaphore("park");
    // park.acquire();//获取一个信号值.若获取不到将一直阻塞
    boolean b = park.tryAcquire(); //获取一个信号值, 获取不到给false, 以便后续操作!
    return "ok:" + b;
}

@GetMapping("/go")
@ResponseBody
public String go() {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.release();//释放一个车位
    return "ok";
}

分布式闭锁

/**
 * 放假,锁门
 * 1班没人了,2
 * 5个班全部走完,我们可以锁大门
 */
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();//等待闭锁完成

    return "放假了";
}

@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id){
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.countDown();//计数-1
    return id+"班的人都走了";
}

关于Redisson watch dog 业务执行后释放锁失败 导致的死锁问题疑问

@RequestMapping("/redis")
public class RedisLearnController {
    @Autowired
    RedissonClient redissonClient;

    @PostMapping(value = "/c")
    @ResponseBody
    public String enter3(@RequestBody String param) {
//        Executors.newFixedThreadPool(1);

        RLock rLock = redissonClient.getLock("Lock");
        if (rLock.tryLock()) {
            System.out.println("当前线程已被锁定");
            try {
                new Thread(() -> {
                    System.out.println("异步方法开始执行");
                    try {
                        Thread.sleep(1000 * 10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            System.out.println("isLocked " + rLock.isLocked() + "\n" + "isHeldByCurrentThread " + rLock.isHeldByCurrentThread());
                            rLock.unlock();
                        } catch (Exception e) {
                            System.out.println("其他线程释放锁失败的异常:" + e.toString());
                        }
                    }
                }).start();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("除特别情况下,一定会执行的 finally");
            }
        } else {
            System.out.println("获取锁失败");
        }
        return "ok()";
    }
}
当前线程已被锁定
除特别情况下,一定会执行的 finally
异步方法开始执行
isLocked true
isHeldByCurrentThread false
其他线程释放锁失败的异常:java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 3a01370a-7c95-4612-8c2a-260e62ca6330 thread-id: 111

同时,查看redis库,也能看到对应的这个锁一直存在且不断续期,不会释放
如果二次执行这个接口,获取锁会失败
如果是此时强行停止进程,watch-dog将无法判断进程状态,不会再续期,锁才会释放掉
原因在于,Redisson的锁,只能由当前的线程才能释放。redisson的锁key,由 id:threadid 来组成