I user Docker to run the backend. In docker, I have 3 container which are bus-db, bus-server, bus-redis. This is my docker-compose.yaml
\`services:
db:
container_name: "bus-db"
image: postgis/postgis:17-3.5
ports:
\- "5432:5432"
volumes:
\- postgres:/var/lib/postgresql/data
\- ./init:/docker-entrypoint-initdb.d
environment:
POSTGRES_USER: ....
POSTGRES_PASSWORD: ....
POSTGRES_DB: postgres
healthcheck:
test: \["CMD", "pg_isready", "-U", "postgres"\]
interval: 10s
timeout: 5s
retries: 5
cache:
container_name: "bus-redis"
image: redis:7-alpine3.20
ports:
\- "6379:6379"
command: redis-server --appendonly yes --save 20 1 --loglevel warning --requirepass \<password\>
volumes:
\- cache:/data
restart: always
healthcheck:
test:
\["CMD", "redis-cli", "-a", "\<password\>", "ping"\]
interval: 10s
timeout: 5s
retries: 5
server:
container_name: "bus-backend"
build:
context: .
target: development
develop:
watch:
\- action: rebuild
path: .
ports:
\- "8080:8080" # running port
\- "8000:8000" # debugging port
environment:
SPRING_DATASOURCE_URL: "url"
SPRING_DATASOURCE_USERNAME: ...
SPRING_DATASOURCE_PASSWORD: ...
SPRING_JPA_HIBERNATE_DDL_AUTO: update
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
volumes:
postgres:
cache:\
** This is code setup for redis in application.properties**
# Redis configuration
spring.data.redis.host=binh-duong-bus-redis
spring.data.redis.port=6379
spring.data.redis.password=<password>
spring.cache.type=redis
Then I setup the RedisConfiguration.java file like this:
@EnableCaching
@Configuration
public class RedisConfiguration {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${spring.data.redis.password}")
private String redisPassword;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
if (redisPassword != null && !redisPassword.isEmpty()) {
config.setPassword(redisPassword);
}
return new LettuceConnectionFactory(config);
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
I used Cachable annotation in RouteServiceImpl.java like this:
@Service
public class RouteServiceImpl implements RouteService {
private final RouteRepository routeRepository;
private final StopRepository stopRepository;
private final TimeLineRepository timeLineRepository;
private final RouteMapper routeMapper;
private final StopMapper stopMapper;
private final TimelineMapper timelineMapper;
public RouteServiceImpl(RouteRepository routeRepository, StopRepository stopRepository,
TimeLineRepository timeLineRepository, RouteMapper routeMapper, StopMapper stopMapper,
TimelineMapper timelineMapper) {
this.routeRepository = routeRepository;
this.stopRepository = stopRepository;
this.timeLineRepository = timeLineRepository;
this.routeMapper = routeMapper;
this.stopMapper = stopMapper;
this.timelineMapper = timelineMapper;
}
@Override
@Cacheable(value = "routes", key = "'all_routes'")
public List<RouteOnlyDto> getAllRoutesData() {
System.out.println("CACHE MISS: Loading from database...");
List<RouteModel> routes = routeRepository.findAll();
if (routes.isEmpty()) {
throw new RouteNotFound("Không có tuyến xe nào trong hệ thống");
}
return routeMapper.toRouteOnlyDtoList(routes);
}
@Override
public ResponseEntity<ApiResponse<List<RouteOnlyDto>>> getAllRoutes() {
List<RouteOnlyDto> routeDtos = getAllRoutesData();
return ResponseEntity.ok(new ApiResponse<>("Danh sách tuyến đường", routeDtos));
}
@Override
@Cacheable(value = "route", key = "#id")
public RouteDto getRouteByIdData(UUID id) {
return routeRepository.findByIdWithAllRelations(id)
.map(routeMapper::toRouteDto)
.orElseThrow(() -> new RouteNotFound("Tuyến đường với ID " + id + " không tồn tại"));
}
@Override
public ResponseEntity<ApiResponse<RouteDto>> getRouteById(UUID id) {
RouteDto routeDto = getRouteByIdData(id);
return ResponseEntity.ok(new ApiResponse<>("Thông tin tuyến đường", routeDto));
}
@Override
@CachePut(value = "route", key = "#result.body.data.id")
public ResponseEntity<?> addRoute(RouteModel route) {
RouteModel savedRoute = routeRepository.save(route);
RouteDto responseDto = routeMapper.toRouteDto(savedRoute);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new ApiResponse<>("Tuyến đường đã được thêm thành công!", responseDto));
}
@Transactional
@Override
@CachePut(value = "route", key = "#id")
@CacheEvict(value = "routes", allEntries = true)
public ResponseEntity<?> updateRoute(UUID id, RouteModel updatedRoute) {
RouteModel existingRoute = routeRepository.findById(id)
.orElseThrow(() -> new RouteNotFound("Tuyến đường với ID " + id + " không tồn tại"));
existingRoute.setRouteNumber(updatedRoute.getRouteNumber());
existingRoute.setRouteName(updatedRoute.getRouteName());
RouteModel savedRoute = routeRepository.save(existingRoute);
RouteDto responseDto = routeMapper.toRouteDto(savedRoute);
return ResponseEntity.ok(new ApiResponse<>("Tuyến đường đã được cập nhật thành công!", responseDto));
}
@Transactional
@Override
@CacheEvict(value = { "routes", "route", "routesByName", "stops", "timelines", "returnRoute" }, allEntries = true)
public ResponseEntity<?> deleteRoute(UUID id) {
RouteModel existingRoute = routeRepository.findById(id)
.orElseThrow(() -> new RouteNotFound("Tuyến đường với ID " + id + " không tồn tại"));
stopRepository.deleteByRouteId(id);
timeLineRepository.deleteByRouteId(id);
routeRepository.delete(existingRoute);
return ResponseEntity.ok(new ApiResponse<>("Tuyến đường đã bị xóa thành công!", null));
}
@Override
@Cacheable(value = "stops", key = "#routeId + #direction")
public ResponseEntity<ApiResponse<List<StopDto>>> getRouteStops(UUID routeId, String direction) {
List<StopModel> stops = stopRepository.findByRouteId(routeId);
List<StopDto> stopDtos = stopMapper.toStopDtoList(stops);
return ResponseEntity.ok(new ApiResponse<>("Danh sách điểm dừng", stopDtos));
}
@Override
@Cacheable(value = "timelines", key = "#routeId + #direction")
public ResponseEntity<ApiResponse<List<TimelineDto>>> getRouteTimelines(UUID routeId, String direction) {
List<TimeLineModel> timeLines = timeLineRepository.findByRouteId(routeId);
List<TimelineDto> timeLineDtos = timelineMapper.toTimelineDtoList(timeLines);
return ResponseEntity.ok(new ApiResponse<>("Danh sách thời gian", timeLineDtos));
}
@Override
@Cacheable(value = "routesByName", key = "#routeName")
public ResponseEntity<ApiResponse<List<RouteDto>>> getRouteByRouteName(String routeName) {
List<RouteModel> routes = routeRepository.findByRouteNameWithAllRelations(routeName);
List<RouteDto> routeDtos = routeMapper.toRouteDtoList(routes);
return ResponseEntity.ok(new ApiResponse<>("Danh sách tuyến đường", routeDtos));
}
@Override
@Cacheable(value = "returnRoute", key = "#routeId")
public ResponseEntity<ApiResponse<RouteDto>> getReturnRoute(UUID routeId) {
List<RouteModel> matchingRoutes = routeRepository
.findByRouteNumber(routeRepository.findById(routeId).get().getRouteNumber());
RouteModel returnRoute = matchingRoutes.stream().filter(route -> !route.getId().equals(routeId)).findFirst()
.orElseThrow(() -> new RouteNotFound("Không tìm thấy tuyến lượt về"));
RouteDto returnRouteDto = routeMapper.toRouteDto(returnRoute);
return ResponseEntity.ok(new ApiResponse<>("Thông tin tuyến lượt về", returnRouteDto));
}
}
When I run api ex:http://localhost:8080/api/routes. The first time, the database will be retrieved from database (cost 4 second). But when I call that api again, it still retrieved from database but the time query has been faster. When I check redis-cli keys *. It returns empty array.
**Note: I already add @EnableCaching in the main file
Please, help me find the way to sold this prob!!! Thanks
I user Docker to run the backend. In docker, I have 3 container which are bus-db, bus-server, bus-redis. This is my docker-compose.yaml
\`services:
db:
container_name: "bus-db"
image: postgis/postgis:17-3.5
ports:
\- "5432:5432"
volumes:
\- postgres:/var/lib/postgresql/data
\- ./init:/docker-entrypoint-initdb.d
environment:
POSTGRES_USER: ....
POSTGRES_PASSWORD: ....
POSTGRES_DB: postgres
healthcheck:
test: \["CMD", "pg_isready", "-U", "postgres"\]
interval: 10s
timeout: 5s
retries: 5
cache:
container_name: "bus-redis"
image: redis:7-alpine3.20
ports:
\- "6379:6379"
command: redis-server --appendonly yes --save 20 1 --loglevel warning --requirepass \<password\>
volumes:
\- cache:/data
restart: always
healthcheck:
test:
\["CMD", "redis-cli", "-a", "\<password\>", "ping"\]
interval: 10s
timeout: 5s
retries: 5
server:
container_name: "bus-backend"
build:
context: .
target: development
develop:
watch:
\- action: rebuild
path: .
ports:
\- "8080:8080" # running port
\- "8000:8000" # debugging port
environment:
SPRING_DATASOURCE_URL: "url"
SPRING_DATASOURCE_USERNAME: ...
SPRING_DATASOURCE_PASSWORD: ...
SPRING_JPA_HIBERNATE_DDL_AUTO: update
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
volumes:
postgres:
cache:\
** This is code setup for redis in application.properties**
# Redis configuration
spring.data.redis.host=binh-duong-bus-redis
spring.data.redis.port=6379
spring.data.redis.password=<password>
spring.cache.type=redis
Then I setup the RedisConfiguration.java file like this:
@EnableCaching
@Configuration
public class RedisConfiguration {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${spring.data.redis.password}")
private String redisPassword;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
if (redisPassword != null && !redisPassword.isEmpty()) {
config.setPassword(redisPassword);
}
return new LettuceConnectionFactory(config);
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
I used Cachable annotation in RouteServiceImpl.java like this:
@Service
public class RouteServiceImpl implements RouteService {
private final RouteRepository routeRepository;
private final StopRepository stopRepository;
private final TimeLineRepository timeLineRepository;
private final RouteMapper routeMapper;
private final StopMapper stopMapper;
private final TimelineMapper timelineMapper;
public RouteServiceImpl(RouteRepository routeRepository, StopRepository stopRepository,
TimeLineRepository timeLineRepository, RouteMapper routeMapper, StopMapper stopMapper,
TimelineMapper timelineMapper) {
this.routeRepository = routeRepository;
this.stopRepository = stopRepository;
this.timeLineRepository = timeLineRepository;
this.routeMapper = routeMapper;
this.stopMapper = stopMapper;
this.timelineMapper = timelineMapper;
}
@Override
@Cacheable(value = "routes", key = "'all_routes'")
public List<RouteOnlyDto> getAllRoutesData() {
System.out.println("CACHE MISS: Loading from database...");
List<RouteModel> routes = routeRepository.findAll();
if (routes.isEmpty()) {
throw new RouteNotFound("Không có tuyến xe nào trong hệ thống");
}
return routeMapper.toRouteOnlyDtoList(routes);
}
@Override
public ResponseEntity<ApiResponse<List<RouteOnlyDto>>> getAllRoutes() {
List<RouteOnlyDto> routeDtos = getAllRoutesData();
return ResponseEntity.ok(new ApiResponse<>("Danh sách tuyến đường", routeDtos));
}
@Override
@Cacheable(value = "route", key = "#id")
public RouteDto getRouteByIdData(UUID id) {
return routeRepository.findByIdWithAllRelations(id)
.map(routeMapper::toRouteDto)
.orElseThrow(() -> new RouteNotFound("Tuyến đường với ID " + id + " không tồn tại"));
}
@Override
public ResponseEntity<ApiResponse<RouteDto>> getRouteById(UUID id) {
RouteDto routeDto = getRouteByIdData(id);
return ResponseEntity.ok(new ApiResponse<>("Thông tin tuyến đường", routeDto));
}
@Override
@CachePut(value = "route", key = "#result.body.data.id")
public ResponseEntity<?> addRoute(RouteModel route) {
RouteModel savedRoute = routeRepository.save(route);
RouteDto responseDto = routeMapper.toRouteDto(savedRoute);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new ApiResponse<>("Tuyến đường đã được thêm thành công!", responseDto));
}
@Transactional
@Override
@CachePut(value = "route", key = "#id")
@CacheEvict(value = "routes", allEntries = true)
public ResponseEntity<?> updateRoute(UUID id, RouteModel updatedRoute) {
RouteModel existingRoute = routeRepository.findById(id)
.orElseThrow(() -> new RouteNotFound("Tuyến đường với ID " + id + " không tồn tại"));
existingRoute.setRouteNumber(updatedRoute.getRouteNumber());
existingRoute.setRouteName(updatedRoute.getRouteName());
RouteModel savedRoute = routeRepository.save(existingRoute);
RouteDto responseDto = routeMapper.toRouteDto(savedRoute);
return ResponseEntity.ok(new ApiResponse<>("Tuyến đường đã được cập nhật thành công!", responseDto));
}
@Transactional
@Override
@CacheEvict(value = { "routes", "route", "routesByName", "stops", "timelines", "returnRoute" }, allEntries = true)
public ResponseEntity<?> deleteRoute(UUID id) {
RouteModel existingRoute = routeRepository.findById(id)
.orElseThrow(() -> new RouteNotFound("Tuyến đường với ID " + id + " không tồn tại"));
stopRepository.deleteByRouteId(id);
timeLineRepository.deleteByRouteId(id);
routeRepository.delete(existingRoute);
return ResponseEntity.ok(new ApiResponse<>("Tuyến đường đã bị xóa thành công!", null));
}
@Override
@Cacheable(value = "stops", key = "#routeId + #direction")
public ResponseEntity<ApiResponse<List<StopDto>>> getRouteStops(UUID routeId, String direction) {
List<StopModel> stops = stopRepository.findByRouteId(routeId);
List<StopDto> stopDtos = stopMapper.toStopDtoList(stops);
return ResponseEntity.ok(new ApiResponse<>("Danh sách điểm dừng", stopDtos));
}
@Override
@Cacheable(value = "timelines", key = "#routeId + #direction")
public ResponseEntity<ApiResponse<List<TimelineDto>>> getRouteTimelines(UUID routeId, String direction) {
List<TimeLineModel> timeLines = timeLineRepository.findByRouteId(routeId);
List<TimelineDto> timeLineDtos = timelineMapper.toTimelineDtoList(timeLines);
return ResponseEntity.ok(new ApiResponse<>("Danh sách thời gian", timeLineDtos));
}
@Override
@Cacheable(value = "routesByName", key = "#routeName")
public ResponseEntity<ApiResponse<List<RouteDto>>> getRouteByRouteName(String routeName) {
List<RouteModel> routes = routeRepository.findByRouteNameWithAllRelations(routeName);
List<RouteDto> routeDtos = routeMapper.toRouteDtoList(routes);
return ResponseEntity.ok(new ApiResponse<>("Danh sách tuyến đường", routeDtos));
}
@Override
@Cacheable(value = "returnRoute", key = "#routeId")
public ResponseEntity<ApiResponse<RouteDto>> getReturnRoute(UUID routeId) {
List<RouteModel> matchingRoutes = routeRepository
.findByRouteNumber(routeRepository.findById(routeId).get().getRouteNumber());
RouteModel returnRoute = matchingRoutes.stream().filter(route -> !route.getId().equals(routeId)).findFirst()
.orElseThrow(() -> new RouteNotFound("Không tìm thấy tuyến lượt về"));
RouteDto returnRouteDto = routeMapper.toRouteDto(returnRoute);
return ResponseEntity.ok(new ApiResponse<>("Thông tin tuyến lượt về", returnRouteDto));
}
}
When I run api ex:http://localhost:8080/api/routes. The first time, the database will be retrieved from database (cost 4 second). But when I call that api again, it still retrieved from database but the time query has been faster. When I check redis-cli keys *. It returns empty array.
**Note: I already add @EnableCaching in the main file
Please, help me find the way to sold this prob!!! Thanks
Share Improve this question asked Mar 18 at 7:00 ormorm 1 1 |1 Answer
Reset to default 0In application.properties
, you set spring.data.redis.host=binh-duong-bus-redis
, but in your docker-compose.yaml, the Redis service is named cache with container name bus-redis
. Spring Boot can't resolve binh-duong-bus-redis
.
Revise application.properties
like shown below
spring.data.redis.host=bus-redis
Or add a network alias as binh-duong-bus-redis
in docker-compose.yaml
under the cache
service
networks:
default:
aliases:
- binh-duong-bus-redis
I hope it can help you fix the issue.
@Cacheable
is useless. YOu are calling it from within the class that won't work. Caching, or rather AOP, will only work for external method calls (calls to an external class). – M. Deinum Commented Mar 18 at 7:26