07 典型业务场景
目录
🎯 学习要点
验证码登录与频控 点赞去重与关注关系 排行榜与分页 签到与活跃度 UV 统计与近似去重 地理搜索与围栏 会话与 Token 管理 幂等与去重
📖 名词解释
位图(Bitmap):用位记录布尔状态,适合签到这类“是否发生”的标记。UV:独立访客数,指一天内来访的不同用户数量。HyperLogLog:近似去重数据结构,用极小空间估算不同元素数量。Geo:地理空间索引,用经纬度进行附近搜索与距离计算。Token:登录后的凭证,用于标识会话身份并控制访问。幂等:同一操作重复执行结果一致,避免重复下单或重复扣费。
🧭 学习方案
为每个场景写一个最小可运行的例子:验证码、点赞、排行榜、签到、UV、附近、会话、幂等。 在示例中加入边界条件:例如重复点赞、重复验证码验证、分页越界。 将数据定期同步到数据库或日志,形成持久化与可审计的闭环。 汇总场景的键命名与 TTL 规范,避免键冲突与过期不一致。
🗺️ 应用场景说明
验证码登录:像临时条形码,过期自动失效,防止“旧码”使用。 点赞:像投票箱,Set 保证一个人投一次,统计总票数。 排行榜:像成绩单,分数越高名次越靠前,取前 N 名展示。 签到:像日历打勾,用位图记录每天是否完成任务。 UV:像门禁计数器,近似统计当天来过的人数,节省空间。 附近搜索:像地图导航,按经纬度找离我最近的店。 会话:像临时通行证,2 小时有效,可续期或强制作废。 幂等:像一次性票据,用过一次就不能再用,防重复提交。
⚠️ 注意事项
频控与黑名单策略 键命名与生命周期 幂等性保证与重复提交
🔐 验证码登录
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
import java. time. Duration ;
@Service
public class SmsLogin {
@Autowired
RedisTemplate < String , String > tpl;
public void sendCode ( String phone, String code) {
tpl. opsForValue ( ) . set ( "sms:" + phone, code, Duration . ofMinutes ( 5 ) ) ;
}
public boolean verify ( String phone, String code) {
String v = tpl. opsForValue ( ) . get ( "sms:" + phone) ;
return code. equals ( v) ;
}
public boolean allowSend ( String phone) {
String key = "ratelimit:sms:" + phone + ":60" ;
Long c = tpl. opsForValue ( ) . increment ( key) ;
if ( c != null && c == 1 ) {
tpl. expire ( key, Duration . ofSeconds ( 60 ) ) ;
}
return c != null && c <= 5 ;
}
public void addBlacklist ( String phone) {
tpl. opsForSet ( ) . add ( "blacklist:sms" , phone) ;
}
public boolean isBlacklisted ( String phone) {
Boolean b = tpl. opsForSet ( ) . isMember ( "blacklist:sms" , phone) ;
return Boolean . TRUE. equals ( b) ;
}
public boolean sendCodeSafely ( String phone, String code) {
if ( isBlacklisted ( phone) ) {
return false ;
}
if ( ! allowSend ( phone) ) {
return false ;
}
tpl. opsForValue ( ) . set ( "sms:" + phone, code, Duration . ofMinutes ( 5 ) ) ;
return true ;
}
public boolean allowSendIp ( String ip) {
String key = "ratelimit:sms:ip:" + ip + ":60" ;
Long c = tpl. opsForValue ( ) . increment ( key) ;
if ( c != null && c == 1 ) {
tpl. expire ( key, Duration . ofSeconds ( 60 ) ) ;
}
return c != null && c <= 20 ;
}
public boolean sendCodeSafely ( String phone, String code, String ip) {
if ( isBlacklisted ( phone) ) {
return false ;
}
if ( ! allowSend ( phone) ) {
return false ;
}
if ( ! allowSendIp ( ip) ) {
return false ;
}
tpl. opsForValue ( ) . set ( "sms:" + phone, code, Duration . ofMinutes ( 5 ) ) ;
return true ;
}
}
通俗说明
发验证码后把值放入 Redis 并设置过期,验证时直接比对即可。 频控与黑名单要配合使用,防止短信接口被刷。
详细解释
键形如 sms:{phone},写入时设置 5 分钟过期,避免旧码被使用。 频控用 计数+TTL,黑名单用 Set 管理异常号码,防刷更稳。 手机 + IP 双限流组合,先黑名单后限流,进一步提升防刷效果。 验证成功后建议删除验证码或缩短有效期。
速记口诀
👍 点赞与取消
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
@Service
public class LikesService {
@Autowired
RedisTemplate < String , String > tpl;
public boolean like ( String newsId, String userId) {
Long r = tpl. opsForSet ( ) . add ( "like:news:" + newsId, userId) ;
return r != null && r > 0 ;
}
public boolean unlike ( String newsId, String userId) {
Long r = tpl. opsForSet ( ) . remove ( "like:news:" + newsId, userId) ;
return r != null && r > 0 ;
}
}
通俗说明
点赞用 Set 天然去重;取消点赞用 remove 即可。 展示总数用 size,高并发场景注意写入与读取一致性。
详细解释
点赞键按内容分片 like:news:{id},成员为用户 ID,唯一性自然保证。 取消点赞是移除成员;展示总数用 SCARD/size,大集合下注意性能。 并发一致性可通过队列异步落库,避免强一致开销。
速记口诀
🏆 排行榜
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
import java. util. Set ;
@Service
public class RankService {
@Autowired
RedisTemplate < String , String > tpl;
public void addScore ( String userId, double s) {
tpl. opsForZSet ( ) . incrementScore ( "rank:game" , userId, s) ;
}
public Set < String > top ( int n) {
return tpl. opsForZSet ( ) . reverseRange ( "rank:game" , 0 , n - 1 ) ;
}
}
通俗说明
排行榜用 ZSet,分数越高排名越靠前;取 TopN 用倒序范围。 分数设计要可叠加,避免浮点误差带来排序抖动。
详细解释
分数可做加权或时间衰减,设计好累加策略与精度控制。 读取 TopN 用倒序范围,带分数展示更直观;分页需控制范围与内存。 并发写需使用原子增分接口,避免排名抖动。
速记口诀
🗓️ 签到位图
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
@Service
public class SignCalendar {
@Autowired
RedisTemplate < String , String > tpl;
public void sign ( String userId, int dayIndex) {
tpl. opsForValue ( ) . setBit ( "signin:2025-11:" + userId, dayIndex, true ) ;
}
public Long days ( String userId) {
return tpl. execute ( c -> c. bitCount ( ( "signin:2025-11:" + userId) . getBytes ( ) ) ) ;
}
}
通俗说明
每天一个位,签到就把对应位设为 1;统计用位计数即可。 偏移从 0 开始,注意与日历日期的对应关系。
详细解释
键形如 signin:{yyyy-MM}:{uid},偏移按当天序号映射(0 起)。 统计用 BITCOUNT;连续天数与补签可通过位运算实现。 月度数据可加 TTL 或归档,避免长期占用内存。
速记口诀
📈 UV 统计 HLL
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Component ;
import org. springframework. data. redis. core. RedisTemplate ;
@Component
public class UVService {
@Autowired
RedisTemplate < String , String > tpl;
public void visit ( String day, String uid) {
tpl. opsForHyperLogLog ( ) . add ( "uv:" + day, uid) ;
}
public long uv ( String day) {
Long c = tpl. opsForHyperLogLog ( ) . size ( "uv:" + day) ;
return c == null ? 0 : c;
}
}
通俗说明
HLL 用极小内存近似统计 UV,有轻微误差但可接受。 适合访问统计,不用于强一致性结算。
详细解释
记录用 PFADD,统计用 PFCOUNT;误差约 1% 可接受。 比 Set 更省内存,适合千万级 UV 场景;不用于账务结算。 结果为近似值,需明确业务使用边界。
速记口诀
📍 Geo 附近
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
import org. springframework. data. geo. Point ;
import org. springframework. data. geo. Distance ;
import org. springframework. data. geo. Metrics ;
import org. springframework. data. geo. Circle ;
import org. springframework. data. redis. connection. RedisGeoCommands ;
import org. springframework. data. redis. connection. RedisGeoCommands. GeoRadiusCommandArgs ;
import org. springframework. data. redis. connection. RedisGeoCommands. GeoLocation ;
import org. springframework. data. redis. core. RedisGeoCommands ;
import org. springframework. data. redis. core. GeoResults ;
import java. util. ArrayList ;
import java. util. List ;
import java. util. Comparator ;
@Service
public class NearbyService {
@Autowired
RedisTemplate < String , String > tpl;
public void add ( String name, double lon, double lat) {
tpl. opsForGeo ( ) . add ( "geo:shop" , new Point ( lon, lat) , name) ;
}
public Object search ( double lon, double lat) {
return tpl. opsForGeo ( ) . radius ( "geo:shop" , new Point ( lon, lat) , new Distance ( 5 , Metrics . KILOMETERS) ) ;
}
public Object searchPageByDistance ( double lon, double lat, double radiusKm, int page, int size) {
Circle circle = new Circle ( new Point ( lon, lat) , new Distance ( radiusKm, Metrics . KILOMETERS) ) ;
GeoRadiusCommandArgs args = GeoRadiusCommandArgs . newGeoRadiusArgs ( ) . includeDistance ( ) . sortAscending ( ) . limit ( page * size + size) ;
GeoResults < GeoLocation < String > > results = ( GeoResults < GeoLocation < String > > ) tpl. opsForGeo ( ) . radius ( "geo:shop" , circle, args) ;
List < GeoResults < GeoLocation < String > > . GeoResult< GeoLocation < String > > > list = results. getContent ( ) ;
int from = Math . min ( page * size, list. size ( ) ) ;
int to = Math . min ( from + size, list. size ( ) ) ;
return list. subList ( from, to ) ;
}
public List < String > searchByDistanceAndRating ( double lon, double lat, double radiusKm, int size, double alpha) {
Circle circle = new Circle ( new Point ( lon, lat) , new Distance ( radiusKm, Metrics . KILOMETERS) ) ;
GeoRadiusCommandArgs args = GeoRadiusCommandArgs . newGeoRadiusArgs ( ) . includeDistance ( ) ;
GeoResults < GeoLocation < String > > results = ( GeoResults < GeoLocation < String > > ) tpl. opsForGeo ( ) . radius ( "geo:shop" , circle, args) ;
class Scored { String id; double s; Scored ( String id, double s) { this . id= id; this . s= s; } }
List < Scored > scored = new ArrayList < > ( ) ;
for ( var r : results) {
String shop = r. getContent ( ) . getName ( ) ;
double distKm = r. getDistance ( ) . getValue ( ) ;
Double rating = tpl. opsForZSet ( ) . score ( "shop:rating" , shop) ;
double score = alpha * distKm + ( rating != null ? ( 1 - rating / 5.0 ) * ( 1 - alpha) : ( 1 - alpha) ) ;
scored. add ( new Scored ( shop, score) ) ;
}
scored. sort ( Comparator . comparingDouble ( x -> x. s) ) ;
List < String > out = new ArrayList < > ( ) ;
for ( int i = 0 ; i < Math . min ( size, scored. size ( ) ) ; i++ ) {
out. add ( scored. get ( i) . id) ;
}
return out;
}
public void addToCity ( String city, String name, double lon, double lat) {
tpl. opsForGeo ( ) . add ( "geo:shop:" + city, new Point ( lon, lat) , name) ;
}
public Object searchInCity ( String city, double lon, double lat, double radiusKm) {
return tpl. opsForGeo ( ) . radius ( "geo:shop:" + city, new Point ( lon, lat) , new Distance ( radiusKm, Metrics . KILOMETERS) ) ;
}
private String tileKey ( double lon, double lat, double sizeKm) {
double sizeDeg = sizeKm / 111.0 ;
int x = ( int ) Math . floor ( lon / sizeDeg) ;
int y = ( int ) Math . floor ( lat / sizeDeg) ;
return x + ":" + y;
}
private List < String > tilesInRange ( double lon, double lat, double radiusKm, double sizeKm) {
double sizeDeg = sizeKm / 111.0 ;
double rDeg = radiusKm / 111.0 ;
int minX = ( int ) Math . floor ( ( lon - rDeg) / sizeDeg) ;
int maxX = ( int ) Math . floor ( ( lon + rDeg) / sizeDeg) ;
int minY = ( int ) Math . floor ( ( lat - rDeg) / sizeDeg) ;
int maxY = ( int ) Math . floor ( ( lat + rDeg) / sizeDeg) ;
List < String > t = new ArrayList < > ( ) ;
for ( int x = minX; x <= maxX; x++ ) {
for ( int y = minY; y <= maxY; y++ ) {
t. add ( x + ":" + y) ;
}
}
return t;
}
public void addPartitioned ( String name, double lon, double lat, double sizeKm) {
String part = "geo:shop:tile:" + tileKey ( lon, lat, sizeKm) ;
tpl. opsForGeo ( ) . add ( part, new Point ( lon, lat) , name) ;
}
public List < Object > searchPartitioned ( double lon, double lat, double radiusKm, double sizeKm) {
List < String > tiles = tilesInRange ( lon, lat, radiusKm, sizeKm) ;
List < Object > res = new ArrayList < > ( ) ;
for ( String t : tiles) {
res. add ( tpl. opsForGeo ( ) . radius ( "geo:shop:tile:" + t, new Point ( lon, lat) , new Distance ( radiusKm, Metrics . KILOMETERS) ) ) ;
}
return res;
}
}
通俗说明
添加门店坐标后,可按当前位置与半径搜索附近结果。 距离单位需统一(米/公里),避免显示误差。
详细解释
添加用 GEOADD,查询用 GEORADIUS 或 Spring 对应操作。 单位统一为米或公里;分页与排序按距离或评分组合。 大量点需分区或分层存储,避免单键过大导致慢查询。
速记口诀
🎫 会话与 Token
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
import java. time. Duration ;
@Service
public class SessionService {
@Autowired
RedisTemplate < String , String > tpl;
public void issue ( String token, String uid) {
tpl. opsForValue ( ) . set ( "session:" + token, uid, Duration . ofHours ( 2 ) ) ;
}
public String validate ( String token) {
return tpl. opsForValue ( ) . get ( "session:" + token) ;
}
}
通俗说明
会话用 token->用户ID 存储并设置过期,校验时直接读取。 支持续期与强制下线,结合 TTL 实现会话管理。
详细解释
发放时写入 session:{token} 并设置过期,续期用刷新 TTL。 强制下线是删除键或缩短 TTL;可记录最近活动时间作为参考。 安全性需结合白名单与加密,避免伪造与窃取。
速记口诀
♻️ 幂等与去重
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
import java. time. Duration ;
@Service
public class IdempotentService {
@Autowired
RedisTemplate < String , String > tpl;
public boolean once ( String reqId) {
Boolean ok = tpl. opsForValue ( ) . setIfAbsent ( "once:" + reqId, "1" , Duration . ofMinutes ( 10 ) ) ;
return ok != null && ok;
}
}
通俗说明
幂等就像“一次性票”:同一个请求 ID 只能用一次,防止重复下单或扣费。 setIfAbsent 写入成功代表“首次”,失败代表“已经用过”,简单直接。请求 ID 可以用订单号、消息 ID 或指纹哈希,保证全局唯一。
详细解释
键形如 once:{reqId},过期时间覆盖业务重试窗口即可。 失败即表示重复调用,直接返回上次结果或提示用户。 请求指纹可由入参+时间窗哈希生成,确保全局唯一。
速记口诀
📊 API 网关唯一设备估算(HLL)
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
@Service
public class ApiGatewayDeviceStats {
@Autowired
RedisTemplate < String , String > tpl;
public void record ( String day, String deviceId) {
tpl. opsForHyperLogLog ( ) . add ( "uv:api:gw:" + day, deviceId) ;
}
public long uniqueDevices ( String day) {
Long c = tpl. opsForHyperLogLog ( ) . size ( "uv:api:gw:" + day) ;
return c == null ? 0 : c;
}
public long mergeWeek ( String week, String . . . days) {
return tpl. opsForHyperLogLog ( ) . union ( "uv:api:gw:" + week, days) ;
}
}
通俗说明
以设备 ID 为唯一标识,按日记录、按周汇总近似独设备数。 适合流量画像与容量规划,误差可接受,不用于计费或风控精确统计。
详细解释
统一设备标识规范(如硬件 ID/账号关联),避免匿名与重复导致偏差。 合并按寄存器最大值近似去重;月度汇总可用周汇总再并集。
速记口诀
🧾 日志采集去重估算(HLL)
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
@Service
public class LogIngestUnique {
@Autowired
RedisTemplate < String , String > tpl;
public void ingest ( String day, String fingerprint) {
tpl. opsForHyperLogLog ( ) . add ( "uv:log:fp:" + day, fingerprint) ;
}
public long uniqueEvents ( String day) {
Long c = tpl. opsForHyperLogLog ( ) . size ( "uv:log:fp:" + day) ;
return c == null ? 0 : c;
}
}
通俗说明
以日志指纹(字段拼接哈希)近似统计唯一事件规模,评估采集质量与重复率。 指纹重复不会增加计数;近似结果用于报表与监控。
详细解释
指纹建议包含关键字段(时间窗、级别、来源、错误码等),提升去重准确度。 精确去重或重放需用队列/数据库实现;HLL 仅承担规模估算。
速记口诀
🌐 全域用户合并估算(HLL)
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. stereotype. Service ;
import org. springframework. data. redis. core. RedisTemplate ;
@Service
public class AllRegionUserUnion {
@Autowired
RedisTemplate < String , String > tpl;
public long unionDaily ( String day) {
return tpl. opsForHyperLogLog ( ) . union (
"uv:user:all:" + day,
"uv:user:cn:" + day,
"uv:user:us:" + day,
"uv:user:eu:" + day
) ;
}
}
通俗说明
跨地域/业务线的用户 ID 做并集合并,得到全域近似规模。 适合全域画像与跨区域容量评估,误差可接受。
详细解释
统一用户 ID 映射,避免跨系统标识不一致导致重复或漏计。 按日并集后可再做周/月度并集,形成层级汇总。
🔍 小结
验证码登录需设置频控与黑名单,避免短信接口被滥用。 点赞去重使用 Set,取消时需保证并发一致性;可结合消息异步同步数据库。 排行榜使用 ZSet,分页查询时控制范围与内存开销。 签到位图适合按天记录,统计连续天数可结合位运算。 UV 统计用 HLL 近似去重,误差在可接受范围内换取低内存占用。 Geo 适合附近搜索,需配合距离与分页策略。 会话与 Token 要求可续期与踢人策略,避免长期占用。 幂等与去重通过一次性令牌或请求指纹,防止重复提交。