此文档为拉钩Java高薪训练营2期课程学习过程,完成作业的文档。顺便说一句拉钩的课程,整个课程,体系非常全,价格上也是所有培训课程中最便宜的。如果希望构建一个整体的技术视野,非常推荐。
模拟拉勾网首页热门职位的缓存设计和实现
要求
- BS结构:springboot或ssm都行
- 设计合适的数据结构用于缓存数据
- 采用CacheAsidePattern方式读写缓存
- 分布式缓存可以采用RedisCluster或Tair搭建
- 当分布式缓存超时时读取本地GuavaCache
- 本地GuavaCache高并发访问时可以防止缓存击穿
整合 Reids
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties 配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=Test_123
spring.jpa.show-sql=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
######### thyemleaf ##############
# 启用模板缓存 (默认开启,开发过程中,一般关闭,保证页面及时刷新)
spring.thymeleaf.cache=false
# 下面配置都可以直接采用默认配置
# 模板编码
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML
# 模板页面存放路径
spring.thymeleaf.prefix=classpath:/templates/
# 模板页面后缀
spring.thymeleaf.suffix=.html
####### redis ###########
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 实现
@Service
public class RedisService {
private final String REDIS_HOT_POSITION_KEY = "lagou:hot-positions";
@Autowired
// 发现有一个 redisTemplate 和一个 stringRedisTemplate
@Qualifier("redisTemplate")
private RedisTemplate template;
/**
* 添加热门职位缓存
* @param positionList 热门职位
*/
public void setHotPosition(List<HotPosition> positionList) {
BoundListOperations<String, HotPosition> ops = template.boundListOps(REDIS_HOT_POSITION_KEY);
for (HotPosition p : positionList) {
ops.leftPush(p);
}
}
/**
* 删除热门职位缓存
*/
public void deleteHotPosition() {
template.unlink(REDIS_HOT_POSITION_KEY);
}
/**
* 获取所有热门职位
* @return 热门职位
*/
public List<HotPosition> getHotPosition() {
return template.boundListOps(REDIS_HOT_POSITION_KEY).range(0, -1);
}
}
整合 Guava
引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
GuavaCache 配置
@Configuration
public class GuavaCacheConfig {
@Bean
public Cache<Object, Object> localCache(){
return CacheBuilder.newBuilder().initialCapacity(10).maximumSize(20).build();
}
}
GuavaCacheService 实现
@Service
public class GuavaCacheService {
private final String CACHE_HOT_POSITION_KEY = "lagou:hot-position";
@Autowired
private RedisService redisService;
@Autowired
private Cache<Object, Object> localCache;
public void loadHotPosition() {
List<HotPosition> hotPositions = redisService.getHotPosition();
localCache.put(CACHE_HOT_POSITION_KEY, hotPositions);
}
@SuppressWarnings("unchecked")
public List<HotPosition> getHotPositions() {
return (List<HotPosition>)localCache.getIfPresent(CACHE_HOT_POSITION_KEY);
}
}
项目启动时初始化本地缓存
@Component
public class LocalCacheListener implements ApplicationListener<ApplicationStartedEvent> {
@Autowired
private GuavaCacheService guavaCacheService;
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
guavaCacheService.loadHotPosition();
System.out.println("guava 本地缓存初始化成功");
}
}
整合 Redisson
整合 Redisson 防止缓存击穿:在 Redis 缓存不存在时,只允许一个线程查询数据库,其他的请求查询 Guava 本地缓存。
引入依赖
<!-- redis 分布式锁 -->
<!-- <dependency>-->
<!-- <groupId>org.redisson</groupId>-->
<!-- <artifactId>redisson-spring-boot-starter</artifactId>-->
<!-- <version>3.10.6</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.4</version>
</dependency>
- 上面的 starter 没找到集群的配置,所以下面自己整合一下 Redis 集群
Redis 配置
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private String password;
private cluster cluster;
public static class cluster {
private List<String> nodes;
public List<String> getNodes() {
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public RedisProperties.cluster getCluster() {
return cluster;
}
public void setCluster(RedisProperties.cluster cluster) {
this.cluster = cluster;
}
}
Redisson 配置
@Configuration
public class RedissonConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public Redisson redisson() {
//redisson版本是3.5,集群的ip前面要加上“redis://”,不然会报错,3.2版本可不加
List<String> clusterNodes = new ArrayList<>();
for (int i = 0; i < redisProperties.getCluster().getNodes().size(); i++) {
clusterNodes.add("redis://" + redisProperties.getCluster().getNodes().get(i));
}
Config config = new Config();
ClusterServersConfig clusterServersConfig = config.useClusterServers()
.addNodeAddress(clusterNodes.toArray(new String[clusterNodes.size()]));
clusterServersConfig.setPassword(redisProperties.getPassword());
return (Redisson) Redisson.create(config);
}
}
缓存模式查询逻辑
@Service
public class HotPositionServiceImpl implements IHotPositionService {
private final String LOCK_HOT_POSITION = "lock:hot-position";
@Autowired
private RedisService redisService;
@Autowired
private HotPositionDao hotPositionDao;
@Autowired
private GuavaCacheService guavaCacheService;
@Autowired
private Redisson redisson;
/**
* 首先从 Redis 缓存中查询数据,如果 Redis 缓存为空,通过分布式锁限制只允许一个线程到数据库里查询数据。
* 并将查询到的数据更新到 Redis 和 Guava 缓存。
* 其他的线程直接读取 Guava 缓存中的数据。
*
* @return 热门职位
*/
@Override
public List<HotPosition> findAllHotPosition() {
List<HotPosition> hotPosition = redisService.getHotPosition();
if (hotPosition != null && hotPosition.size() > 0) {
System.out.println("直接从 Redis 缓存中加载数据");
return hotPosition;
}
System.out.println("Redis 缓存数据为空 ");
RLock lock = redisson.getLock(LOCK_HOT_POSITION);
boolean canLock = false;
try {
canLock = lock.tryLock(100L, TimeUnit.MILLISECONDS);
if (canLock) {
// 获取锁成功
// 模拟耗时操作,测试从本地 guava 缓存中读取数据
Thread.sleep(1000L);
System.out.println("休眠 1000L 完成");
// 从数据库获取数据
List<HotPosition> hotPositionList = hotPositionDao.findAll();
// 更新 redis 缓存
redisService.setHotPosition(hotPositionList);
// 更新本地缓存
guavaCacheService.loadHotPosition();
System.out.println("从数据库获取数据,并写入缓存成功");
return hotPositionList;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果时当前线程锁住,需要解锁
if (canLock) {
lock.unlock();
}
}
// 如果获取锁失败(超时), 直接从本地缓存取
System.out.println("从本地 guava 缓存中读取数据");
return guavaCacheService.getHotPositions();
}
}