• 4. 项目实践

    • 4.1 Cache Object

    • 4.2 数据访问层

    • 4.3 序列化

  • 5. 示例补充

    • 5.1 Pipeline

    • 5.2 Transaction

    • 5.3 Session

    • 5.4 Pub/Sub

    • 5.5 Script

  • 6. 尝试 Redisson

    • 6.1 快速入门

    • 6.2 Redis 分布式锁

    • 6.3 Redis 限流器

  • 666. 彩蛋


4. 项目实践

本小节,我们来分享我们在生产中的一些实践。关于这块,希望大家可以一起讨论,能够让我们的代码更加优雅干净。

4.1 Cache Object

在我们使用数据库时,我们会创建 dataobject 包,存放 DO(Data Object)数据库实体对象。

那么同理,我们缓存对象,怎么进行对应呢?对于复杂的缓存对象,我们创建了 cacheobject 包,和 dataobject 包同层。如:

service # 业务逻辑层
dao # 数据库访问层
dataobject # DO
cacheobject # 缓存对象

并且所有的 Cache Object 对象使用 CacheObject 结尾,例如说 UserCacheObject、ProductCacheObject 。

4.2 数据访问层

在我们访问数据库时,我们会创建 dao 包,存放每个 DO 对应的 Dao 对应。那么对于每一个 CacheObject 类,我们也会创建一个其对应的 Dao 类。例如说,UserCacheObject 对应 UserCacheObjectDao 类。示例代码如下:

@Repository
public class UserCacheDao {

    private static final String KEY_PATTERN = "user:%d"// user:用户编号 <1>

    @Resource(name = "redisTemplate")
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    private ValueOperations<String, String> operations; // <2>

    private static String buildKey(Integer id) // <3>
        return String.format(KEY_PATTERN, id);
    }

    public UserCacheObject get(Integer id) {
        String key = buildKey(id);
        String value = operations.get(key);
        return JSONUtil.parseObject(value, UserCacheObject.class);
    }

    public void set(Integer id, UserCacheObject object) {
        String key = buildKey(id);
        String value = JSONUtil.toJSONString(object);
        operations.set(key, value);
    }

}
  • <1> 处,通过静态变量,声明 KEY 的前缀,并且使用冒号作为间隔

  • <3> 处,声明 KEY_PATTERN 对应的 KEY 拼接方法,避免散落在每个方法中。

  • <2> 处,通过 @Resource 注入指定名字的 RedisTemplate 对应的 Operations 对象,这样明确每个 KEY 的类型。

  • 剩余的,就是每个方法封装对应的操作。

可能会有胖友问,为什么不支持将 RedisTemplate 直接 Service 业务层调用呢?如果这样,我们业务代码里,就容易混杂着很多 Redis 访问代码的细节,导致很脏乱。我们试着把 RedisTemplate 想象成 Spring JDBCTemplate ,我们一定会声明对应的 Dao 类,访问数据库。所以,同理落。

那么还有一个问题,UserCacheDao 放在哪个包下?目前的想法是,将 dao 包下拆成 mysqlredis 包。这样,MySQL 相关的 Dao 放在 mysql 包下,Redis 相关的 Dao 放在 redis 。

4.3 序列化

在 「3. 序列化」 小节中,我们仔细翻看了每个序列化方式,暂时没有一个能够完美的契合我们的需求,所以我们直接使用最简单的 StringRedisSerializer 作为序列化实现类。而真正的序列化,我们在各个 Dao 类里,自己手动来调用。

例如说,在 UserCacheDao 示例中,已经看到了这么做了。这里还有一个细化点,虽然我们是自己手动序列化,可以自己简单封装一个 JSONUtil 类,未来如果我们想换 JSON 库,就比较方便了。其实,这个和 Spring Data Redis 所做的封装是一个思路。

5. 示例补充

像 String、List、Set、ZSet、Geo、HyperLogLog 等等数据结构的操作,胖友自己去用用对应的 Operations 操作类的 API 方法,就非常容易懂了,我们更多的,补充 Pipeline、Transaction、Pub/Sub、Script 等等功能的示例。

5.1 Pipeline

如果胖友没有了解过 Redis 的 Pipeline 机制,可以看看 《Redis 文档 —— Pipeline》 文章,批量操作,提升性能必备神器。

在 RedisTemplate 类中,提供了 2 组四个方法,用于执行 Redis Pipeline 操作。代码如下:

// <1> 基于 Session 执行 Pipeline
@Override
public List<Object> executePipelined(SessionCallback<?> session) {
    return executePipelined(session, valueSerializer);
}
@Override
public List<Object> executePipelined(SessionCallback<?> session, @Nullable RedisSerializer<?> resultSerializer) {
    // ... 省略代码
}

// <2> 直接执行 Pipeline
@Override
public List<Object> executePipelined(RedisCallback<?> action) {
    return executePipelined(action, valueSerializer);
}
@Override
public List<Object> executePipelined(RedisCallback<?> action, @Nullable RedisSerializer<?> resultSerializer) {
    // ... 省略代码
}
  • 两组方法的差异,在于是否是 Session 中执行。那么 Session 是什么呢?卖个关子,在 「5.3 Session」 中来详细解析。本小节,我们只讲 Pipeline + RedisCallback 的组合的方法。

  • 每组方法里,差别在于是否传入 RedisSerializer 参数。如果不传,则使用 RedisTemplate 自己的序列化相关的属性。

5.1.1 源码解读

在看具体的 #executePipelined(RedisCallback<?> action, ...) 方法的示例之前,我们先来看一波源码,这样我们才能更好的理解具体的使用方法。代码如下:

// RedisTemplate.java
@Override
public List<Object> executePipelined(RedisCallback<?> action, @Nullable RedisSerializer<?> resultSerializer) {
    // <1> 执行 Redis 方法
    return execute((RedisCallback<List<Object>>) connection -> {
        // <2> 打开 pipeline
        connection.openPipeline();
        boolean pipelinedClosed = false// 标记 pipeline 是否关闭
        try {
            // <3> 执行
            Object result = action.doInRedis(connection);
            // <4> 不要返回结果
            if (result != null) {
                throw new InvalidDataAccessApiUsageException(
                        "Callback cannot return a non-null value as it gets overwritten by the pipeline");
            }
            // <5> 提交 pipeline 执行
            List<Object> closePipeline = connection.closePipeline();
            pipelinedClosed = true;
            // <6> 反序列化结果,并返回
            return deserializeMixedResults(closePipeline, resultSerializer, hashKeySerializer, hashValueSerializer);
        } finally {
            if (!pipelinedClosed) {
                connection.closePipeline();
            }
        }
    });
}
  • <1> 处,调用 #execute(RedisCallback action) 方法,执行 Redis 方法。注意,此处传入的 action 参数,不是我们传入的 RedisCallback 参数。我们的会在该 action 中被执行。

  • <2> 处,调用 RedisConnection#openPipeline() 方法,自动打开 Pipeline 模式。这样,我们就不需要手动去打开了。

  • <3> 处,调用我们传入的实现的 RedisCallback#doInRedis(RedisConnection connection) 方法,执行在 Pipeline 中,想要执行的 Redis 操作。

  • <4> 处,不要返回结果。因为 RedisCallback 是统一定义的接口,所以可以返回一个结果。但是在 Pipeline 中,未提交执行时,显然是没有结果,返回也没有意思。简单来说,就是我们在实现 RedisCallback#doInRedis(RedisConnection connection) 方法时,返回 null 即可。

  • <5> 处,调用 RedisConnection#closePipeline() 方法,自动提交 Pipeline 执行,并返回执行结果。

  • <6> 处,反序列化结果,并返回 Pipeline 结果。

至此,Spring Data Redis 对 Pipeline 的封装,我们已经做了一个简单的了解,实际就是经典的“模板方法”设计模式化的应用。下面,在让我们来看看 org.springframework.data.redis.core.RedisCallback<T> 接口,Redis 回调接口。代码如下:

// RedisCallback.java
public interface RedisCallback<T{

    /**
     * Gets called by {@link RedisTemplate} with an active Redis connection. Does not need to care about activating or
     * closing the connection or handling exceptions.
     *
     * @param connection active Redis connection
     * @return a result object or {@code null} if none
     * @throws DataAccessException
     */

    @Nullable
    doInRedis(RedisConnection connection) throws DataAccessException;
}
  • 虽然接口名是以 Callback 结尾,但是通过 #doInRedis(RedisConnection connection) 方法可以很容易知道,实际可以理解是 Redis Action ,想要执行的 Redis 操作。

  • 有一点要注意,传入的 connection 参数是 RedisConnection 对象,它提供的 'low level' 更底层的 Redis API 操作。例如说:

    // RedisStringCommands.java
    // RedisConnection 实现 RedisStringCommands 接口

    byte[] get(byte[] key);

    Boolean set(byte[] key, byte[] value);
    • 传入和返回的是二进制数组,实际就是 RedisTemplate 已经序列化的入参和会被反序列化的出参。

5.1.2 具体示例

示例代码对应测试类:PipelineTest 。

创建 PipelineTest 单元测试类,编写代码如下:

// PipelineTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PipelineTest {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void test01() {
        List<Object> results = stringRedisTemplate.executePipelined(new RedisCallback<Object>() {

            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                // set 写入
                for (int i = 0; i < 3; i++) {
                    connection.set(String.format("yunai:%d", i).getBytes(), "shuai".getBytes());
                }

                // get
                for (int i = 0; i < 3; i++) {
                    connection.get(String.format("yunai:%d", i).getBytes());
                }

                // 返回 null 即可
                return null;
            }
        });

        // 打印结果
        System.out.println(results);
    }
}

执行 #test01() 方法,结果如下:

[truetruetrue, shuai, shuai, shuai]
  • 因为我们使用 StringRedisTemplate 自己的序列化相关属性,所以 Redis GET 命令返回的二进制,被反序列化成了字符串。

5.2 Transaction

基情提示:实际项目实战中,Redis Transaction 事务基本不用,至少问了一些胖友,包括自己,都没有再用。所以呢,本小节可以选择性看看。或者,就不看,哈哈哈哈。

在看 Redis Transaction 事务之前,我们先回想下 Spring 是如何管理数据库 Transaction 的。在应用程序中处理一个请求时,如果我们的方法开启Trasaction 功能,Spring 会把数据库的 Connection 连接和当前线程进行绑定,从而实现 Connection 打开一个 Transaction 后,所有当前线程的数据库操作都在该 Connection 上执行,达到所有操作在这个 Transaction 中,最终提交或回滚。

在 Spring Data Redis 中,实现 Redis Transaction 也是这个思路。通过 SessionCallback 操作 Redis 时,会从当前线程获得 Redis Connection ,如果获取不到,则会去“创建”一个 Redis Connection 并绑定到当前线程中。这样,我们在该 Redis Connection 开启 Redis Transaction 后,在该线程的所有操作,都可以在这个 Transaction 中,最后交由 Spring 事务管理器统一提供或回滚 Transaction 。

如果想要使用 Redis Transaction 功能,需要创建 RedisTemplate Bean 时,设置其 enableTransactionSupport 属性为 true ,默认为 false 不开启。示例如下:

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        // 【重要】设置开启事务支持
        template.setEnableTransactionSupport(true);

        // 设置 RedisConnection 工厂。它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
       template.setConnectionFactory(factory);

       // 使用 String 序列化方式,序列化 KEY 。
       template.setKeySerializer(RedisSerializer.string());

       // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
       template.setValueSerializer(RedisSerializer.json());
       return template;
   }

}

©著作权归作者所有:来自51CTO博客作者mb5ff80520dfa04的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. DoDAF2.0方法论探究
  2. http协议请求方法都有哪些?网络安全学习提升
  3. flask示例
  4. 【前端词典】8 个提高 JS 性能的方法
  5. AngularJS 日期时间选择组件(附详细使用方法)
  6. 5 种方法教你用Python玩转histogram直方图
  7. IDEA Debug 无法进入断点的解决方法
  8. libp2p-rs kad 使用及调试方法
  9. 只会爬虫不会反爬虫?动图详解利用 User-Agent 进行反爬虫的原理和

随机推荐

  1. 学Linux云计算技术有意义吗?如何学习linux
  2. nginx1.19.1自动安装部署脚本
  3. 利用端口扫描进行终端合规性检查的一个示
  4. 动画:散列表 | 文本编辑器是如何检查英文
  5. 认命,但就不服命!
  6. Linux学习之linux的find命令如何使用?
  7. python数据类型的强制转换
  8. 动画:面试必刷之对称的二叉树
  9. 字符串处理函数
  10. 就说一件事!