Redis 在项目中的使用方式


3/3/2017 Redis

Redis 的几种集成方式

缓存服务组件

依赖于:

  1. jedis
  2. spring-data-redis
  3. spring-session-data-redis

redis 集群使用的是 ShardedJedisPool, redis 3.x 后自带集群负载

jar中重要的类

  1. JedisConnectionFactory 用于获取 jedis 实例,从而操作 redis

  2. ShardedJedisPool 用于连接 redis 集群

cache 重要的类

  1. RedisDataSource 使用 JedisConnectionFactory 从 ShardedJedisPool 连接池中获取 jedis

  2. RedisClientTemplate 依赖 RedisDataSource 操作 redis 的具体模板方法

  3. RedisCacheServiceImpl 对 RedisClientTemplate 再次封装

JedisPool(非切片链接池) 和 ShardedJedisPool(切片链接池) 有什么区别

JedisPool 连一台 Redis, ShardedJedisPool 连 Redis 集群, 通过一致性哈希算法决定把数据存到哪台上, 算是一种客户端负载均衡, 所以添加是用这个(Redis 3.0 之后支持服务端负载均衡) 删除那个问题的答案就显而易见了, 总不可能随机找一个 Redis 服务端去删吧

集群 session 共享机制

现在集群中使用的 Session 共享机制有两种, 分别是 session 复制和 session 粘性.

Session 复制

该种方式下, 负载均衡器会根据各个 node 的状态, 把每个 request 进行分发, 使用这样的测试, 必须在多个 node 之间复制用户的 session, 实时保持整个集群中用户的状态同步. 其中 jboss 的实现原理是使用拦截器, 根据用户的同步策略拦截 request, 做完同步处理后再交给 server 产生响应.

优点: session 不会被绑定到具体的 node, 只要有一个 node 存活, 用户状态就不会丢失, 集群能够正常工作.

缺点: node 之间通信频繁, 响应速度有影响, 高并发情况下性能下降比较厉害.

Session 粘性

该种方式下, 当用户发出第一个 request 后, 负载均衡器动态的把该用户分配至到某个节点, 并记录该节点的 jvm 路由, 以后该用户的所有的 request 都会绑定到这个 jvm 路由, 用户只会和该 server 交互.

优点: 响应速度快, 多个节点之间无需通信

缺点: 某个 node 死掉之后, 它负责的所有用户都会丢失 session.

改进: servlet 容器在新建、更新或维护 session 时, 向其它 no de 推送修改信息. 这种方式在高并发情况下同样会影响效率.

以上这两种方式都需要负载均衡器和 Servlet 容器的支持, 在部署时需要单独配置负载均衡器和 Servelt 容器.

基于分布式缓存的 session 共享机制

将会话 Session 统一存储在分布式缓存中, 并使用 Cookie 保持客户端和服务端的联系, 每一次会话开始就生成一个 GUID 作为 SessionID, 保存在客户端的 Cookie 中, 在服务端通过 SessionID 向分布式缓存中获取 session.

实现思路: 通过一个 Filter 过滤所有的 request 请求, 在 Filter 创建 request 和 session 的代理, 通过代理使用分布式缓存对 session 进行操作. 这样实现对现有应用中对 request 对象的操作透明 扩展指定 server利用 Servlet 容器提供的插件功能, 自定义 HttpSession 的创建和管理策略, 并通过配置的方式替换掉默认的策略.

不过这种方式有个缺点, 就是需要耦合 Tomcat/Jetty 等 Servlet 容器的代码. 这方面其实早就有开源项目了, 例如 memcached-session-manager , 以及 tomcat-redis-session-manager . 暂时都只支持 Tomcat6/Tomcat7.

设计一个 Filter利用 HttpServletRequestWrapper, 实现自己的 getSession() 方法, 接管创建和管理 Session 数据的工作. spring-session 就是通过这样的思路实现的.

spring-session SpringSession 的几个关键类

  1. SessionRepositoryFilter(order是Integer.MIN_VALUE + 50)
  2. SessionRepositoryRequestWrapper 与 SessionRepositoryResponseWrapper, 通过 SessionRepository 去操纵 session
  3. SessionRepository
  4. CookieHttpSessionStrategy

redis 过期数据的删除方式

  1. 定时删除: 在设置键的过期时间的同时, 创建一个定时器 (timer), 让定时器在键的过期时间来临时, 立即执行对键的删除操作.
  2. 惰性删除: 放任键过期不管, 但是每次从键空间中获取键时, 都检查取得的键是否过期, 如果过期的话, 就删除该键;如果没有过期, 就返回该键.
  3. 定期删除: 每隔一段时间, 程序就对数据库进行一次检查, 删除里面的过期键. 至于要删除多少过期键, 以及要检查多少个数据库, 则由算法决定.

redis 实际是以懒性删除 + 定期删除这种策略组合来实现过期键删除的, 导致 Spring 需要采用及时删除的策略(定时轮询), 在过期的时候, 访问一下该 key, 然后及时触发惰性删除 Spring 的轮询如何保证时效性

@Scheduled(cron = "0 * * * * *")
//每分钟跑一次, 每次清除前一分钟的过期键
public void cleanExpiredSessions() {
    long now = system.currentTimeMillis();
    long prevMin = roundDownMinute(now);

    if (logger.isDebugEnabled()) {
        logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
    }

    String expirationKey = getExpirationKey(prevMin);
    Set < String > sessionsToExpire = expirationRedisOperations.boundSetOps(expirationKey).members();
    expirationRedisOperations.delete(expirationKey);
    for (String session: sessionsToExpire) {
        String sessionKey = getSessionKey(session);
        touch(sessionKey);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里的 touch 操作就是访问该 key, 然后触发 redis 删除.

/**
     * By trying to access the session we only trigger a deletion if it the TTL is expired. This is done to handle
     * https://github.com/spring-projects/spring-session/issues/93
     *
     * @param key
     */
    private void touch(String key) {
        sessionRedisOperations.hasKey(key);
    }
1
2
3
4
5
6
7
8
9

主动删除 session

public void onDelete(ExpiringSession session) {
    long toExpire = roundUpToNextMinute(expiresInMillis(session));
    String expireKey = getExpirationKey(toExpire);
    expirationRedisOperations.boundSetOps(expireKey).remove(session.getId());
}
1
2
3
4
5

延长 session 过期时间

public void onExpirationupdated(Long originalExpirationTimeInMilli, ExpiringSession session) {
    if (originalExpirationTimeInMilli != null) {
        long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
        String expireKey = getExpirationKey(originalRoundedUp);
        expirationRedisOperations.boundSetOps(expireKey).remove(session.getId());
    }

    long toExpire = roundUpToNextMinute(expiresInMillis(session));

    String expireKey = getExpirationKey(toExpire);
    BoundSetOperations < String,
    String > expireOperations = expirationRedisOperations.boundSetOps(expireKey);
    expireOperations.add(session.getId());

    long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds();
    String sessionKey = getSessionKey(session.getId());

    expireOperations.expire(sessionExpireInSeconds + 60, TimeUnit.SECONDS);
    sessionRedisOperations.boundHashOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

替换序列化方式

使用 GenericJackson2JsonRedisSerializer 替换 JdkSerializationRedisSerializer 使得存入 redis 的数据显示更友好

存在的问题

数据迁移 原来存在于 redis 中的数据 不能使用 GenericJackson2JsonRedisSerializer 反序列化

jedisPool 与 RedisTemplate 的区别

jedisPool 是直接通过获取 jedis 来操作 redis 而 RedisTemplate 是通过 spring 由 IOC 来配置依赖关系

Spring 提供的 redis 序列化方式的区别

  1. JdkSerializationRedisSerializer
  2. GenericJackson2JsonRedisSerializer
Last Updated: 7/3/2019, 6:17:56 PM