跳至主要內容

如何设计一个Redis客户端SDK

勇哥cachecacheRedis约 1968 字大约 7 分钟

这篇文章,我们聊聊如何设计一个 Redis 客户端 SDK 。

1 SDK 设计思路

SDK 的设计理念核心两点:

  1. 提供精简的 API 供开发者使用,方便与用户接入;

  2. 屏蔽三方依赖,用户只和对外 API 层交互 。

Redis SDK 如上图,分为三层:

  1. 最上层为 API ,用户与该层打交道 ,为了方便用户方便接入,设计了springboot starter模块。

  2. 中间为核心功能层,提供了两类功能:Redis 操作命令(比如 String 相关命令)和增强功能(比如分布式锁)。

  3. 最底层是三方依赖,我们有两种选择:自己实现与 Redis 服务端的交互,工作量非常大,另一种是选择选择成熟的 Redis Java 客户端框架 ,再此基础上进行封装,并补充其他的实现。

    笔者选择 Redisson 框架,因为该框架实现了非常高级的功能,比如分布式锁,延迟队列等。

2 操作命令封装

封装操作命令时基本原则是:尽量不要提供危险的接口供开封者使用,尽量减少开封者的使用心智

比如,KEYS 命令用于查找符合指定模式的所有键。它是一个用于调试目的的命令,但在生产环境中不推荐频繁使用,因为在大型数据库中执行 KEYS 命令可能会导致性能问题。考虑到假如提供给开发者使用会引发,我们并不会封装 KEYS 命令给开发者使用。

项目分为四个模块:

  1. 客户端模块:核心模块,封装了 Redis 操作命令(比如 String 相关命令)和增强功能(比如分布式锁)。
  2. springboot starter : 开发者只需要在 pom.xml 引入 starter 的依赖定义,在配置文件中编写约定的配置
  3. ID 生成器: 通过 Redis 改造雪花算法生成唯一编号。
  4. 使用示例:使用简单的 springboot starter 的例子。

本节我们重点讲解客户端模块的实现。

//1. 定义配置
SingleConfig config = new SingleConfig();
config.setAddress("redis://127.0.0.1:6379");

//2. 定义Redis操作对象
RedisOperation redisOperation = new RedisOperation(config);

// 3.使用String命令的 setEx 方法
StringCommand stringCommand = redisOperation.getStringCommand();
stringCommand.setEx("hello", "mylife", 109);

// 4.使用Hash命令的hset方法
HashCommand hashCommand = this.redisOperation.getHashCommand();
hashCommand.hset("myhash", "time", "mybatis");

首先我们定义一个配置类,例子中是单机配置类 SingleConfig ,还有集群配置、主从配置。

然后初始化操作对象 RedisOperation , 参数是配置对象。最后获取内置的 String 命令Hash 命令对象,调用该对象的操作方法。

RedisOperation 对象内置了 RedissonClient 对象 ,该对象对用户是不可视的。

public RedisOperation(SingleConfig SingleServerConfig) {
       Config config = ConfigBuilder.buildBySingleServerConfig(SingleServerConfig);
       //默认string编解码
       config.setCodec(new StringCodec());
       this.redissonClient = Redisson.create(config);
}

三种根据配置类初始化的构造函数,每个构造函数内会创建初始化 RedissonClient 对象。

然后根据 RedissonClient 对象创建不同的操作命令:

public StringCommand getStringCommand() {
      StringCommand StringCommand = new StringCommandImpl(this.redissonClient);
      return StringCommand;
}
public StringCommand getStringCommand(RedisCodec RedisCodec) {
      StringCommand StringCommand = new StringCommandImpl(this.redissonClient , RedisCodec);
      return StringCommand;
}
public AtomicCommand getAtomicCommand() {
      AtomicCommand AtomicCommand = new AtomicCommandImpl(this.redissonClient);
      return AtomicCommand;
}
public ListCommand getListCommand() {
      ListCommand ListCommand = new ListCommandImpl(this.redissonClient);
      return ListCommand;
}
// 省略其他的命令代码

最后,我们看看字符串操作命令 StringComand 如何实现。

1、核心接口

public interface StringCommand extends KeyCommand {

    String get(String key);

    void set(String key, String value);

    void setEx(String key, String value, int second);

    boolean setNx(String key, String value);

    boolean setNx(String key, String value, int aliveSecond);

    Map<String, Object> mget(String... keys);

}

2、接口实现类

public class StringCommandImpl extends KeyCommandImpl implements StringCommand {

    public StringCommandImpl(RedissonClient redissonClient) {
        super(redissonClient);
    }

    public StringCommandImpl(RedissonClient redissonClient, RedisCodec redisCodec) {
        super(redissonClient, redisCodec);
    }

    public String get(final String key) {
        return invokeCommand(new InvokeCommand<String>(RedisCommandType.GET) {
            @Override
            public String exe(RedissonClient redissonClient) {
                RBucket<String> result = getRedissonClient().getBucket(key);
                if (result == null || !result.isExists()) {
                    return null;
                }
                return result.get();
            }
        });
    }

    public void set(final String key, final String value) {
        invokeCommand(new InvokeCommand<String>(RedisCommandType.SET) {
            @Override
            public String exe(RedissonClient redissonClient) {
                RBucket<String> result = getRedissonClient().getBucket(key);
                result.set(value);
                return null;
            }
        });
    }

    public void setEx(final String key, final String value, final int second) {
        invokeCommand(new InvokeCommand<String>(RedisCommandType.SET_EX) {
            @Override
            public String exe(RedissonClient redissonClient) {
                RBucket<String> result = getRedissonClient().getBucket(key);
                result.set(value, second, TimeUnit.SECONDS);
                return null;
            }
        });
    }

    public boolean setNx(final String key, final String value) {
        return invokeCommand(new InvokeCommand<Boolean>(RedisCommandType.SET_NX) {
            @Override
            public Boolean exe(RedissonClient redissonClient) {
                RBucket<String> result = getRedissonClient().getBucket(key);
                return result.trySet(value);
            }
        });
    }

    public boolean setNx(final String key, final String value, final int aliveSecond) {
        return invokeCommand(new InvokeCommand<Boolean>(RedisCommandType.SET_NX) {
            @Override
            public Boolean exe(RedissonClient redissonClient) {
                RBucket<String> result = getRedissonClient().getBucket(key);
                return result.trySet(value, aliveSecond, TimeUnit.SECONDS);
            }
        });
    }

    public Map<String, Object> mget(final String... keys) {
        return invokeCommand(new InvokeCommand<Map<String, Object>>(RedisCommandType.MGET) {
            @Override
            public Map<String, Object> exe(RedissonClient redissonClient) {
                return getRedissonClient().getBuckets().get(keys);
            }
        });
    }

}

实现类里面方法实现都比较简单,都是使用 RedissonClient 的 API 方法 ,我们做一层简单的包装。

在包装类内部,我们除了实现基本的 API 调用之外,也可以做访问统计等额外功能。

3 实现 springboot starter

3.1 启动器

我们都知道,Spring Boot 基于“约定大于配置”(Convention over configuration)这一理念来快速地开发、测试、运行和部署 Spring 应用,并能通过简单地与各种启动器(如 spring-boot-web-starter)结合,让应用直接以命令行的方式运行,不需再部署到独立容器中。

Spring Boot starter 构造的启动器使用起来非常方便,开发者只需要在 pom.xml 引入 starter 的依赖定义,在配置文件中编写约定的配置即可。

很多开源组件都会为 Spring 的用户提供一个 spring-boot-starter 封装给开发者,让开发者非常方便集成和使用。

spring-boot-starter 实现流程如下:

01、定创建starter项目,定义 Spring 自身的依赖包和 Bean 的依赖包 ;

02、定义spring.factories 文件

在 resources 包下创建 META-INF 目录后,新建 spring.factories 文件,并在文件中定义自动加载类,文件内容格式:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
xx.xx.xx.xx.xx.MyConfig

spring boot 会根据文件中配置的自动化配置类来自动初始化相关的 Bean、Component 或 Service。

03、配置自动配置类

编写自动配置类,这些类将在Spring应用程序中自动配置starter。自动配置类应该有一个@Configuration注解,并且应该包含可以覆盖的默认值,以允许用户自定义配置。在自动配置类中,可以使用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,以便只有在需要的情况下才会配置 starter。

3.2 实现方式

首先在 resources 包下创建 META-INF 目录后,新建 spring.factories 文件,并在文件中定义自动加载类,文件内容格式:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.courage.platform.redis.client.springboot.starter.configuration.RedisClientAutoConfiguration

然后定义自动配置类 RedisClientAutoConfiguration

@Configuration
public class RedisClientAutoConfiguration {

    @Configuration
    @ConditionalOnMissingBean(Config.class)
    @ConditionalOnProperty(name = "spring.redis.type", havingValue = "single")
    static class StaticBuildSingleServer {

        @Bean(value = "platformSingleServerConfig")
        @ConfigurationProperties(prefix = "spring.redis.single")
        public SingleConfig getSingleConfig() {
            SingleConfig config = new SingleConfig();
            return config;
        }

        @Bean
        public Config singleServerConfig(SingleConfig singleConfig) {
            return ConfigBuilder.buildBySingleServerConfig(singleConfig);
        }

        @Bean(destroyMethod = "shutdown")
        public RedisOperation redisOperation(Config config) {
            RedisOperation redisOperation = new RedisOperation(config);
            return redisOperation;
        }

    }

    @Configuration
    @ConditionalOnMissingBean(Config.class)
    @ConditionalOnProperty(name = "spring.redis.type", havingValue = "sentinel")
    static class StaticBuildSentinelServer {

        @Bean(value = "platformSentinelServerConfig")
        @ConfigurationProperties(prefix = "spring.redis.sentinel")
        public SentinelConfig getPlatformSentinelServersConfig() {
            SentinelConfig sentinelConfig = new SentinelConfig();
            return sentinelConfig;
        }

        @Bean
        public Config sentinelServerConfig(SentinelConfig sentinelConfig) {
            return ConfigBuilder.buildBySentinelServerConfig(sentinelConfig);
        }

        @Bean(destroyMethod = "shutdown")
        public RedisOperation redisOperation(Config config) {
            RedisOperation redisOperation = new RedisOperation(config);
            return redisOperation;
        }
    }

    @Bean("stringCommand")
    @ConditionalOnBean(RedisOperation.class)
    public StringCommand createStringCommand(RedisOperation redisOperation) {
        return redisOperation.getStringCommand();
    }

    @Bean
    @ConditionalOnBean(RedisOperation.class)
    public ZSetCommand createZSetCommand(RedisOperation redisOperation) {
        return redisOperation.getZSetCommand();
    }

    @Bean
    @ConditionalOnBean(RedisOperation.class)
    public SetCommand createPlatformSetCommand(RedisOperation redisOperation) {
        return redisOperation.getSetCommand();
    }

    @Bean
    @ConditionalOnBean(RedisOperation.class)
    public AtomicCommand createPlatformAtomicCommand(RedisOperation redisOperation) {
        return redisOperation.getAtomicCommand();
    }

    @Bean
    @ConditionalOnBean(RedisOperation.class)
    public HashCommand createHashCommand(RedisOperation redisOperation) {
        return redisOperation.getHashCommand();
    }

    @Bean
    @ConditionalOnBean(RedisOperation.class)
    public ScriptCommand createPlatformScriptCommand(RedisOperation redisOperation) {
        return redisOperation.getScriptCommand();
    }

    @Bean
    @Primary
    @ConditionalOnBean(RedisOperation.class)
    public IdGenerator createIdGenerator(RedisOperation redisOperation) {
        return new IdGenerator(redisOperation);
    }

}

该配置类首先会根据配置类创建 RedisOperation 对象 ,然后获取命令对象(比如 StringCommand、HashCommand)注入到 Spring 容器里。

3.3 如何使用

1、pom文件添加依赖

<dependency>
     <groupId>com.courage</groupId>
     <artifactId>platform-redis-client-springboot-starter</artifactId>
     <version>1.0.0-SNAPSHOT</version>
</dependency>

2、yaml缓存配置

spring:
  redis:
    type: single
    single:
      address: 127.0.0.1:6379

3、使用缓存

@Autowired
private RedisOperation redisOperation;

@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hellodanbiao() {
     String mylife = redisOperation.getStringCommand().get("hello");
     System.out.println(mylife);
     redisOperation.getStringCommand().set("hello", "zhang勇");
     return "hello-service";
}

参考资料:

Redis 集成简介:https://juejin.cn/post/7076244567569203208