点击上方“芋道源码
做积极的人,而不是积极废人!
源码精品专栏
摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Redis/ 「芋道源码」欢迎转载,保留摘要,谢谢!
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
包下拆成 mysql
、redis
包。这样,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
T 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);
传入和返回的是二进制