分布式架构下基于Redisson实现Redis分布式锁

一、前言

作为后台开发,相信大家都对 Redis 并不陌生了。Redis 有三个客户端 JedisRedissonLettuce。也就是提供基本的驱动来连接操作 Redis 数据库的。我们先简单介绍下这几个客户端的异同。

  • Jedis:是Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持。
  • Redisson:实现了分布式和可扩展的 Java 数据结构。
  • Lettuce:高级 Redis 客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:

  • Jedis:比较全面的提供了 Redis 的操作特性
  • Redisson:促使使用者对 Redis 的关注分离,提供很多分布式相关操作服务,例如,分布式锁分布式集合,可通过 Redis 支持延迟队列。
  • Lettuce:主要在一些分布式缓存框架上使用比较多。

对比后不难发现,Redisson 实现 Redis 分布式锁是比较好的。那为啥不是其他两个呢?不着急,我们下面一一道来。小伙伴们应该接触 Jedis 会比较多吧,因为它出现的时间比较长了。

但是随着现代系统的多核和异步,为了不断提高的吞吐量,异步非阻塞线程模型大行其道,这里面非常热门的框架就是 Netty,Netty 因其设计优秀,应用面广,实际使用的场景广泛,很多大型框架比如 Hadoop、Dubbo 等许多的底层都是通过Netty来实现的通信。所以我们就专门针对异步的且基于 Netty 的 Redis 驱动来分析,Redisson 和 Lettuce 都是基于 Netty 的也就是说他俩都是异步非阻塞的,但是他们有什么区别呢?其实在使用语法上面有一些区别,Redisson对结果做了一层包装,通过包装类来进行一些额外的操作来达到异步操作,并且 Redisson 提供了额外的分布式锁功能,那我们接下来就来说说 Redisson 的使用方式吧。

二、代码实现

在大厂中,经常有高并发场景。我就以下单减库存场景为例来模拟下怎么实现 Redis 分布式锁。

@Controller
public class RedisLockController {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockController.class);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_stock")
    @Transactional
    public ApiResult deductStock() {
        String lockKey = "lockKey";
        String clientId = UUID.randomUUID().toString();
        try {
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
            if (!flag) return new ApiResult(ReturnEnum.FAILED, "Not the same lock");
            int stockNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                int realStockNum = stockNum - 1;
                stringRedisTemplate.opsForValue().set("stock", realStockNum + "");
                LOGGER.info("扣减成功,剩余库存:{}", realStockNum);
            } else {
                LOGGER.info("扣减失败,库存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return new ApiResult();
    }
}

我这里只是简单写了一个 RedisLockController 来模拟高并发场景实现Redis分布式锁,实际中的项目业务远远复杂的多哈。

不要小看以上几行代码,包含了很多场景在里面。

1、synchronized

我们通常都喜欢用 synchronized 来加锁,这是 jvm 在单机上才能这样操作,分布式中行不通,高并发场景依旧会出现两个线程共抢到这把锁。所以要用我上面实现的 Redis分布式锁。

2、finally 语句块的代码

if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
    stringRedisTemplate.delete(lockKey);
}

这个是为了防止不是同一线程操作删除的,不这样加个 clientId 唯一标识那条线程的话,那个锁永远不会生效。

3、Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);

其实这行代码包含了以下两行代码:

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "riemann");
stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);

还有这里为什么要设置 30s 的超时呢?

因为怕整个应用宕机了,像有的时候运维直接 kill -9 或者应用没有做容灾突然断电了,导致应用挂掉。这里设置了 30s 后锁直接过期,这样可以解决应用挂掉导致没有这个锁,后面的其他请求都直接是这个锁导致程序出Bug。

所以对于高并发的业务一定要考虑到各种场景,不然一不小心就会出Bug。

下面我来介绍下基于 Redisson 实现 Redis 分布式锁。Redisson 底层封装了 Luna 脚本来实现的分布式锁,直接拿来用,放心,很多大厂用了那么多年了,Redisson 有的 Bug 基本修复的基本没有了。这样一来,也让我们开发人员省心了很多,也不至于一不小心写了有 Bug 的分布式锁业务。一旦涉及到了客户的投诉、对公司造成了影响,轻则 KPI 没有了,重则直接叫你走人。

三、基于Redisson实现Redis分布式锁

Talk is cheap, show me the code.

1、Maven 依赖


<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redissonartifactId>
    <version>3.12.3version>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2、RedissonConfig.java

@Configuration
public class RedissonConfig {
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        // 此为单机模式
        config.useSingleServer().setAddress("redis://localhost:6379");
        return Redisson.create(config);
    }
}

3、RedisLockController.java

@Controller
public class RedisLockController {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockController.class);

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_stock_2")
    @Transactional
    public ApiResult deductStock() {
        String lockKey = "lockKey";
        RLock rLock = redissonClient.getLock(lockKey);
        try {
            rLock.lock();
            int stockNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stockNum > 0) {
                int realStockNum = stockNum - 1;
                stringRedisTemplate.opsForValue().set("stock", realStockNum + "");
                LOGGER.info("扣减成功,剩余库存:{}", realStockNum);
            } else {
                LOGGER.info("扣减失败,库存不足");
            }
        } finally {
            rLock.unlock();
        }
        return new ApiResult();
    }
}

这样我们用的 Redisson 的几行代码就可以实现我们前面所担心的那些问题了。

你可能感兴趣的:(Java架构设计与分布式,Redis)