掘金 后端 ( ) • 2024-05-06 11:44

愉快的五一过去了,迎来的是连续六天的工作时间,学习也将继续。

概述:

RMap 是 Redisson 提供的一个 Java 对象,它实现了 java.util.concurrent.ConcurrentMap 接口,是对 Redis 的哈希数据结构的封装。Redisson 是一个在 Redis 的基础上提供了许多分布式数据结构和服务的 Java 客户端库。RMap 通过使用 Redis 哈希表提供了一个分布式的 Map 实现。原理:

RMap 内部使用 Redis 的哈希(hash)数据类型来存储键值对。Redis 哈希是一个键值对集合,是存储对象属性的理想选择。RMap 提供的操作,如 putgetremove 等,都是通过 Redis 命令来实现的。

当对 RMap 执行操作时,Redisson 会将这些操作转换为 Redis 的命令,然后通过网络发送到 Redis 服务器执行。例如,当调用 RMap.put(key, value) 方法时,Redisson 会发送一个 HSET 命令到 Redis 服务器。

优点:

  1. 分布式特性: RMap 由于底层依赖于 Redis,因此天然具有分布式特性,可以在多个应用实例间共享数据。
  2. 并发支持: RMap 实现了 java.util.concurrent.ConcurrentMap 接口,提供了线程安全的操作。
  3. 高性能: Redis 作为内存数据库,提供了非常高的读写速度,RMap 因此也继承了这一特性。
  4. 数据持久化: Redis 支持数据持久化到磁盘,因此使用 RMap 存储的数据可以在系统重启后恢复。
  5. 特性丰富: RMap 支持许多高级特性,如监听器、过期键、事务操作等。

缺点:

  1. 内存限制: 由于 Redis 是内存数据库,RMap 存储的数据量受限于服务器内存大小。
  2. 网络延迟: 所有操作都需要通过网络与 Redis 服务器通信,网络延迟可能会影响性能。
  3. 成本: 对于需要大量内存来存储数据的应用,使用 Redis 可能会增加成本。
  4. 数据一致性: 如果在多个节点上部署 Redisson 客户端,且 Redis 配置为主从复制模式,可能会遇到数据一致性问题,因为 Redis 的复制是异步的。
  5. 复杂性: 对于简单应用来说,引入 Redisson 和 Redis 可能会增加系统的复杂性。

RMap 是 Redisson 中的一个接口,它提供了一个分布式且可扩展的映射(Map)。

流程图:

graph LR
    Client -- put k,v --> RMap
    RMap -- returns --> AckPut[("Acknowledgement for put operation")]

    Client -- get k --> RMap
    RMap -- returns --> Value[("Value associated with key k")]

    Client -- remove k --> RMap
    RMap -- returns --> AckRemove[("Acknowledgement for remove operation")]

    Client -- containsKey k --> RMap
    RMap -- returns --> Bool[("Boolean result for containsKey")]

    style RMap fill:#f9f,stroke:#333,stroke-width:2px
    style Client fill:#ccf,stroke:#333,stroke-width:2px
    style AckPut fill:#cfc,stroke:#333,stroke-width:2px
    style Value fill:#cfc,stroke:#333,stroke-width:2px
    style AckRemove fill:#cfc,stroke:#333,stroke-width:2px
    style Bool fill:#cfc,stroke:#333,stroke-width:2px

以下步骤:

  • 一个客户端 (Client) 使用 put(k, v) 方法向 RMap 添加一个键值对。
  • RMap 返回一个确认操作的消息 (Acknowledgement for put operation)。
  • 客户端使用 get(k) 方法从 RMap 获取与键 k 关联的值。
  • RMap 返回与键 k 关联的值 (Value associated with key k)。
  • 客户端使用 remove(k) 方法从 RMap 中删除一个键值对。
  • RMap 返回一个确认删除操作的消息 (Acknowledgement for remove operation)。
  • 客户端使用 containsKey(k) 方法检查 RMap 是否包含键 k
  • RMap 返回一个布尔结果 (Boolean result for containsKey) 表示是否包含该键。

每个操作都是由客户端发起,并且 RMap 对象对每个操作都给出了相应的响应。这个流程图可以帮助理解 RMap 提供的基本操作和客户端与之交互的方式。

时序图:

sequenceDiagram
    participant C as Client
    participant R as RMap

    C->>+R: put(key, value)
    R->>-C: Acknowledgment

    C->>+R: get(key)
    R->>-C: Return value

    C->>+R: remove(key)
    R->>-C: Acknowledgment

    C->>+R: containsKey(key)
    R->>-C: Return boolean

在这个时序图中:

  • ClientRMap 发送 put(key, value) 消息以添加一个键值对。
  • RMap 返回操作的确认(Acknowledgment)。
  • ClientRMap 发送 get(key) 消息以检索与键关联的值。
  • RMap 返回与键关联的值。
  • ClientRMap 发送 remove(key) 消息以移除一个键值对。
  • RMap 返回操作的确认。
  • ClientRMap 发送 containsKey(key) 消息以检查是否含有特定的键。
  • RMap 返回一个布尔值表示是否包含该键。

这个时序图显示了 RMap 操作的基本流程,展示了客户端与 RMap 之间的交互。在实际应用中,每个操作可能还会涉及到更多的细节,例如错误处理和异步操作的处理。

工具类:

封装 RMap 的基本功能:


import org.redisson.api.RMap;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.codec.JsonJacksonCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
 * @Author derek_smart
 * @Date 202/4/28 10:21
 * @Description RMap 工具类
 * <p>
 */
@Component
public class RedissonMapHelper<K, V> {

    private static final Logger logger = LoggerFactory.getLogger(RedissonMapHelper.class);

    private final RedissonClient redissonClient;

    private final Codec codec;

    @Autowired
    public RedissonMapHelper(RedissonClient redissonClient, Codec codec) {
        this.redissonClient = redissonClient;
        // Default to JSON codec, can be customized or set via configuration
        this.codec = codec != null ? codec : new JsonJacksonCodec();
    }

    public <K, V> V get(String mapName, K key) {
        try {
            RMap<K, V> map = redissonClient.getMap(mapName);
            return map.get(key);
        } catch (Exception e) {
            logger.error("Error getting value from map: {}", mapName, e);
            return null;
        }
    }

    public <K, V> V getForCodec(String mapName, K key) {
        try {
            RMap<K, V> map = redissonClient.getMap(mapName, codec);
            return map.get(key);
        } catch (Exception e) {
            logger.error("Error getting value from map: {}", mapName, e);
            return null;
        }
    }

    // ... (Other methods with exception handling and logging)

    public <K, V> V put(String mapName, K key, V value) {
        try {
            RMap<K, V> map = redissonClient.getMap(mapName);
            return map.put(key, value);
        } catch (Exception e) {
            logger.error("Error putting value into map: {}", mapName, e);
            return null;
        }
    }

    public V putIfAbsent(String mapName, K key, V value) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        return map.putIfAbsent(key, value);
    }

    public boolean remove(String mapName, K key) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        return map.remove(key) != null;
    }

    public boolean removeForCodec(String mapName, K key) {
        try {
            RMapCache<K, V> mapCache = redissonClient.getMapCache(mapName, codec);
            return mapCache.fastRemove(key) > 0;
        } catch (Exception e) {
            logger.error("Error removing key from map: {}", mapName, e);
            // Implement a retry mechanism
            return false;
        }
    }

    public boolean containsKey(String mapName, K key) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        return map.containsKey(key);
    }

    public long size(String mapName) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        return map.size();
    }

    public void clear(String mapName) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        map.clear();
    }

    public void delete(String mapName) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        map.delete();
    }

/*    public V putWithTTL(String mapName, K key, V value, long ttl, TimeUnit timeUnit) {
        RMap<K, V> map = redissonClient.getMap(mapName);
        V previousValue = map.put(key, value);
        map.expireKey(key, ttl, timeUnit);
        return previousValue;
    }*/

    public V putWithTTL(String mapName, K key, V value, long ttl, TimeUnit timeUnit) {
        RMapCache<K, V> mapCache = redissonClient.getMapCache(mapName);
        V previousValue = mapCache.put(key, value, ttl, timeUnit);
        return previousValue;
    }


    public <K, V> CompletableFuture<Void> putAllAsync(String mapName, Map<? extends K, ? extends V> map) {
        return (CompletableFuture<Void>) redissonClient.getMap(mapName).putAllAsync(map);
    }

    public <K, V> CompletableFuture<Map<K, V>> getAllAsync(String mapName, Iterable<? extends K> keys) {
        return (CompletableFuture<Map<K, V>>) redissonClient.getMap(mapName).getAllAsync((Set<Object>) keys);
    }

    public <K, V> void putAll(String mapName, Map<? extends K, ? extends V> map) {
        try {
            RMap<K, V> rMap = redissonClient.getMap(mapName, codec);
            rMap.putAll(map);
        } catch (Exception e) {
            logger.error("Error putting all values into map: {}", mapName, e);
        }
    }

    public <K, V> Map<K, V> getAll(String mapName, Iterable<? extends K> keys) {
        try {
            RMap<K, V> rMap = redissonClient.getMap(mapName, codec);
            return rMap.getAll((Set<K>) keys);
        } catch (Exception e) {
            logger.error("Error getting values from map: {}", mapName, e);
            return null;
        }
    }

}

image.png

工具类总结:

在这个工具类中,定义了一些基本的方法,如 getputputIfAbsentremove 等,这些方法都是通过 Redisson 客户端与 Redis 服务器通信的。还添加了一个 putWithTTL 方法,它允许为特定的键设置一个生存时间(TTL),这样键就会在经过指定的时间后自动从 Redis 中删除。 引入了异步方法,如 putAllAsyncgetAllAsync,它们返回 CompletableFuture 对象。这些方法允许非阻塞地执行操作,从而提高性能。 请注意,这个工具类是泛型的,这意味着可以用它来存储和检索任何类型的键和值,只要它们是可序列化的。

工具类功能点:

  1. 异常处理:为了提高鲁棒性,应该捕获可能发生的异常,并根据需要进行适当的处理。
  2. 泛型方法:使用泛型方法而不是在类级别定义泛型,这样可以为不同类型的键和值使用同一个实例。
  3. 配置化:允许从配置文件中读取 Redis 地址和其他参数,而不是硬编码。
  4. 序列化:确保提供了对键和值序列化策略的支持。
  5. 资源管理:确保 Redis 连接被适当管理,例如使用连接池等。
  6. 日志记录:添加日志记录以便于调试和监控。
  7. 批量操作:提供批量操作的方法,以减少网络往返次数。
  8. 异步操作:提供异步操作的方法,以提高性能。
  9. 增加批量操作方法:这些方法可以在单次调用中处理多个键和值,减少网络往返次数,从而提高性能。
  10. 定制序列化策略:Redisson 允许定制序列化和反序列化策略。可以根据需要选择不同的序列化器,例如 JSON、Avro、Kryo 等。
  11. 支持异步和同步操作:提供异步(non-blocking)和同步(blocking)版本的方法,以便用户根据具体场景选择。
  12. 优化资源使用:确保 Redis 连接和其他资源被正确管理,例如使用 Redisson 的连接池功能。
  13. 增加配置和灵活性:允许通过配置来设置默认的 map 名称、序列化策略等,使得工具类更加灵活。
  14. 增加安全性:考虑到安全性,对敏感数据进行加密。

Redission配置

Spring 配置中,需要配置 RedissonClient bean,这样它就可以被自动装配到工具类中。这里是一个简单的配置示例:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

在这个配置类中,创建了一个 RedissonClient 的 bean,它将连接到本地运行的 Redis 服务器。在实际部署时,需要根据的环境配置 Redis 服务器的地址和其他设置。

现在,可以在应用程序中注入 RedissonMapHelper 并使用它来与 Redis 中的 RMap 进行交互。

总的来说,RMap 是一个功能强大的分布式 Map 实现,适用于需要高性能、高并发和分布式特性的场景。选择使用 RMap 时,需要根据应用的具体需求权衡其优缺点。