一、Redis简述
Redis is an opensource (BSD licensed), in-memory data structure store, used as a database,cache and message broker. It supports data structures such as strings, hashes,lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-diskpersistence, and provides high availability via Redis Sentinel and automaticpartitioning with Redis Cluster. (借用官网描述)
二、使用诉求
在分布式应用中我们每个模块都会有一些缓存数据需要存放起来,有三种方式,关系型数据库,如MYSQL,非关系型数据库如NoSQL、Cloudant,JVM缓存,由于读取速度的要求,我们弃用MYSQL,针对后两种场景提一下所设想的两种方案。
三、两种方案对比
首先简述一下分布式系统的缓存同步痛点。我们项目是采用spring cloud进行开发的,我的服务作为其中一个服务,服务下属多个示例,每个示例都是一模一样的,包括功能和配置,这就要求服务亦或者实例是无状态的,但是在实际开发中很难做到服务无状态,实例或多或少都会带有一些缓存信息,这里不得不提一下经典的CAP理论。CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(数据一致性)、 Availability(服务可用性)、Partition tolerance(服务分区容错性),三者不可兼得,spring cloud设计者认为分布式系统AP大于CP,所以spring cloud服务是不支持Consistency的,因此为了数据一致性的目标我们有两种选择,要么基础服务无状态,要么我们自己实现数据一致性。此处本应点一下有状态和无状态的区别,但是篇幅有限,大家自行了解即可,给出一篇示例: https://www.jianshu.com/p/51fee96f2e62;
http://dockone.io/article/3682。
前文赘述,为了解决数据一致性,我们提出两种方案,具体阐述一下两种方案。
方案一 实例同步
通过Eureka反向获取服务注册的所有实例,在spring 4中通过RestTemplate调用具体路径实现服务同步,在spring 5中webFlux框架下也可使用WebClient实现,通过请求返回信息判断同步是否成功,此时为了同步可靠性,借鉴TCP三次握手实现,流程如下:
实例获取同步示例如下:
@Autowired
private DiscoveryClientdiscoveryClient;
/**
*
服务上线
* @return
*/
@RequestMapping(value ="basic/synchronization", method = RequestMethod.GET)
public SimpleResponsesynchronization() {
ListserviceInstanceList=discoveryClient.getInstances("EVO-BASIC");
RestTemplate restTemplate=newRestTemplate();
for (ServiceInstances:serviceInstanceList
) {
/**
* do sth
**/
}
returnSimpleResponse.successResponse("Synchronization success!");
}
方案二Redis
通过Redis实现,将信息保存在Redis中,所有实例访问同一个Redis-Server。Redis提供了简单的事务机制,通过事务机制可以有效保证在高井发的场景下数据的一致性。同时Redis提供了流水线技术,极大地提升了Redis命令执行效率。Spring对Redis的支持算是十分友好的。
到这里感觉已经很简单了,但是真正的踩坑记才刚刚开始,我们从序列化,事务和流水线三方面进行踩坑记录。依赖首先说明我们使用spring-data-redis,依赖为:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
要注意的是默认使用的是lettuce而不是Jedis,如果你要使用Jedis,请把lettuce剔除,增加Jedis的依赖,二者的区别自行百度。
序列化
先从RedisTemplate的序列化开始说起。首先为什么要采用合适的序列化器,Redis默认使用的是JdkSerializationRedisSerializer,如果我们的key采用默认的序列化器,序列化过程如图所示:
由图可知,在Redis中将会把key变成一个二进制串,结果就是你使用原先的key进行查找时查找失败。RedisTemplate中的序列化器属性如图所示:
spring-data-redis的序列化类有下面这几个:
GenericToStringSerializer:可以将任何对象泛化为字符串并序列化
Jackson2JsonRedisSerializer:跟JacksonJsonRedisSerializer实际上是一样的
JacksonJsonRedisSerializer:序列化object对象为json字符串
JdkSerializationRedisSerializer:序列化java对象(被序列化的对象必须实现Serializable接口
StringRedisSerializer:简单的字符串序列化
GenericToStringSerializer:类似StringRedisSerializer的字符串序列化
GenericJackson2JsonRedisSerializer:类似Jackson2JsonRedisSerializer,但使用时构造函数不用特定的类参考以上序列化,自定义序列化类;
这里给出大家一个使用示例,即Basic中的序列化器采用:
@Bean(name="Evo_Basic_Redis")
public RedisTemplate objectRedisTemplate(){
RedisTemplate template=new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializerjackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = newObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializerstringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
template.setEnableTransactionSupport(false);
return template;
}
至此,序列化使用完成,下面开始阐述Redis事务。
Redis事务
Redis事务使用是很方便的,关键在于
template.setEnableTransactionSupport(true);
当把这个开关打开以后,在方法中调用RedisTemplate时,只需要在方法上加@Transactional注解即可,值得注意的是,Redis没有自己的事务管理器,因此需要和MYSQL共用同一个事务控制器,庆幸的是我们在配置JDBC的是一般会配置PlatformTransactionManager,这一步我们可以忽略。
@Bean
public PlatformTransactionManagertransactionManager() throws SQLException {
return newDataSourceTransactionManager(dataSource());
}
当我们打开了Redis事务支持后,在标明@Transactional注解的方法中调用RedisTemplate时,将会把Redis命令放于一个队列中,发生异常时,可以和MYSQL命令一起回滚,值得注意的两个坑说明:
1、在未用@Transactional注解标明的方法中调用RedisTemplate后,RedisTemplate连接不会主动释放,需要手动释放连接,原因是@Transactional在方法执行时会遍历得到每一个TransactionSynchronization,然后调用它的afterCompletion方法,afterCompletion方法源码如下:
publicvoid afterCompletion(int status) {
try {
switch (status) {
//如果事务正常,最终提交事务
case TransactionSynchronization.STATUS_COMMITTED:
connection.exec();
break;
//如果有异常,事务回滚
case TransactionSynchronization.STATUS_ROLLED_BACK:
case TransactionSynchronization.STATUS_UNKNOWN:
default:
connection.discard();
}
} finally {
if (log.isDebugEnabled()) {
log.debug("Closing bound connection after transaction completed with "+ status);
}
connHolder.setTransactionSyncronisationActive(false);
//关闭连接
connection.close();
//从当前线程释放连接
TransactionSynchronizationManager.unbindResource(factory);
}
}
我们可以看到在调用结束后会主动释放连接,但是在未用@Transactional注解标明的方法中调用后就需要我们手动释放了,释放连接代码示例:
/**
*普通缓存获取
* @param key 键
* @return value
*/
public Object get(String key){
Object value = redisTemplate.opsForValue().get(key);
TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
return value;
}
未释放连接的原因如下:
public static void releaseConnection(RedisConnection conn, RedisConnectionFactoryfactory) {
if (conn == null) {
return;
}
RedisConnectionUtils.RedisConnectionHolder connHolder =(RedisConnectionUtils.RedisConnectionHolder)TransactionSynchronizationManager.getResource(factory);
/**
*可以获取到connHolder 但是connHolder.isTransactionSyncronisationActive()却是false,
*因为之前绑定连接的时候,并没有在一个事务中,连接绑定了,但是isTransactionSyncronisationActive属性
*并没有给值,可以看一下第四步potentiallyRegisterTransactionSynchronisation中的代码,其实是没有执行的,
*所以 isTransactionSyncronisationActive 的默认值是false
**/
if (connHolder != null &&connHolder.isTransactionSyncronisationActive()) {
if (log.isDebugEnabled()) {
log.debug("Redis Connection will be closed when transactionfinished.");
}
return;
}
// release transactional/read-only and non-transactional/non-bound connections.
// transactional connections for read-only transactions get no synchronizerregistered
//第一个条件判断为true 但是第二个条件判断为false 不是一个只读事务,所以unbindConnection(factory) 代码没有执行
if (isConnectionTransactional(conn, factory)&&TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
unbindConnection(factory);
//然后 else if 也是返回false 因为isConnectionTransactional(conn, factory)返回的是true 内部代码判断连接这个连接和 线程中绑定的连接是不是同一个,是同一个 由于前面加了一个 ! 号 所以结果为false
} else if (!isConnectionTransactional(conn, factory)) {
if (log.isDebugEnabled()) {
log.debug("Closing Redis Connection");
}
conn.close();
}
}
第一个坑阐述完毕,我们再讲第二个坑
2、Redis事务和MYSQL事务不同,Redis事务在执行时不会立即执行命令,而是放到队列中,延迟执行,因此如果你这样使用的话:
@Transactional
public Long getCurrentTenantId() {
Object tenantIdStr=context.redisTemplate.opsForValue().get(Redis_Tenant_Id);
return Long.valueOf(tenantIdStr.toString());
}
那么恭喜你,你查询到值一定是NULL,因为这条命令不会被执行,Basic采用的用法是配置两个RedisTemplet,如下:
@Bean(name="Evo_Basic_Redis")
public RedisTemplateobjectRedisTemplate(){
RedisTemplate template=new RedisTemplate<>();
template.setConnectionFactory(factory);
/**
* do sth
**/
template.setEnableTransactionSupport(false);
return template;
}
@Bean(name="Evo_Basic_Redis_Write")
public RedisTemplate objectWriteRedisTemplate(){
RedisTemplate template=new RedisTemplate<>();
template.setConnectionFactory(factory);
/**
* do sth
**/
template.setEnableTransactionSupport(true);
return template;
}
一个关闭事务支持用来执行读操作,另一个打开事务支持执行写操作。
Redis流水线
我们可能一直没有思考过如下代码的执行过程:
redisTemplateopsForValue () .set (” keyl”,”valuel” ) ;
redisTemplate opsForHash( .put (”hash ”,”field ", "value");
看着在一个方法中执行,但实际上它们是在两个连接中完成的,即执行完第一个命令后redisTemplate会断开连接,执行第二条命令时再申请新的连接,如果想深挖的话可以研究一下Redis的连接池。这样显然存在资源浪费的问题。为了克服这个问题,Spring 为我们提供了RedisCallback和SessionCallback两个接口,它们的作用是让RedisTemplate进行回调,通过他们可以在同一条连接下执行多Redis命令。其中SessionCallback提供了良好的封装,对于开发者比较友好,因此在实际的开发中应该优先选择使用它;相对而言RedisCallback接口比较底层,需要处理的内容也比较多,可读性较差,所以非必要的时候尽量不选择使用它。代码示例如图所示:
而流水线接口的调用类似于excute,调用方法为executePipelined,二者的区别我决定采用官网原文来描述,
Redis provides support forpipelining,which involves sending multiple commands to the server without waiting for thereplies and then reading the replies in a single step. Pipelining can improveperformance when you need to send several commands in a row, such as addingmany elements to the same List.Spring Data Redis provides severalRedisTemplatemethodsfor executing commands in a pipeline. If you do not care about the results ofthe pipelined operations, you can use the standardexecutemethod, passingtruefor thepipelineargument. TheexecutePipelinedmethods run theprovidedRedisCallbackorSessionCallbackin a pipeline andreturn the results, as shown in the following example:
//popa specified number of items from a queue
List results = stringRedisTemplate.executePipelined(
new RedisCallback() {
public ObjectdoInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnectionstringRedisConn = (StringRedisConnection)connection;
for(int i=0; i< batchSize; i++){
stringRedisConn.rPop("myqueue");
}
return null;
}
});
关于这三块的描述感觉自己还是很多没有讲出来,更多细节还是希望大家通过源码或者官网doc进行探索,官网地址为:https://docs.spring.io/spring-data/redis/docs/2.1.6.RELEASE/reference/html/#tx.spring
四、基础服务Redis使用流程
Basic服务的更新流程如下,为了保证数据一致性,我们觉得更新必须保证;两个操作都正常完成,否则不予更新,流程图如下:
查询流程如下:
我们在其中加入了查询MYSQL的流程,主要是为了防止Redis_Server发生异常的情况,保证系统的可用性即服务的Availability。至于系统启动时对Redis服务器的同步流程则不做赘述。