此文档为拉钩Java高薪训练营2期课程学习过程,完成作业的文档。顺便说一句拉钩的课程,整个课程,体系非常全,价格上也是所有培训课程中最便宜的。如果希望构建一个整体的技术视野,非常推荐。

模拟拉勾网首页热门职位的缓存设计和实现

image.png
image.png
image.png

要求

  1. BS结构:springboot或ssm都行
  2. 设计合适的数据结构用于缓存数据
  3. 采用CacheAsidePattern方式读写缓存
  4. 分布式缓存可以采用RedisCluster或Tair搭建
  5. 当分布式缓存超时时读取本地GuavaCache
  6. 本地GuavaCache高并发访问时可以防止缓存击穿

整合 Reids

引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

application.properties 配置

  1. spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=GMT%2B8
  2. spring.datasource.username=root
  3. spring.datasource.password=Test_123
  4. spring.jpa.show-sql=true
  5. spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  6. ######### thyemleaf ##############
  7. # 启用模板缓存 (默认开启,开发过程中,一般关闭,保证页面及时刷新)
  8. spring.thymeleaf.cache=false
  9. # 下面配置都可以直接采用默认配置
  10. # 模板编码
  11. spring.thymeleaf.encoding=UTF-8
  12. spring.thymeleaf.mode=HTML
  13. # 模板页面存放路径
  14. spring.thymeleaf.prefix=classpath:/templates/
  15. # 模板页面后缀
  16. spring.thymeleaf.suffix=.html
  17. ####### redis ###########
  18. spring.redis.cluster.nodes=192.168.158.124:7001,192.168.158.124:7002,192.168.158.124:7003,192.168.158.124:7004,192.168.158.124:7021,192.168.158.124:7022,192.168.158.124:7023,192.168.158.124:7024

RedisService 实现

  1. @Service
  2. public class RedisService {
  3. private final String REDIS_HOT_POSITION_KEY = "lagou:hot-positions";
  4. @Autowired
  5. // 发现有一个 redisTemplate 和一个 stringRedisTemplate
  6. @Qualifier("redisTemplate")
  7. private RedisTemplate template;
  8. /**
  9. * 添加热门职位缓存
  10. * @param positionList 热门职位
  11. */
  12. public void setHotPosition(List<HotPosition> positionList) {
  13. BoundListOperations<String, HotPosition> ops = template.boundListOps(REDIS_HOT_POSITION_KEY);
  14. for (HotPosition p : positionList) {
  15. ops.leftPush(p);
  16. }
  17. }
  18. /**
  19. * 删除热门职位缓存
  20. */
  21. public void deleteHotPosition() {
  22. template.unlink(REDIS_HOT_POSITION_KEY);
  23. }
  24. /**
  25. * 获取所有热门职位
  26. * @return 热门职位
  27. */
  28. public List<HotPosition> getHotPosition() {
  29. return template.boundListOps(REDIS_HOT_POSITION_KEY).range(0, -1);
  30. }
  31. }

整合 Guava

引入依赖

  1. <dependency>
  2. <groupId>com.google.guava</groupId>
  3. <artifactId>guava</artifactId>
  4. <version>26.0-jre</version>
  5. </dependency>

GuavaCache 配置

  1. @Configuration
  2. public class GuavaCacheConfig {
  3. @Bean
  4. public Cache<Object, Object> localCache(){
  5. return CacheBuilder.newBuilder().initialCapacity(10).maximumSize(20).build();
  6. }
  7. }

GuavaCacheService 实现

  1. @Service
  2. public class GuavaCacheService {
  3. private final String CACHE_HOT_POSITION_KEY = "lagou:hot-position";
  4. @Autowired
  5. private RedisService redisService;
  6. @Autowired
  7. private Cache<Object, Object> localCache;
  8. public void loadHotPosition() {
  9. List<HotPosition> hotPositions = redisService.getHotPosition();
  10. localCache.put(CACHE_HOT_POSITION_KEY, hotPositions);
  11. }
  12. @SuppressWarnings("unchecked")
  13. public List<HotPosition> getHotPositions() {
  14. return (List<HotPosition>)localCache.getIfPresent(CACHE_HOT_POSITION_KEY);
  15. }
  16. }

项目启动时初始化本地缓存

  1. @Component
  2. public class LocalCacheListener implements ApplicationListener<ApplicationStartedEvent> {
  3. @Autowired
  4. private GuavaCacheService guavaCacheService;
  5. @Override
  6. public void onApplicationEvent(ApplicationStartedEvent event) {
  7. guavaCacheService.loadHotPosition();
  8. System.out.println("guava 本地缓存初始化成功");
  9. }
  10. }

整合 Redisson

整合 Redisson 防止缓存击穿:在 Redis 缓存不存在时,只允许一个线程查询数据库,其他的请求查询 Guava 本地缓存。

引入依赖

  1. <!-- redis 分布式锁 -->
  2. <!-- <dependency>-->
  3. <!-- <groupId>org.redisson</groupId>-->
  4. <!-- <artifactId>redisson-spring-boot-starter</artifactId>-->
  5. <!-- <version>3.10.6</version>-->
  6. <!-- </dependency>-->
  7. <dependency>
  8. <groupId>org.redisson</groupId>
  9. <artifactId>redisson</artifactId>
  10. <version>3.11.4</version>
  11. </dependency>
  • 上面的 starter 没找到集群的配置,所以下面自己整合一下 Redis 集群

Redis 配置

  1. @Component
  2. @ConfigurationProperties(prefix = "spring.redis")
  3. public class RedisProperties {
  4. private String password;
  5. private cluster cluster;
  6. public static class cluster {
  7. private List<String> nodes;
  8. public List<String> getNodes() {
  9. return nodes;
  10. }
  11. public void setNodes(List<String> nodes) {
  12. this.nodes = nodes;
  13. }
  14. }
  15. public String getPassword() {
  16. return password;
  17. }
  18. public void setPassword(String password) {
  19. this.password = password;
  20. }
  21. public RedisProperties.cluster getCluster() {
  22. return cluster;
  23. }
  24. public void setCluster(RedisProperties.cluster cluster) {
  25. this.cluster = cluster;
  26. }
  27. }

Redisson 配置

  1. @Configuration
  2. public class RedissonConfig {
  3. @Autowired
  4. private RedisProperties redisProperties;
  5. @Bean
  6. public Redisson redisson() {
  7. //redisson版本是3.5,集群的ip前面要加上“redis://”,不然会报错,3.2版本可不加
  8. List<String> clusterNodes = new ArrayList<>();
  9. for (int i = 0; i < redisProperties.getCluster().getNodes().size(); i++) {
  10. clusterNodes.add("redis://" + redisProperties.getCluster().getNodes().get(i));
  11. }
  12. Config config = new Config();
  13. ClusterServersConfig clusterServersConfig = config.useClusterServers()
  14. .addNodeAddress(clusterNodes.toArray(new String[clusterNodes.size()]));
  15. clusterServersConfig.setPassword(redisProperties.getPassword());
  16. return (Redisson) Redisson.create(config);
  17. }
  18. }

缓存模式查询逻辑

  1. @Service
  2. public class HotPositionServiceImpl implements IHotPositionService {
  3. private final String LOCK_HOT_POSITION = "lock:hot-position";
  4. @Autowired
  5. private RedisService redisService;
  6. @Autowired
  7. private HotPositionDao hotPositionDao;
  8. @Autowired
  9. private GuavaCacheService guavaCacheService;
  10. @Autowired
  11. private Redisson redisson;
  12. /**
  13. * 首先从 Redis 缓存中查询数据,如果 Redis 缓存为空,通过分布式锁限制只允许一个线程到数据库里查询数据。
  14. * 并将查询到的数据更新到 Redis 和 Guava 缓存。
  15. * 其他的线程直接读取 Guava 缓存中的数据。
  16. *
  17. * @return 热门职位
  18. */
  19. @Override
  20. public List<HotPosition> findAllHotPosition() {
  21. List<HotPosition> hotPosition = redisService.getHotPosition();
  22. if (hotPosition != null && hotPosition.size() > 0) {
  23. System.out.println("直接从 Redis 缓存中加载数据");
  24. return hotPosition;
  25. }
  26. System.out.println("Redis 缓存数据为空 ");
  27. RLock lock = redisson.getLock(LOCK_HOT_POSITION);
  28. boolean canLock = false;
  29. try {
  30. canLock = lock.tryLock(100L, TimeUnit.MILLISECONDS);
  31. if (canLock) {
  32. // 获取锁成功
  33. // 模拟耗时操作,测试从本地 guava 缓存中读取数据
  34. Thread.sleep(1000L);
  35. System.out.println("休眠 1000L 完成");
  36. // 从数据库获取数据
  37. List<HotPosition> hotPositionList = hotPositionDao.findAll();
  38. // 更新 redis 缓存
  39. redisService.setHotPosition(hotPositionList);
  40. // 更新本地缓存
  41. guavaCacheService.loadHotPosition();
  42. System.out.println("从数据库获取数据,并写入缓存成功");
  43. return hotPositionList;
  44. }
  45. } catch (InterruptedException e) {
  46. e.printStackTrace();
  47. } finally {
  48. // 如果时当前线程锁住,需要解锁
  49. if (canLock) {
  50. lock.unlock();
  51. }
  52. }
  53. // 如果获取锁失败(超时), 直接从本地缓存取
  54. System.out.println("从本地 guava 缓存中读取数据");
  55. return guavaCacheService.getHotPositions();
  56. }
  57. }