Talk about the computeIfAbsent operation of redisson’s RMap.

  redis, redisson

Order

This paper mainly studies the computeIfAbsent operation of redisson’s RMap.

Example

    @Test
    public void testRMapComputeIfAbsent(){
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.99.100:6379");
        RedissonClient redisson = Redisson.create(config);
        RMap<String, CityInfo> map = redisson.getMap("anyMap");

        CityInfo bj = CityInfo.builder().name("bj").build();
        CityInfo currentObject = map.computeIfAbsent("bj", k -> bj);
        Assert.assertEquals(bj.toString(),currentObject.toString());
    }

Source code analysis

ConcurrentMap.computeIfAbsent

java/util/concurrent/ConcurrentMap.java

    /**
     * {@inheritDoc}
     *
     * @implSpec
     * The default implementation is equivalent to the following steps for this
     * {@code map}, then returning the current value or {@code null} if now
     * absent:
     *
     * <pre> {@code
     * if (map.get(key) == null) {
     *     V newValue = mappingFunction.apply(key);
     *     if (newValue != null)
     *         return map.putIfAbsent(key, newValue);
     * }
     * }</pre>
     *
     * The default implementation may retry these steps when multiple
     * threads attempt updates including potentially calling the mapping
     * function multiple times.
     *
     * <p>This implementation assumes that the ConcurrentMap cannot contain null
     * values and {@code get()} returning null unambiguously means the key is
     * absent. Implementations which support null values <strong>must</strong>
     * override this default implementation.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     * @since 1.8
     */
    @Override
    default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        Objects.requireNonNull(mappingFunction);
        V v, newValue;
        return ((v = get(key)) == null &&
                (newValue = mappingFunction.apply(key)) != null &&
                (v = putIfAbsent(key, newValue)) == null) ? newValue : v;
    }
  • ComputeIfAbsent returns a new value instead of null when the key does not exist.
  • PutIfAbsent was called in the computeIfAbsent method.

RedissonMap.putIfAbsent

redisson-3.8.1-sources.jar! /org/redisson/RedissonMap.java

    @Override
    public V putIfAbsent(K key, V value) {
        return get(putIfAbsentAsync(key, value));
    }

    @Override
    public RFuture<V> putIfAbsentAsync(final K key, final V value) {
        checkKey(key);
        checkValue(key);
        
        RFuture<V> future = putIfAbsentOperationAsync(key, value);
        if (hasNoWriter()) {
            return future;
        }
        
        MapWriterTask<V> listener = new MapWriterTask<V>() {
            @Override
            public void execute() {
                options.getWriter().write(key, value);
            }
            
            @Override
            protected boolean condition(Future<V> future) {
                return future.getNow() == null;
            }

        };
        return mapWriterFuture(future, listener);
    }

    protected boolean hasNoWriter() {
        return options == null || options.getWriter() == null;
    }

    protected RFuture<V> putIfAbsentOperationAsync(K key, V value) {
        return commandExecutor.evalWriteAsync(getName(key), codec, RedisCommands.EVAL_MAP_VALUE,
                 "if redis.call('hsetnx', KEYS[1], ARGV[1], ARGV[2]) == 1 then "
                    + "return nil "
                + "else "
                    + "return redis.call('hget', KEYS[1], ARGV[1]) "
                + "end",
                Collections.<Object>singletonList(getName(key)), encodeMapKey(key), encodeMapValue(value));
    }

    protected <M> RFuture<M> mapWriterFuture(RFuture<M> future, final MapWriterTask<M> listener) {
        if (options != null && options.getWriteMode() == WriteMode.WRITE_BEHIND) {
            future.addListener(new MapWriteBehindListener<M>(commandExecutor, listener, writeBehindCurrentThreads, writeBehindTasks, options.getWriteBehindThreads()));
            return future;
        }        

        final RPromise<M> promise = new RedissonPromise<M>();
        future.addListener(new FutureListener<M>() {
            @Override
            public void operationComplete(final Future<M> future) throws Exception {
                if (!future.isSuccess()) {
                    promise.tryFailure(future.cause());
                    return;
                }

                if (listener.condition(future)) {
                    commandExecutor.getConnectionManager().getExecutor().execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                listener.execute();
                            } catch (Exception e) {
                                promise.tryFailure(e);
                                return;
                            }
                            promise.trySuccess(future.getNow());
                        }
                    });
                } else {
                    promise.trySuccess(future.getNow());
                }
            }
        });

        return promise;
    }
  • RedissonMap covers the putIfAbsent method, which calls putIfAbsentAsync here. The method mainly calls putIfAbsentOperationAsync
  • PutIfAbsentOperationAsync uses a lua script to ensure atomicity. if the key before hsetnx does not exist and the setting is successful, nil is returned; otherwise, the existing value is found and returned.
  • PutIfAbsentAsync not only calls putiabsenttoperationasync, but also adds the writer’s logic. if the writer is set, the writer will be triggered by callback when putiabsenttoperationasync’s future is successful.
  • Writer is mainly used for external storage, such as bypass storage of a copy to a local disk. There are two modes of synchronous operation and asynchronous operation.

Summary

Redisson further encapsulates redis’ data structure operations. For example, redisson’s RMap implements Java. Util. Concurrent. Concurrent Map interface and java.util.Map interface, implements methods such as putIfAbsent, and uses lua script to ensure the atomicity of operations at the server.

doc