构建高效且可伸缩的结果缓存

几乎所有的服务器应用程序都会使用某种形式的缓存。重用之前的结果能降低延迟,提高吞吐量,但却需要消耗更多的内存。像许多“重复发明的轮子”一样,缓存看上去都非常简单。然而,简单的缓存可能会将性能瓶颈转变成可伸缩性瓶颈,即使缓存是用于提升单线程的性能。我们将开发一个高效且可伸缩的缓存,用于改进一个高计算开销的函数,我们首先从简单的HashMap开始,然后分析它的并发性缺点,并讨论如何修复他们。

案例1 使用HashMap和同步机制开初始化缓存

public interface computable<A, V> {
	V compute(A arg) throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String, BigInteger>{

	@Override
	public BigInteger compute(String arg) throws InterruptedException {
		//在经过长时间的计算后
		return new BigInteger(arg);
	}
}
public class Memorizer1<A, V> implements Computable<A, V>{
    private final Computable<A, V> compute;
    private final Map<A, V> cache;
    public Memorizer1(Computable<A, V> compute){
        this.compute = compute;
        cache = new HashMap<A, V>();
    }
    @Override
    public synchronized V compute(A a) throws InterruptedException {
        V result = cache.get(a);
        if(result == null){
            result = compute.compute(a);
            cache.put(a, result);
        }
        return result;
    }
}

如图所示可以发现HashMap不是一个线程安全的,因此要确保两个线程不会同时访问HashMap,Memoizer1采用了一种保守的方法,即对整个compute方法进行同步。这种方法虽然可以确保线程安全性,但会带来明显的可伸缩性问题:每次只有一个线程能够执行compute。如果另一个线程正在计算结果,那么其他调用compute的线程可能被阻塞很长时间。

案例2 用ConcurrentHashMap替换HashMap

public class Memorizer2<A, V> implements Computable<A, V>{
    private final Computable<A, V> compute;
    private final Map<A, V> cache;
    public Memorizer2(Computable<A, V> compute){
        this.compute = compute;
        cache = new ConcurrentHashMap<A, V>();
    }
    @Override
    public V compute(A a) throws InterruptedException {
        V result = cache.get(a);
        if(result == null){
            result = compute.compute(a);
            cache.put(a, result);
        }
        return result;
    }
}

如上程序中Memorizer2用ConcurrentHashMap替代HashMap来改进Memoizer1中糟糕的并发行为。由于ConcurrentHashMap是线程安全的,因此在访问底层Map时就不需要进行同步,因而避免了在对Memoizer1中的compute方法进行同步时带来的串行性。然而,Memoizer2比Memoizer1有着更好的并发行为:多线程可以同时并发地使用它。但它在作为缓存时仍然存在一些不足——当两个线程同时调用compute时存在一个漏洞,可能会导致计算得到相同的值。在使用memoization的情况下,这只会带来低效,因为缓存的作用是避免相同的数据被计算多次。

案例3 基于FutureTask的Memoizing封装器

public class Memorizer3<A, V> implements Computable<A, V>{
    private final Computable<A, V> compute;
    private final Map<A, FutureTask<V>> cache;
    public Memorizer3(Computable<A, V> compute){
        this.compute = compute;
        cache = new ConcurrentHashMap<A, FutureTask<V>>();
    }
    @Override
    public V compute(A a) throws InterruptedException {
        V f = cache.get(a);
        if(f == null){
            Callable<V> eval = new Callable<V>(){
                public V call() throw InterruptedException{
                    return c.compute(arg);
                }
            }
            FutureTask<V> ft = new FutureTask<V>(eval);
            f = ft;
            cache.put(a, ft);
            ft.run();
        }
        try{
            return f.get();
        }cache(ExecutionException e){
            throw launderThrowable(e.getCause());
        }
    }
}

程序中的Memoizer3将用于缓存键的Map重新定义为ConcurrentHashMap<A, Future>,替换原来的ConcurrentHash<A,V>。Memoizer3首先检查某个相应的计算是否已经开始(Memoizer2与之相反,它首先判断某个计算是否已经完成)。如果好没有启动就创建一个FutureTask,并注册到Map中,然后启动计算:如果已经启动,那么等待先有计算的结果。可以说,Memoizer3的实现已基本完美,它可以表现出较好的并发性,若结果已经计算出来,那么将立即返回。如果其他线程正在计算该结果,那么新到的线程将一直等待这个结果被计算出来。然而它有一个缺陷,即仍然可能会存在两个线程计算出相同值得漏铜。这个漏洞的发生概率要远小于Memoizer2中发生的概率,但由于compute方法中的if代码块仍然是非原子的“先检查在在执行”操作,因此两个线程仍然有可能在同一时间内调用compute来计算相同的值。

案例4 Memoizer的最终实现

public class Memorizer4<A, V> implements Computable<A, V>{
    private final Computable<A, V> compute;
    private final Map<A, FutureTask<V>> cache;
    public Memorizer4(Computable<A, V> compute){
        this.compute = compute;
        cache = new ConcurrentHashMap<A, FutureTask<V>>();
    }
    @Override
    public V compute(A a) throws InterruptedException {
        while(true){
            V f = cache.get(a);
            if(f == null){
                Callable<V> eval = new Callable<V>(){
                    public V call() throw InterruptedException{
                        return c.compute(arg);
                    }
                }
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(a, ft);
                if(f == null){
                    f = ft;
                    ft.run();
                }
            }
            try{
                return f.get();
            }catch(CancellationException e){
                cache.remove(arg, f);
            }catch(ExecutionException e){
                throw launderThrowable(e.getCause());
            }
        }
    }
}

在最后的案例中我们发现,Memorizer3中往缓存中设置值得操作是在Map底层上执行的复合操作(“若没有则添加”),相比于Memorizer3这里所做的改进是使用了ConcurrentMap中的原子方法putIfAbsent预先对缓存值进行了判断从而避免了Memoizer3中出现的漏洞,即存在两个线程计算出相同值得情况。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Boot集成Redis可以使用Spring Data Redis。Spring Data Redis是Spring Data的一部分,它提供了对Redis的支持,包括对Redis的连接、操作、数据序列化等。 以下是使用Spring Boot集成Redis缓存的步骤: 1. 添加Spring Data Redis和Redis客户端依赖 在Maven中添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> ``` 2. 配置Redis连接信息 在application.properties中添加以下配置: ```properties spring.redis.host=localhost spring.redis.port=6379 ``` 3. 配置Redis缓存管理器 在Java配置类中添加以下代码: ```java @Configuration @EnableCaching public class CacheConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { return new JedisConnectionFactory(); } @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } @Bean public CacheManager cacheManager() { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate()); cacheManager.setDefaultExpiration(600); return cacheManager; } } ``` 4. 在需要缓存的方法上添加@Cacheable注解 例如: ```java @Service public class UserService { @Autowired private UserRepository userRepository; @Cacheable(value = "userCache", key = "#id") public User getUserById(Long id) { Optional<User> optionalUser = userRepository.findById(id); return optionalUser.orElse(null); } } ``` @Cacheable注解会将方法的返回值缓存到Redis中,value属性指定缓存的名称,key属性指定缓存的键。 以上就是使用Spring Boot集成Redis缓存的步骤。注意,这里使用了默认的Redis连接工厂和Redis模板,如果需要更多的定制化配置,可以参考Spring Data Redis文档进行设置。 ### 回答2: Spring Boot 是一个用于快速构建 Java 应用程序的开源框架。它简化了基于 Spring 框架的应用程序的创建和配置过程。而 Redis 是一种快速且高效的内存数据库,用于存储和检索数据。 在 Spring Boot 中使用 Redis 缓存可以提高应用程序的性能和响应速度。要使用 Redis 缓存,首先需要在项目的依赖中添加 Redis 相关的依赖项。然后,在应用程序的配置文件中配置 Redis 的连接信息,包括主机名、端口号、密码等。 一旦配置完成,就可以在应用程序中使用 @Cacheable 注解将方法标记为可缓存的。当调用被标记为缓存的方法时,Spring Boot 会首先检查缓存中是否已经存在该数据,如果存在则直接返回缓存中的数据,否则执行方法并将结果存入缓存。可以使用 @CacheEvict 注解来清除缓存中的数据,以便在数据发生变化时及时更新缓存。 使用 Redis 缓存还可以有其他一些高级特性,例如设置缓存的过期时间、使用不同的缓存键生成策略等。还可以通过配置 Redis 集群实现高可用和负载均衡。 总而言之,Spring Boot 提供了简单而强大的工具来集成 Redis 缓存,通过使用 Redis 缓存可以提高应用程序的性能和可伸缩性,减轻后端数据库的负载,从而提供更好的用户体验。 ### 回答3: Spring Boot使用Redis作为缓存的步骤如下: 1. 导入Redis依赖:在pom.xml文件中添加Spring Boot对Redis的依赖。 2. 配置Redis连接信息:在application.properties或application.yml文件中配置Redis的连接信息,包括主机名、端口号、密码等。 3. 创建RedisTemplate Bean:在Spring Boot的配置类中创建RedisTemplate Bean,用于操作Redis数据库。 4. 使用RedisTemplate进行缓存操作:在需要使用缓存的方法上添加注解@EnableCaching,然后在方法执行时,使用RedisTemplate进行缓存的读取和写入操作。 5. 添加缓存注解:在需要进行缓存的方法上添加注解@Cacheable,用于标记此方法的结果需要被缓存。可以通过设置缓存的key,来定制不同参数下的缓存策略。 6. 清除缓存:在需要清除缓存的方法上添加注解@CacheEvict,用于标记此方法执行后需要清除缓存。 通过以上步骤,Spring Boot就可以和Redis进行连接,并使用Redis作为缓存来提高应用程序的性能。在缓存读取时,先从Redis中获取数据,如果Redis中不存在,则从数据库中读取,然后将读取到的数据写入Redis中;在缓存写入时,先将数据写入Redis中,再同步写入数据库。这样可以大大提高读取数据的速度,并减轻数据库的压力。同时,Spring Boot提供了灵活的缓存策略配置,可以根据业务需求来定制缓存的命中规则和过期时间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值