Nearby Shops
Redis GEO data structures enable efficient nearby shop queries. The key pattern uses a prefix combined with shop type ID, with each GEO entry storing a shop identifier and its geographic coordinates.
Data Import
A unit test populates Redis with shop location data from MySQL:
@Test
void loadShopData() {
// Fetch all shops from database
List<Shop> shopList = shopService.list();
// Group shops by type for batch processing
Map<Long, List<Shop>> groupedShops = shopList.stream()
.collect(Collectors.groupingBy(Shop::getTypeId));
// Process each group
for (Map.Entry<Long, List<Shop>> group : groupedShops.entrySet()) {
Long typeId = group.getKey();
String geoKey = "shop:geo:" + typeId;
List<Shop> shopsByType = group.getValue();
// Prepare geo locations for batch insert
List<RedisGeoCommands.GeoLocation<String>> locations =
new ArrayList<>(shopsByType.size());
for (Shop shop : shopsByType) {
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getLongitude(), shop.getLatitude())
));
}
stringRedisTemplate.opsForGeo().add(geoKey, locations);
}
}
Feature Implemantation
SpringDataRedis 2.3.9 lacks GEOSEARCH support from Redis 6.2, requiring version upgrades:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-data-redis</artifactId>
<groupId>org.springframework.data</groupId>
</exclusion>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.6.RELEASE</version>
</dependency>
Controller endpoint:
@GetMapping("/of/type")
public Result queryShopByType(
@RequestParam("typeId") Integer typeId,
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam(value = "x", required = false) Double x,
@RequestParam(value = "y", required = false) Double y
) {
return shopService.queryShopByType(typeId, current, x, y);
}
Service implementation:
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
// No coordinates provided - query database directly
if (x == null || y == null) {
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
return Result.ok(page.getRecords());
}
// Calculate pagination boundaries
int fromIndex = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int toIndex = current * SystemConstants.DEFAULT_PAGE_SIZE;
// Query Redis for shops within range, sorted by distance
String geoKey = "shop:geo:" + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
stringRedisTemplate.opsForGeo().search(
geoKey,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
.includeDistance().limit(toIndex)
);
// Handle empty results
if (results == null)
return Result.ok(Collections.emptyList());
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> geoResults = results.getContent();
if (geoResults.size() < fromIndex)
return Result.ok(Collections.emptyList());
// Extract shop IDs and distances
ArrayList<Object> shopIds = new ArrayList<>(geoResults.size());
Map<String, Distance> distanceMap = new HashMap<>(geoResults.size());
geoResults.stream().skip(fromIndex).forEach(result -> {
String shopIdStr = result.getContent().getName();
shopIds.add(Long.valueOf(shopIdStr));
distanceMap.put(shopIdStr, result.getDistance());
});
// Fetch shop details and attach distance info
String idString = StrUtil.join(",", shopIds);
List<Shop> shops = query()
.in("id", shopIds)
.last("ORDER BY FIELD(id," + idString + ")")
.list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
return Result.ok(shops);
}
User Check-in
Check-in Operation
Controller:
@PostMapping("/sign")
public Result sign() {
return userService.sign();
}
Service:
@Override
public Result sign() {
Long userId = UserHolder.getUser().getId();
LocalDateTime now = LocalDateTime.now();
// Key format: sign:userId:yyyyMM
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String signKey = "sign:" + userId + keySuffix;
// Day offset (0-indexed for SETBIT)
int dayOffset = now.getDayOfMonth() - 1;
stringRedisTemplate.opsForValue().setBit(signKey, dayOffset, true);
return Result.ok();
}
Conseuctive Check-in Statistics
Controller:
@GetMapping("/sign/count")
public Result signCount() {
return userService.signCount();
}
Service:
@Override
public Result signCount() {
Long userId = UserHolder.getUser().getId();
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String signKey = "sign:" + userId + keySuffix;
int dayOfMonth = now.getDayOfMonth();
// Retrieve bitfield for the current month
List<Long> bitfieldResult = stringRedisTemplate.opsForValue().bitField(
signKey,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
.valueAt(0)
);
if (bitfieldResult == null || bitfieldResult.isEmpty())
return Result.ok(0);
Long bitmap = bitfieldResult.get(0);
if (bitmap == null || bitmap == 0)
return Result.ok(0);
// Count consecutive 1s from the right (today's position)
int consecutiveDays = 0;
while ((bitmap & 1) != 0) {
bitmap >>= 1;
consecutiveDays++;
}
return Result.ok(consecutiveDays);
}
UV Statistics
HyperLogLog provides approximate unique visitor counting with minimal memory usage. The following test inserts one million entries:
@Test
void testHyperLogLog() {
String[] userBatch = new String[1000];
int position = 0;
for (int i = 1; i <= 1000000; i++) {
userBatch[position++] = "user_" + i;
// Flush batch every 1000 entries
if (i % 1000 == 0) {
position = 0;
stringRedisTemplate.opsForHyperLogLog().add("hll1", userBatch);
}
}
Long estimatedCount = stringRedisTemplate.opsForHyperLogLog().size("hll1");
System.out.println("estimatedCount =" + estimatedCount);
}
Result: With one million entries, the estimated count was 997,593, yielding an error rate of approximately 0.24% — negligible for most analytical purposes.