第四章:Redis实战应用及常见问题(下篇)

目录

[1. 优惠券秒杀系统](#1. 优惠券秒杀系统)

[1.1 全局唯一ID生成](#1.1 全局唯一ID生成)

[1.2 秒杀下单基础实现](#1.2 秒杀下单基础实现)

[1.3 一人一单限制](#1.3 一人一单限制)

[2. 分布式锁](#2. 分布式锁)

[2.1 分布式锁基本概念](#2.1 分布式锁基本概念)

[2.2 Redis分布式锁基础实现](#2.2 Redis分布式锁基础实现)

[2.3 解决锁误删问题](#2.3 解决锁误删问题)

[2.4 Lua脚本保证原子性](#2.4 Lua脚本保证原子性)

[2.5 Redisson分布式锁](#2.5 Redisson分布式锁)

[2.6 多级锁(MultiLock)](#2.6 多级锁(MultiLock))

[3. 异步秒杀优化](#3. 异步秒杀优化)

[3.1 秒杀优化思路](#3.1 秒杀优化思路)

[3.2 Redis快速校验](#3.2 Redis快速校验)

[3.3 异步下单实现](#3.3 异步下单实现)

[4. Redis消息队列](#4. Redis消息队列)

[4.1 消息队列基本概念](#4.1 消息队列基本概念)

[4.2 基于List的消息队列](#4.2 基于List的消息队列)

[4.3 基于PubSub的消息队列](#4.3 基于PubSub的消息队列)

[4.4 基于Stream的消息队列](#4.4 基于Stream的消息队列)

[4.5 Stream实现异步秒杀](#4.5 Stream实现异步秒杀)

[5. 点赞与关注功能](#5. 点赞与关注功能)

[5.1 点赞功能实现](#5.1 点赞功能实现)

[5.2 关注功能实现](#5.2 关注功能实现)

[6. Feed流实现](#6. Feed流实现)

[6.1 Feed流模式](#6.1 Feed流模式)

[6.2 基于推模式的Feed流](#6.2 基于推模式的Feed流)

[6.3 滚动分页查询](#6.3 滚动分页查询)

[7. 附近商户搜索](#7. 附近商户搜索)

[7.1 GEO数据结构](#7.1 GEO数据结构)

[7.2 商户数据导入GEO](#7.2 商户数据导入GEO)

[7.3 附近商户查询](#7.3 附近商户查询)

[8. 用户签到](#8. 用户签到)

[8.1 BitMap数据结构](#8.1 BitMap数据结构)

[8.2 签到功能实现](#8.2 签到功能实现)

[9. UV统计](#9. UV统计)

[9.1 HyperLogLog](#9.1 HyperLogLog)

[9.2 UV统计实现](#9.2 UV统计实现)

总结


本文介绍了基于Redis的优惠券秒杀系统实现方案,重点讲解了分布式锁、异步秒杀优化和Redis高级功能应用。系统采用Redis分布式ID生成器解决ID冲突问题,通过Redis+Lua脚本实现原子操作,优化了秒杀流程。文章详细阐述了Redis分布式锁的实现与优化方案,包括锁误删问题解决、Lua脚本原子操作和Redisson集成。在秒杀优化方面,提出异步下单方案,使用Redis快速校验资格,通过消息队列处理订单。此外,还介绍了Redis在社交功能(点赞、关注、Feed流)、地理位置搜索、用户签到和UV统计等场景的应用实践,展示了Redis作为高性能数据存储的多样化解决方案。

1. 优惠券秒杀系统

1.1 全局唯一ID生成

数据库自增ID的问题

  1. ID规律性明显,易猜测

  2. 受单表数据量限制

  3. 分库分表时ID冲突

Redis分布式ID生成器

java 复制代码
@Component
public class RedisIdWorker {
    private static final long BEGIN_TIMESTAMP = 1640995200L; // 开始时间戳
    private static final int COUNT_BITS = 32; // 序列号位数
    
    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        
        // 2.生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue()
            .increment("icr:" + keyPrefix + ":" + date);
        
        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

1.2 秒杀下单基础实现

下单流程

  1. 查询优惠券信息

  2. 判断秒杀时间

  3. 判断库存

  4. 扣减库存

  5. 创建订单

基础代码:

java 复制代码
public Result seckillVoucher(Long voucherId) {
    // 1.查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    
    // 2.判断秒杀时间
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束!");
    }
    
    // 3.判断库存
    if (voucher.getStock() < 1) {
        return Result.fail("库存不足!");
    }
    
    // 4.扣减库存(乐观锁)
    boolean success = seckillVoucherService.update()
        .setSql("stock = stock - 1")
        .eq("voucher_id", voucherId)
        .gt("stock", 0)
        .update();
    
    if (!success) {
        return Result.fail("库存不足!");
    }
    
    // 5.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setId(redisIdWorker.nextId("order"));
    voucherOrder.setUserId(UserHolder.getUser().getId());
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    
    return Result.ok(voucherOrder.getId());
}

1.3 一人一单限制

实现思路

  1. 查询用户是否已下单

  2. 使用 synchronized 保证单机线程安全

  3. 使用分布式锁解决集群环境问题

单机版实现:

java 复制代码
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    
    // 判断是否已购买
    int count = query().eq("user_id", userId)
        .eq("voucher_id", voucherId).count();
    if (count > 0) {
        return Result.fail("用户已经购买过一次!");
    }
    
    // ... 扣减库存和创建订单逻辑
}

2. 分布式锁

2.1 分布式锁基本概念

分布式锁要求

  1. 可见性:多个进程都能看到锁状态

  2. 互斥性:同一时刻只有一个进程持有锁

  3. 高可用:不易崩溃

  4. 高性能:加锁释放锁性能高

  5. 安全性:防止死锁

实现方案对比

  1. MySQL:性能一般

  2. Redis:常用方案,基于 setnx

  3. ZooKeeper:企业级方案

2.2 Redis分布式锁基础实现

基本操作:

java 复制代码
// 获取锁
public boolean tryLock(long timeoutSec) {
    String threadId = Thread.currentThread().getId();
    Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

// 释放锁
public void unlock() {
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

2.3 解决锁误删问题

问题:线程A锁超时自动释放,线程B获取锁,线程A恢复后误删线程B的锁

解决方案:在锁中存储线程标识,释放时验证

改进代码:

java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

public boolean tryLock(long timeoutSec) {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

public void unlock() {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    if (threadId.equals(id)) {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

2.4 Lua脚本保证原子性

问题:判断锁标识和删除锁不是原子操作

Lua脚本解决方案:

Lua 复制代码
-- 释放锁的Lua脚本
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    return redis.call('DEL', KEYS[1])
end
return 0

Java调用:

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static {
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    UNLOCK_SCRIPT.setResultType(Long.class);
}

public void unlock() {
    stringRedisTemplate.execute(
        UNLOCK_SCRIPT,
        Collections.singletonList(KEY_PREFIX + name),
        ID_PREFIX + Thread.currentThread().getId()
    );
}

2.5 Redisson分布式锁

Redisson优势

  1. 可重入锁

  2. 可重试机制

  3. 超时续期(看门狗机制)

  4. 主从一致性支持

使用示例:

java 复制代码
@Resource
private RedissonClient redissonClient;

public Result seckillVoucher(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    RLock lock = redissonClient.getLock("lock:order:" + userId);
    
    boolean isLock = lock.tryLock();
    if (!isLock) {
        return Result.fail("不允许重复下单");
    }
    
    try {
        // 业务逻辑
        return createVoucherOrder(voucherId);
    } finally {
        lock.unlock();
    }
}

2.6 多级锁(MultiLock)

解决主从一致性问题

  • 写入所有Redis节点,所有节点成功才算加锁成功

  • 提高锁的可靠性,防止主节点宕机导致锁丢失

3. 异步秒杀优化

3.1 秒杀优化思路

传统流程问题

  1. 查询优惠券

  2. 判断库存

  3. 查询订单

  4. 校验一人一单

  5. 扣减库存

  6. 创建订单

优化方案

  1. Redis快速校验(库存、一人一单)

  2. 异步下单

  3. 消息队列处理订单

3.2 Redis快速校验

Lua脚本实现原子操作:

Lua 复制代码
-- 秒杀资格判断Lua脚本
local voucherId = ARGV[1]
local userId = ARGV[2]

local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId

-- 判断库存
if(tonumber(redis.call('get', stockKey)) <= 0) then
    return 1
end

-- 判断是否重复下单
if(redis.call('sismember', orderKey, userId) == 1) then
    return 2
end

-- 扣减库存,保存订单
redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
return 0

3.3 异步下单实现

阻塞队列方案:

java 复制代码
// 创建阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

// 异步处理线程
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                VoucherOrder voucherOrder = orderTasks.take();
                handleVoucherOrder(voucherOrder);
            } catch (Exception e) {
                log.error("处理订单异常", e);
            }
        }
    }
}

// 下单方法
public Result seckillVoucher(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    
    // 执行Lua脚本
    Long result = stringRedisTemplate.execute(SECKILL_SCRIPT, ...);
    
    if (result != 0) {
        return Result.fail(result == 1 ? "库存不足" : "不能重复下单");
    }
    
    // 创建订单对象
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setId(redisIdWorker.nextId("order"));
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);
    
    // 放入阻塞队列
    orderTasks.add(voucherOrder);
    
    return Result.ok(voucherOrder.getId());
}

4. Redis消息队列

4.1 消息队列基本概念

消息队列角色

  1. 消息队列:存储管理消息

  2. 生产者:发送消息

  3. 消费者:获取并处理消息

优点:解耦、异步、削峰

4.2 基于List的消息队列

基本操作

  • LPUSH + RPOP 或 RPUSH + LPOP

  • BRPOP/BLPOP 实现阻塞

优缺点

  • 优点:数据安全、有序

  • 缺点:消息丢失、单消费者

4.3 基于PubSub的消息队列

基本操作

  • SUBSCRIBE:订阅频道

  • PUBLISH:发布消息

  • PSUBSCRIBE:模式订阅

优缺点

  • 优点:多生产多消费

  • 缺点:无持久化、可能丢失消息

4.4 基于Stream的消息队列

Stream优势

  1. 消息可回溯

  2. 支持多消费者

  3. 阻塞读取

  4. 消息确认机制

基本命令

bash

java 复制代码
# 添加消息
XADD stream.orders * key1 value1 key2 value2

# 读取消息
XREAD COUNT 1 BLOCK 2000 STREAMS stream.orders 0

# 消费者组
XGROUP CREATE stream.orders g1 0
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >

4.5 Stream实现异步秒杀

优化方案

  1. Lua脚本操作后,将订单信息发送到Stream

  2. 独立线程消费Stream中的订单

实现代码:

Lua 复制代码
// Lua脚本添加发送消息
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
java 复制代码
// 消费者组处理订单
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream()
                    .read(Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create("stream.orders", ReadOffset.lastConsumed()));
                
                if (list != null && !list.isEmpty()) {
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    handleVoucherOrder(voucherOrder);
                    stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
                }
            } catch (Exception e) {
                log.error("处理订单异常", e);
                handlePendingList();
            }
        }
    }
}

5. 点赞与关注功能

5.1 点赞功能实现

需求

  1. 同一用户只能点赞一次

  2. 可取消点赞

  3. 显示点赞数量

  4. 点赞排行榜

SortedSet实现:

java 复制代码
// 点赞
public Result likeBlog(Long id) {
    Long userId = UserHolder.getUser().getId();
    String key = "blog:liked:" + id;
    
    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
    if (score == null) {
        // 未点赞,点赞数+1,记录用户
        boolean success = update().setSql("liked = liked + 1").eq("id", id).update();
        if (success) {
            stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
        }
    } else {
        // 已点赞,取消点赞
        boolean success = update().setSql("liked = liked - 1").eq("id", id).update();
        if (success) {
            stringRedisTemplate.opsForZSet().remove(key, userId.toString());
        }
    }
    return Result.ok();
}

// 查询点赞排行榜
public Result queryBlogLikes(Long id) {
    String key = "blog:liked:" + id;
    Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
    if (top5 == null || top5.isEmpty()) {
        return Result.ok(Collections.emptyList());
    }
    
    List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
    String idStr = StrUtil.join(",", ids);
    
    List<UserDTO> userDTOS = userService.query()
        .in("id", ids)
        .last("ORDER BY FIELD(id," + idStr + ")")
        .list()
        .stream()
        .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
        .collect(Collectors.toList());
    
    return Result.ok(userDTOS);
}

5.2 关注功能实现

关注与取关:

java 复制代码
// 关注/取关
public Result follow(Long followUserId, Boolean isFollow) {
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    
    if (isFollow) {
        // 关注
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        boolean success = save(follow);
        if (success) {
            stringRedisTemplate.opsForSet().add(key, followUserId.toString());
        }
    } else {
        // 取关
        boolean success = remove(new QueryWrapper<Follow>()
            .eq("user_id", userId).eq("follow_user_id", followUserId));
        if (success) {
            stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
        }
    }
    return Result.ok();
}

// 共同关注
public Result followCommons(Long id) {
    Long userId = UserHolder.getUser().getId();
    String key1 = "follows:" + userId;
    String key2 = "follows:" + id;
    
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
    if (intersect == null || intersect.isEmpty()) {
        return Result.ok(Collections.emptyList());
    }
    
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    List<UserDTO> users = userService.listByIds(ids)
        .stream()
        .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
        .collect(Collectors.toList());
    
    return Result.ok(users);
}

6. Feed流实现

6.1 Feed流模式

三种模式

  1. 拉模式:用户读取时拉取关注者内容

    • 优点:节约空间

    • 缺点:延迟高,压力大

  2. 推模式:发布时推送给粉丝

    • 优点:时效性好

    • 缺点:内存压力大

  3. 推拉结合:普通用户推,大V用户推拉结合

6.2 基于推模式的Feed流

实现方案

  1. 发布笔记时推送给所有粉丝

  2. 使用SortedSet存储,分数为时间戳

  3. 实现滚动分页查询

发布笔记:

java 复制代码
public Result saveBlog(Blog blog) {
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    boolean isSuccess = save(blog);
    
    if (!isSuccess) {
        return Result.fail("新增笔记失败!");
    }
    
    // 查询作者粉丝
    List<Follow> follows = followService.query()
        .eq("follow_user_id", user.getId()).list();
    
    // 推送给粉丝
    for (Follow follow : follows) {
        Long userId = follow.getUserId();
        String key = "feed:" + userId;
        stringRedisTemplate.opsForZSet()
            .add(key, blog.getId().toString(), System.currentTimeMillis());
    }
    
    return Result.ok(blog.getId());
}

6.3 滚动分页查询

传统分页问题:数据变化导致重复或遗漏

滚动分页方案

  1. 记录上次查询的最小时间戳

  2. 记录偏移量

  3. 基于SortedSet的ZREVRANGEBYSCORE

实现代码:

java 复制代码
public Result queryBlogOfFollow(Long max, Integer offset) {
    Long userId = UserHolder.getUser().getId();
    String key = "feed:" + userId;
    
    // 查询收件箱
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
        .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
    
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }
    
    // 解析数据
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0;
    int os = 1;
    
    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
        ids.add(Long.valueOf(tuple.getValue()));
        long time = tuple.getScore().longValue();
        
        if (time == minTime) {
            os++;
        } else {
            minTime = time;
            os = 1;
        }
    }
    
    os = minTime == max ? os : os + offset;
    
    // 查询博客详情
    String idStr = StrUtil.join(",", ids);
    List<Blog> blogs = query().in("id", ids)
        .last("ORDER BY FIELD(id," + idStr + ")").list();
    
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);
    
    return Result.ok(r);
}

7. 附近商户搜索

7.1 GEO数据结构

基本命令

  • GEOADD:添加地理坐标

  • GEODIST:计算距离

  • GEOHASH:坐标转哈希

  • GEOPOS:获取坐标

  • GEOSEARCH:范围搜索

7.2 商户数据导入GEO

java 复制代码
@Test
void loadShopData() {
    List<Shop> list = shopService.list();
    Map<Long, List<Shop>> map = list.stream()
        .collect(Collectors.groupingBy(Shop::getTypeId));
    
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
        Long typeId = entry.getKey();
        String key = "shop:geo:" + typeId;
        List<Shop> value = entry.getValue();
        
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
        for (Shop shop : value) {
            locations.add(new RedisGeoCommands.GeoLocation<>(
                shop.getId().toString(),
                new Point(shop.getX(), shop.getY())
            ));
        }
        stringRedisTemplate.opsForGeo().add(key, locations);
    }
}

7.3 附近商户查询

java 复制代码
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
    if (x == null || y == null) {
        // 无坐标查询
        Page<Shop> page = query()
            .eq("type_id", typeId)
            .page(new Page<>(current, DEFAULT_PAGE_SIZE));
        return Result.ok(page.getRecords());
    }
    
    // 分页参数
    int from = (current - 1) * DEFAULT_PAGE_SIZE;
    int end = current * DEFAULT_PAGE_SIZE;
    
    // GEO查询
    String key = "shop:geo:" + typeId;
    GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
        .search(key, GeoReference.fromCoordinate(x, y),
            new Distance(5000),
            RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                .includeDistance().limit(end));
    
    // 解析结果
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
    if (list.size() <= from) {
        return Result.ok(Collections.emptyList());
    }
    
    List<Long> ids = new ArrayList<>(list.size());
    Map<String, Distance> distanceMap = new HashMap<>(list.size());
    
    list.stream().skip(from).forEach(result -> {
        String shopIdStr = result.getContent().getName();
        ids.add(Long.valueOf(shopIdStr));
        distanceMap.put(shopIdStr, result.getDistance());
    });
    
    // 查询店铺详情
    String idStr = StrUtil.join(",", ids);
    List<Shop> shops = query().in("id", ids)
        .last("ORDER BY FIELD(id," + idStr + ")").list();
    
    for (Shop shop : shops) {
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    }
    
    return Result.ok(shops);
}

8. 用户签到

8.1 BitMap数据结构

基本命令

  • SETBIT:设置位

  • GETBIT:获取位

  • BITCOUNT:统计1的数量

  • BITFIELD:操作位数组

8.2 签到功能实现

java 复制代码
// 签到
public Result sign() {
    Long userId = UserHolder.getUser().getId();
    LocalDateTime now = LocalDateTime.now();
    
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = "sign:" + userId + keySuffix;
    
    int dayOfMonth = now.getDayOfMonth();
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    
    return Result.ok();
}

// 统计连续签到天数
public Result signCount() {
    Long userId = UserHolder.getUser().getId();
    LocalDateTime now = LocalDateTime.now();
    
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = "sign:" + userId + keySuffix;
    
    int dayOfMonth = now.getDayOfMonth();
    
    // 获取本月签到记录
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
        key, BitFieldSubCommands.create()
            .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
            .valueAt(0));
    
    if (result == null || result.isEmpty()) {
        return Result.ok(0);
    }
    
    Long num = result.get(0);
    if (num == null || num == 0) {
        return Result.ok(0);
    }
    
    // 统计连续签到
    int count = 0;
    while (true) {
        if ((num & 1) == 0) {
            break;
        } else {
            count++;
        }
        num >>>= 1;
    }
    
    return Result.ok(count);
}

9. UV统计

9.1 HyperLogLog

特点

  • 统计巨大集合的基数

  • 内存占用小(<16KB)

  • 概率性统计(误差<0.81%)

基本命令

  • PFADD:添加元素

  • PFCOUNT:统计基数

  • PFMERGE:合并多个HyperLogLog

9.2 UV统计实现

java 复制代码
// 用户访问时添加
stringRedisTemplate.opsForHyperLogLog().add("uv:" + date, userId.toString());

// 统计UV
Long size = stringRedisTemplate.opsForHyperLogLog().size("uv:" + date);

// 合并多日数据
stringRedisTemplate.opsForHyperLogLog().union("uv:week", "uv:day1", "uv:day2", "uv:day3");
Long weekSize = stringRedisTemplate.opsForHyperLogLog().size("uv:week");

总结

Redis在实际应用中扮演着重要角色,从基础的缓存、会话管理,到复杂的分布式锁、消息队列、地理搜索等高级功能。掌握这些实战技能对于构建高性能、可扩展的系统至关重要。本文涵盖的知识点包括:

  1. 会话管理:使用Redis实现分布式Session

  2. 缓存策略:缓存穿透、雪崩、击穿的解决方案

  3. 分布式锁:基于Redis的分布式锁实现与优化

  4. 秒杀系统:异步秒杀、库存扣减、一人一单

  5. 消息队列:List、PubSub、Stream三种实现方案

  6. 社交功能:点赞、关注、Feed流

  7. 地理位置:GEO数据结构与附近搜索

  8. 数据统计:BitMap签到、HyperLogLog UV统计

相关推荐
量子炒饭大师2 小时前
【C++入门】Cyber霓虹镜像城的跨域通行证 —— 【友元(friend)】跨类协作破坏封装性?友元函数与友元类为你架起特权桥梁!
java·开发语言·c++·友元函数·友元类·friend
菜鸟小九2 小时前
redis高级篇(多级缓存)
数据库·redis·缓存
没有bug.的程序员2 小时前
Spring Cloud Stream:消息驱动微服务的实战与 Kafka 集成终极指南
java·微服务·架构·kafka·stream·springcloud·消息驱动
{Hello World}2 小时前
Java内部类:深入解析四大类型与应用
java·开发语言
春日见2 小时前
C++单例模式 (Singleton Pattern)
java·运维·开发语言·驱动开发·算法·docker·单例模式
SamRol2 小时前
达梦数据库指令 及 在Spring Boot + MyBatis-Plus上的使用
java·数据库·spring boot·mybatis·达梦·intellij idea
Lethehong2 小时前
化繁为简,一库统揽:金仓数据库以“一体化替代”战略重构企业数据核心
数据库·重构
A懿轩A2 小时前
【2026 最新】MySQL 与 DataGrip 详细下载安装教程带图展示(Windows版)
数据库·mysql·datagrip
大白要努力!2 小时前
Android Spinner自定义背景
java·开发语言