一招实现SpringBoot服务缓存性能翻倍

460 阅读5分钟

前言

    绝大多数系统都是读多写少的,众所周知,内存的访问速度很快,是磁盘访问速度的数十倍,如果不使用缓存,都通过数据库访问硬盘,对于双十一这样大的交易量是不可想象的。有人专门写了一篇《让 CPU 告诉你硬盘和网络到底有多慢》,将磁盘、内存、网络对数据的处理速度站在人类的角度来感知表述。

    1. 从内存中读取 1MB 的连续数据,耗时大约为 250us,换算成人类时间是 7.5天。
    1. 从 SSD 读取 1MB 的顺序数据,大约需要 1ms,换算成人类时间是 1个月。 由此可见,内存和硬盘的差距,相当于拖拉机和法拉利的差距。

为什么要用Caffeine

    在Java本地缓存神器---Caffeine(一)里面已经介绍了caffeine和其他缓存框架的性能对比,结果显示,caffeine的性能远远强于其他缓存框架。如果想要了解关于caffeine更多的知识,可以看Java本地缓存神器---Caffeine(一)Java本地缓存神器---Caffeine(二)这两篇文章。

SpringBoot整合caffeine

    SpringBoot默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager来实现缓存,ConcurrentMapCache实质是一个ConcurrentHashMap集合对象java内置,如果需要使用caffeine,那么需要引用caffeine的依赖。

1. 配置caffeine的maven依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
</dependency>

2.在SpringBoot中开启缓存

@SpringBootApplication
//开启缓存
@EnableCaching
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

3. 通过bean装配caffeine

@Configuration
public class CaffeineConfig {

    @Autowired
    BookDao bookDao;

    @Autowired
    CacheLoader cacheLoader;

    @Bean
    @Primary
    public CacheManager caffeineCache() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        Caffeine caffeine = Caffeine.newBuilder()
                //cache的初始容量值
                .initialCapacity(10)
                //maximumSize用来控制cache的最大缓存数量,maximumSize和maximumWeight不可以同时使用,
                .maximumSize(100)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .removalListener((Integer key, Object value, RemovalCause cause) ->
                        System.out.printf("时间:%d,Key:%d,value:%s,移除原因(%s)%n",System.currentTimeMillis()/1000, key,value.toString(),cause)
                )
                //使用refreshAfterWrite必须要设置cacheLoader
                .refreshAfterWrite(5, TimeUnit.SECONDS);
        cacheManager.setCaffeine(caffeine);
        //缓存加载策略,当key不存在或者key过期之类的都可以通过CacheLoader来重新获得数据
        cacheManager.setCacheLoader(cacheLoader);
        return cacheManager;
    }

    @Bean
    public CacheLoader<Object, Object> cacheLoader() {
        CacheLoader<Object, Object> cacheLoader = new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object key) throws Exception {
                System.out.println("重新从数据库加载数据:" + key);
                return bookDao.getBookById((int)key);
            }

            // 达refreshAfterWrite所指定的时候会触发这个事件方法
            @Override
            public Object reload(Object key, Object oldValue) throws Exception {
                //可以在这里处理重新加载策略,本例子,没有处理重新加载,只是返回旧值。
                return oldValue;
            }
        };
        return cacheLoader;
    }

}

4. 定义实体对象

@Getter
@Setter
@ToString
public class BookBean implements Serializable {
    private static final long serialVersionUID = -6585766340444705937L;
    private int bookId;
    private String bookName;
    private String author;
    private float price;
}

5. 定义访问数据库类

@Component
public class BookDao {
    /**
     * 模拟数据库
     */
    private static Map<Integer, BookBean> bookMap = new HashMap<>();
    @PostConstruct
    private void initDB() {
        BookBean xiyou = new BookBean();
        xiyou.setBookId(1);
        xiyou.setBookName("西游记");
        xiyou.setAuthor("吴承恩");
        xiyou.setPrice(55.5f);
        bookMap.put(1, xiyou);
        BookBean honglou = new BookBean();
        honglou.setBookId(2);
        honglou.setBookName("红楼梦");
        honglou.setAuthor("曹雪芹");
        honglou.setPrice(66.6f);
        bookMap.put(2, honglou);
    }

    public BookBean getBookById(int bookId) {
        return bookMap.get(bookId);
    }

    public BookBean update(BookBean bookBean) {
        bookMap.put(bookBean.getBookId(), bookBean);
        return bookMap.get(bookBean.getBookId());
    }

    public BookBean save(BookBean bookBean) {
        bookMap.put(bookBean.getBookId(), bookBean);
        return bookMap.get(bookBean.getBookId());
    }

    public void delete(int bookId) {
        bookMap.remove(bookId);
    }
}

6. 定义服务接口

public interface BookService {

    public BookBean getBookById(int bookId);

    public BookBean updata(BookBean bookBean);

    public BookBean save(BookBean bookBean);

    public String delete(int bookId);

}

7. 定义服务实现类

@Service
//可以在此处统一指定cacheNames的名称
@CacheConfig(cacheNames = {"book"})
public class BookServiceImpl implements BookService {

    @Autowired
    BookDao bookDao;

    /**
     * 如果缓存存在,直接读取缓存值;如果缓存不存在,则调用目标方法,并将结果放入缓存
     * value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名
     * key:缓存对象存储在Map集合中的key值,非必需,默认按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = “#p0”):使用函数第一个参数作为缓存的key值
     *
     * @param bookId
     * @return
     */
    @Cacheable(cacheNames = {"book"}, key = "#bookId")
//    @Cacheable(value = "book" ,key = "targetClass + methodName +#p0")
    @Override
    public BookBean getBookById(int bookId) {
        System.out.println("查询数据库");
        return bookDao.getBookById(bookId);
    }

    @CachePut(cacheNames = {"book"}, key = "#bookBean.bookId")
    @Override
    public BookBean updata(BookBean bookBean) {
        System.out.println("更新数据库:" + bookBean.toString());
        return bookDao.update(bookBean);
    }

    @CachePut(cacheNames = {"book"}, key = "#bookBean.bookId")//写入缓存,key为user.id,一般该注解标注在新增方法上
    @Override
    public BookBean save(BookBean bookBean) {
        System.out.println("保存至数据库:" + bookBean.toString());
        return bookDao.save(bookBean);
    }

    /**
     * CacheEvict 用来从缓存中移除相应数据
     *  allEntries=true:方法调用后清空所有cacheName为book的缓存
     *  beforeInvocation=true:方法调用前清空所有缓存
     *
     * @param bookId
     * @return
     */

    @CacheEvict(cacheNames = {"book"})
    @Override
    public String delete(int bookId) {
        bookDao.delete(bookId);
        return "成功";
    }
}

8. 定义controller

@RestController
@RequestMapping("book")
public class BookController {

    @Autowired
    BookService bookService;

    @GetMapping("get/{bookId}")
    @ResponseBody
    public BookBean getBook(@PathVariable("bookId") int bookId) {
        System.out.println("时间:"+System.currentTimeMillis()/1000+",查询book接口被调用");
        return bookService.getBookById(bookId);
    }

    @PostMapping("updata")
    @ResponseBody
    public BookBean updata(@RequestBody BookBean bookBean) {
        System.out.println("时间:"+System.currentTimeMillis()/1000+",更新book接口被调用");
        return bookService.updata(bookBean);
    }
    @PostMapping("save")
    @ResponseBody
    public BookBean save(@RequestBody BookBean bookBean) {
        System.out.println("时间:"+System.currentTimeMillis()/1000+",保存book接口被调用");
        return bookService.save(bookBean);
    }

    @PostMapping("delete/{bookId}")
    @ResponseBody
    public String delete(@PathVariable("bookId") int bookId) {
        System.out.println("时间:"+System.currentTimeMillis()/1000+",删除book接口被调用");
        return bookService.delete(bookId);
    }
}

9. 测试

1.查询测试

(1) 连续两次访问 http://localhost:8080/book/get/2


可以看到第二次访问的时候,是直接从缓存中查询的数据,并没有从数据库重新加载

(2) 超过五秒之后再访问 http://localhost:8080/book/get/2


我们能看到会重新从数据库加载数据,并且会caffeine访问时间的回收策略

(3) 测试修改对缓存的影响

源码

源码在github.com/coding-hao/…下面spring_cache项目中

欢迎大家关注我的微信公众号:CodingTao