第 6 课:Redis 常见应用场景(下)

本课重点学习 Redis 在企业项目中的高频业务场景:

  • 计数器
  • 排行榜
  • 购物车
  • 分布式限流

6.1 计数器

Redis 的原子性操作非常适合实现各种计数器功能。

常见场景:

  • 文章阅读量
  • 点赞数
  • 网站访问量(PV)
  • 用户登录次数
  • 商品浏览次数

代码示例

@Service
public class CounterService {

    @Resource
    private RedisUtil redisUtil;

    // 文章阅读量自增
    public Long incrementViewCount(Long articleId) {

        String key = "article:view:" + articleId;

        return redisUtil.increment(key);
    }

    // 获取文章阅读量
    public Long getViewCount(Long articleId) {

        String key = "article:view:" + articleId;

        Object count = redisUtil.get(key);

        return count == null
                ? 0
                : Long.parseLong(count.toString());
    }

    // 点赞数自增
    public Long incrementLikeCount(Long articleId) {

        String key = "article:like:" + articleId;

        return redisUtil.increment(key);
    }

    // 取消点赞
    public Long decrementLikeCount(Long articleId) {

        String key = "article:like:" + articleId;

        return redisUtil.increment(key, -1);
    }

    // 网站访问量统计
    public Long incrementSiteVisit() {

        return redisUtil.increment("site:visit:count");
    }
}

Redis 计数器优势

优势 说明
原子性 不会出现并发问题
高性能 单机可达十万级 QPS
实现简单 INCR 即可完成
易扩展 支持分布式部署

6.2 排行榜

Redis 的 ZSet(有序集合)天生适合实现排行榜。

每个成员都有:

member + score

Redis 会自动按照 score 排序。


应用场景

  • 游戏排行榜
  • 热搜排行榜
  • 商品销量排行
  • 用户积分排行
  • 直播打赏榜

代码示例

@Service
public class RankService {

    @Resource
    private RedisUtil redisUtil;

    // 添加用户分数
    public void addScore(Long userId, double score) {

        String key = "game:rank";

        redisUtil.zadd(
                key,
                userId.toString(),
                score
        );
    }

    // 获取排行榜前N名
    public List<RankVO> getTopN(int n) {

        String key = "game:rank";

        Set<ZSetOperations.TypedTuple<Object>> tuples =
                redisUtil.zrevrangeWithScores(
                        key,
                        0,
                        n - 1
                );

        List<RankVO> rankList =
                new ArrayList<>();

        int rank = 1;

        for (ZSetOperations.TypedTuple<Object> tuple : tuples) {

            RankVO rankVO = new RankVO();

            rankVO.setUserId(
                    Long.parseLong(
                            tuple.getValue().toString()
                    )
            );

            rankVO.setScore(
                    tuple.getScore()
            );

            rankVO.setRank(rank++);

            rankList.add(rankVO);
        }

        return rankList;
    }

    // 获取用户排名
    public Long getUserRank(Long userId) {

        String key = "game:rank";

        Long rank =
                redisUtil.zrevrank(
                        key,
                        userId.toString()
                );

        return rank == null
                ? null
                : rank + 1;
    }

    // 获取用户分数
    public Double getUserScore(Long userId) {

        String key = "game:rank";

        return redisUtil.zscore(
                key,
                userId.toString()
        );
    }
}

排行榜核心命令

# 添加分数
zadd rank 100 user1

# 查询排行榜
zrevrange rank 0 9

# 查询排名
zrevrank rank user1

# 查询分数
zscore rank user1

6.3 购物车

Redis Hash 非常适合购物车业务。


数据结构设计

cart:1001
    ├── 2001 -> 3
    ├── 2002 -> 5
    └── 2003 -> 1

含义:

用户1001购物车:

商品2001 数量3
商品2002 数量5
商品2003 数量1

代码示例

@Service
public class CartService {

    @Resource
    private RedisUtil redisUtil;

    // 添加商品
    public void addToCart(
            Long userId,
            Long productId,
            int quantity
    ) {

        String key = "cart:" + userId;

        Object currentQuantity =
                redisUtil.hget(
                        key,
                        productId.toString()
                );

        if (currentQuantity != null) {

            quantity += Integer.parseInt(
                    currentQuantity.toString()
            );
        }

        redisUtil.hset(
                key,
                productId.toString(),
                quantity
        );
    }

    // 更新商品数量
    public void updateCartItem(
            Long userId,
            Long productId,
            int quantity
    ) {

        String key = "cart:" + userId;

        redisUtil.hset(
                key,
                productId.toString(),
                quantity
        );
    }

    // 删除商品
    public void removeCartItem(
            Long userId,
            Long productId
    ) {

        String key = "cart:" + userId;

        redisUtil.hdel(
                key,
                productId.toString()
        );
    }

    // 获取购物车
    public Map<Long, Integer> getCart(
            Long userId
    ) {

        String key = "cart:" + userId;

        Map<Object, Object> entries =
                redisUtil.hgetAll(key);

        Map<Long, Integer> cart =
                new HashMap<>();

        for (Map.Entry<Object, Object> entry
                : entries.entrySet()) {

            Long productId =
                    Long.parseLong(
                            entry.getKey().toString()
                    );

            Integer quantity =
                    Integer.parseInt(
                            entry.getValue().toString()
                    );

            cart.put(productId, quantity);
        }

        return cart;
    }

    // 清空购物车
    public void clearCart(Long userId) {

        String key = "cart:" + userId;

        redisUtil.delete(key);
    }
}

Hash 实现购物车优势

优势 说明
节省内存 一个用户一个 Key
查询快 O(1)
修改方便 单个商品独立更新
支持扩展 可以增加价格、规格等

6.4 分布式限流

限流是高并发系统的核心保护机制。


应用场景

  • 接口防刷
  • 登录防暴力破解
  • 秒杀保护
  • API 调用限制

限流流程

用户请求
     ↓
Redis计数
     ↓
超过限制?
 ┌────Yes────→ 拒绝访问
 │
 No
 │
 ↓
正常放行

Lua 实现限流

@Service
public class RateLimitService {

    @Resource
    private RedisUtil redisUtil;

    /**
     * 限流
     *
     * @param key 限流Key
     * @param limit 最大次数
     * @param period 时间窗口
     */
    public boolean isAllowed(
            String key,
            int limit,
            int period
    ) {

        String script =
                "local key = KEYS[1] " +
                "local limit = tonumber(ARGV[1]) " +
                "local period = tonumber(ARGV[2]) " +
                "local current = tonumber(redis.call('get', key) or '0') " +
                "if current + 1 > limit then " +
                "   return 0 " +
                "else " +
                "   redis.call('incr', key) " +
                "   redis.call('expire', key, period) " +
                "   return 1 " +
                "end";

        Long result =
                (Long) redisUtil.execute(
                        script,
                        Collections.singletonList(key),
                        limit,
                        period
                );

        return Long.valueOf(1)
                .equals(result);
    }
}

Controller 使用示例

@GetMapping("/api/test")
public String test(
        HttpServletRequest request
) {

    String ip =
            request.getRemoteAddr();

    String key =
            "rate:limit:" + ip;

    // 每分钟最多访问10次
    if (!rateLimitService.isAllowed(
            key,
            10,
            60
    )) {

        return "请求过于频繁,请稍后再试";
    }

    return "success";
}

本课知识总结

模块 Redis数据结构
计数器 String
排行榜 ZSet
购物车 Hash
限流 String + Lua

Redis 场景总结

场景 推荐数据结构
用户缓存 String / Hash
商品详情 Hash
阅读量统计 String
点赞数统计 String
排行榜 ZSet
标签系统 Set
消息队列 List
购物车 Hash
限流 String + Lua
分布式锁 String + Lua

课后练习

练习 1

实现文章阅读量统计:

功能:

  • 阅读量 +1
  • 查询阅读量

练习 2

实现点赞系统:

功能:

  • 点赞
  • 取消点赞
  • 查询点赞数

练习 3

实现游戏排行榜:

功能:

  • 添加分数
  • 查询排名
  • 查询 Top10
  • 查询用户积分

练习 4

实现完整购物车:

功能:

  • 添加商品
  • 修改数量
  • 删除商品
  • 查询购物车
  • 清空购物车

练习 5

实现 IP 限流:

要求:

  • 每分钟最多访问 10 次
  • 使用 Redis + Lua
  • 支持分布式部署
  • 支持高并发测试