使用通配符的Spring @ CacheEvit

nxowjjhe  于 2022-10-30  发布在  Spring
关注(0)|答案(7)|浏览(207)

有没有办法在@CacheEvict中使用通配符?
我有一个具有多租户的应用程序,有时需要从租户该高速缓存中清除所有数据,但不是系统中所有租户的缓存。
请考虑以下方法:

@Cacheable(value="users", key="T(Security).getTenant() + #user.key")
public List<User> getUsers(User user) {
    ...
}

所以,我想做一些类似的事情:

@CacheEvict(value="users", key="T(Security).getTenant() + *")
public void deleteOrganization(Organization organization) {
    ...
}

有办法吗?

rseugnpd

rseugnpd1#

答案是:没有。
而且要达到你的目的并不容易。

  1. Spring Cache注解必须简单,以便缓存提供程序易于实现。
    1.有效的缓存必须是简单的。有一个键和一个值。如果在缓存中找到键,就使用值,否则计算值并放入缓存。有效的键必须有快速和诚实的等于()散列代码().假定您缓存了许多对(按键、值)。为了提高效率,不同的键应该有不同的哈希码().然后你决定驱逐整个租户。在缓存中找到租户元素并不容易。你必须迭代所有缓存的对并丢弃属于租户的对。这是没有效率的。它不是原子的,因此它是复杂的并且需要一些同步。
    因此没有。
    但是,如果你找到了解决方案,请告诉我,因为你想要的功能真的很有用。
9gm1akwq

9gm1akwq2#

就像宇宙中99%的问题一样,答案是:这要看情况而定。如果你的缓存管理器实现了一些处理这个问题的东西,那就太好了。但是看起来情况并不是这样。
如果您使用的是SimpleCacheManager,它是Spring提供的一个基本内存缓存管理器,那么您可能使用的是Spring附带的ConcurrentMapCache。虽然不可能扩展ConcurrentMapCache来处理键中的通配符(因为该高速缓存存储是私有的,您不能访问它),但您可以将其作为自己实现的灵感。
下面是一个可能的实现(除了检查它是否工作之外,我并没有真正测试它)。这是ConcurrentMapCache的一个普通副本,对evict()方法进行了修改。不同的是,这个版本的evict()处理键时会检查它是否是一个正则表达式。在这种情况下,它会遍历存储中的所有键,并驱逐那些匹配正则表达式的键。

package com.sigraweb.cache;

import java.io.Serializable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.util.Assert;

public class RegexKeyCache implements Cache {
    private static final Object NULL_HOLDER = new NullHolder();

    private final String name;

    private final ConcurrentMap<Object, Object> store;

    private final boolean allowNullValues;

    public RegexKeyCache(String name) {
        this(name, new ConcurrentHashMap<Object, Object>(256), true);
    }

    public RegexKeyCache(String name, boolean allowNullValues) {
        this(name, new ConcurrentHashMap<Object, Object>(256), allowNullValues);
    }

    public RegexKeyCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) {
        Assert.notNull(name, "Name must not be null");
        Assert.notNull(store, "Store must not be null");
        this.name = name;
        this.store = store;
        this.allowNullValues = allowNullValues;
    }

    @Override
    public final String getName() {
        return this.name;
    }

    @Override
    public final ConcurrentMap<Object, Object> getNativeCache() {
        return this.store;
    }

    public final boolean isAllowNullValues() {
        return this.allowNullValues;
    }

    @Override
    public ValueWrapper get(Object key) {
        Object value = this.store.get(key);
        return toWrapper(value);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(Object key, Class<T> type) {
        Object value = fromStoreValue(this.store.get(key));
        if (value != null && type != null && !type.isInstance(value)) {
            throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
        }
        return (T) value;
    }

    @Override
    public void put(Object key, Object value) {
        this.store.put(key, toStoreValue(value));
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        Object existing = this.store.putIfAbsent(key, value);
        return toWrapper(existing);
    }

    @Override
    public void evict(Object key) {
        this.store.remove(key);
        if (key.toString().startsWith("regex:")) {
            String r = key.toString().replace("regex:", "");
            for (Object k : this.store.keySet()) {
                if (k.toString().matches(r)) {
                    this.store.remove(k);
                }
            }
        }
    }

    @Override
    public void clear() {
        this.store.clear();
    }

    protected Object fromStoreValue(Object storeValue) {
        if (this.allowNullValues && storeValue == NULL_HOLDER) {
            return null;
        }
        return storeValue;
    }

    protected Object toStoreValue(Object userValue) {
        if (this.allowNullValues && userValue == null) {
            return NULL_HOLDER;
        }
        return userValue;
    }

    private ValueWrapper toWrapper(Object value) {
        return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null);
    }

    @SuppressWarnings("serial")
    private static class NullHolder implements Serializable {
    }
}

我相信读者知道如何用自定义缓存实现来初始该高速缓存管理器。有很多文档向您展示了如何做到这一点。在正确配置项目后,您可以像这样正常地使用注解:

@CacheEvict(value = { "cacheName" }, key = "'regex:#tenant'+'.*'")
public myMethod(String tenant){
...
}

同样,这还远远没有经过适当的测试,但它为您提供了一种方法来完成您想要的任务。如果您正在使用另一个缓存管理器,您可以类似地扩展其缓存实现。

nzk0hqpo

nzk0hqpo3#

下面是我在Redis Cache上的工作。假设你想删除所有带有关键字前缀的Cache条目:'cache-name:object-name:parentKey'。使用键值cache-name:object-name:parentKey*呼叫方法。

import org.springframework.data.redis.core.RedisOperations;    
...
private final RedisOperations<Object, Object> redisTemplate;
...    
public void evict(Object key)
{
    redisTemplate.delete(redisTemplate.keys(key));
}

来自RedisOperations.java

/**
 * Delete given {@code keys}.
 *
 * @param keys must not be {@literal null}.
 * @return The number of keys that were removed.
 * @see <a href="http://redis.io/commands/del">Redis Documentation: DEL</a>
 */
void delete(Collection<K> keys);

/**
 * Find all keys matching the given {@code pattern}.
 *
 * @param pattern must not be {@literal null}.
 * @return
 * @see <a href="http://redis.io/commands/keys">Redis Documentation: KEYS</a>
 */
Set<K> keys(K pattern);
ojsjcaue

ojsjcaue4#

通过实现自定义CacheResolver,将承租人作为该高速缓存名称的一部分包括在内;扩展和实现SimpleCacheResolver.getCacheName
然后逐出所有密钥
@CacheEvict(value = {CacheName.CACHE1, CacheName.CACHE2}, allEntries = true)
但请注意,如果你使用redis作为后备缓存,那么spring在后台使用KEYS命令,所以解决方案将无法扩展。一旦你在redis中获得了很少的100 K密钥,KEYS将花费150 ms,redis服务器将在CPU上出现瓶颈。顽皮的spring。

6l7fqoea

6l7fqoea5#

我也遇到过类似的问题。我用那种方法解决了它。

我的配置类

@Bean
RedisTemplate redisTemplate() {
    RedisTemplate template = new RedisTemplate();
    template.setConnectionFactory(lettuceConnectionFactory());
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new RedisSerializerGzip());
    return template;
}

我的实用程序类

public class CacheService {

    final RedisTemplate redisTemplate;

    public void evictCachesByPrefix(String prefix) {
        Set<String> keys = redisTemplate.keys(prefix + "*");
        for (String key : keys) {
            redisTemplate.delete(key);
        }
    }
}

警告:KEYS是一个只应在生产环境中使用的命令,使用时应格外小心。对大型数据库执行KEYS时,可能会破坏性能。https://redis.io/commands/keys

u5rb5r59

u5rb5r596#

我想从缓存中删除所有存储的订单,我用这种方式完成了它。

@CacheEvict(value = "List<Order>", allEntries = true)

据我所知,这种方法将删除所有存储有该值的列表。因此,您可以创建另一个结构,它也可以是一种解决方案。

nzkunb0c

nzkunb0c7#

我通过在这个特例中保留AOP模式来解决这个问题。
阅读仍然是注解驱动:

@Cacheable(value = "imageCache", keyGenerator = "imageKeyGenerator", unless="#result == null")
public byte[] getImageData(int objectId, int imageType, int width, int height, boolean sizeAbsolute) { 
// ... 
}

public boolean deleteImage(int objId, int type) {
    removeFromCacheByPrefix("imageCache", ImageCacheKeyGenerator.generateKey(objId, type));
    int rc = jdbcTemplate.update(SQL_DELETE_IMAGE, new Object[] {objId,type});
    return rc > 0;
}

如您所见,deleteImage(...)没有注解,但调用removeFromCacheByPrefix(...)。这是存储库超类中的一个函数,如下所示:

protected void removeFromCacheByPrefix(String cacheName, String prefix) {
    var cache = this.cacheManager.getCache(cacheName);
    Set<String> keys = new HashSet<String>();
    cache.forEach(entry -> {
        var key = String.valueOf(entry.getKey());
        if (key.startsWith(prefix)) {
            keys.add(String.valueOf(entry.getKey()));
        }
    });
    cache.removeAll(keys);
}

这样对我来说很好!

相关问题