第 5 课:Redis 常见应用场景(上)

Redis 在企业项目中最常见的用途就是:

  • 缓存
  • 分布式锁
  • 解决高并发问题

本课将通过实际项目案例学习 Redis 的核心应用场景。


5.1 缓存

缓存是 Redis 最常用的功能,可以显著提升系统性能,减少数据库压力。

缓存流程

客户端请求
     ↓
查询 Redis
     ↓
存在?
 ┌──Yes──→ 返回数据
 │
 No
 │
 ↓
查询 MySQL
 ↓
写入 Redis
 ↓
返回结果

代码示例

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private RedisUtil redisUtil;

    public User getUserById(Long userId) {

        String key = "user:" + userId;

        // 1. 查询Redis
        User user = (User) redisUtil.get(key);

        if (user != null) {
            return user;
        }

        // 2. Redis没有数据
        user = userMapper.selectById(userId);

        if (user != null) {

            // 3. 写入Redis
            redisUtil.set(
                    key,
                    user,
                    1,
                    TimeUnit.HOURS
            );
        }

        return user;
    }

    /**
     * 更新用户
     */
    public void updateUser(User user) {

        userMapper.updateById(user);

        // 删除缓存
        redisUtil.delete("user:" + user.getId());
    }

    /**
     * 删除用户
     */
    public void deleteUser(Long userId) {

        userMapper.deleteById(userId);

        redisUtil.delete("user:" + userId);
    }
}

缓存常见问题

1、缓存穿透

问题

查询一个根本不存在的数据。

例如:

user:999999999

数据库中不存在。

此时:

Redis没有
      ↓
MySQL没有
      ↓
Redis没有缓存
      ↓
再次访问数据库
      ↓
无限循环

最终导致数据库压力暴增。


解决方案一:缓存空对象

if(user == null){

    redisUtil.set(
            key,
            new NullUser(),
            5,
            TimeUnit.MINUTES
    );

    return null;
}

优点:

  • 实现简单
  • 效果明显

缺点:

  • 占用少量内存

解决方案二:布隆过滤器

流程:

请求
 ↓
BloomFilter
 ↓
存在?
 ┌──No──→ 直接返回
 │
 Yes
 │
 ↓
Redis
 ↓
MySQL

优点:

  • 性能高
  • 节省数据库资源

缺点:

  • 存在误判率

2、缓存击穿

问题

某个热点 Key 过期。

例如:

user:1

恰好:

10万用户同时访问

结果:

Redis失效
      ↓
10万请求同时打到数据库
      ↓
数据库崩溃

解决方案

方案一:热点数据永不过期

redisUtil.set(key,user);

缺点:

需要手动维护缓存。


方案二:互斥锁

只有一个线程查询数据库。

其他线程等待。


代码实现

public User getUserById(Long userId) {

    String key = "user:" + userId;

    User user = (User) redisUtil.get(key);

    if (user != null) {
        return user;
    }

    String lockKey = "lock:user:" + userId;

    String lockValue =
            UUID.randomUUID().toString();

    try {

        // 获取锁
        if (
            redisUtil.setIfAbsent(
                lockKey,
                lockValue,
                30,
                TimeUnit.SECONDS
            )
        ) {

            user =
                    userMapper.selectById(userId);

            if (user != null) {

                redisUtil.set(
                        key,
                        user,
                        1,
                        TimeUnit.HOURS
                );

            } else {

                redisUtil.set(
                        key,
                        new NullUser(),
                        5,
                        TimeUnit.MINUTES
                );
            }

        } else {

            Thread.sleep(100);

            return getUserById(userId);
        }

    } catch (InterruptedException e) {

        Thread.currentThread().interrupt();

    } finally {

        if (
            lockValue.equals(
                    redisUtil.get(lockKey)
            )
        ) {

            redisUtil.delete(lockKey);
        }
    }

    return user;
}

3、缓存雪崩

问题

大量缓存同时过期。

例如:

100万个Key
全部设置1小时过期

到达:

1小时整

结果:

Redis大量失效
       ↓
请求全部打到数据库
       ↓
数据库压力暴增

解决方案

方案一:随机过期时间

错误做法:

redisUtil.set(
        key,
        value,
        60,
        TimeUnit.MINUTES
);

正确做法:

int timeout =
        60 + RandomUtil.randomInt(30);

redisUtil.set(
        key,
        value,
        timeout,
        TimeUnit.MINUTES
);

这样缓存不会同时失效。


方案二:Redis 集群

热点数据
    ↓
分散到多个节点

降低单节点压力。


方案三:熔断降级

当数据库压力过大时:

关闭非核心功能
限制访问
快速失败

保证核心业务可用。


5.2 分布式锁

为什么需要分布式锁

单机项目:

synchronized

即可解决并发问题。

但是:

服务器A
服务器B
服务器C

各自拥有自己的 JVM。

此时:

synchronized

已经失效。

需要:

Redis分布式锁

Redis 分布式锁原理

核心命令:

SET key value NX EX timeout

参数说明:

参数 说明
NX Key 不存在才创建
EX 设置过期时间
key 锁名称
value UUID
timeout 超时时间

分布式锁实现

@Service
public class DistributedLockService {

    @Resource
    private RedisUtil redisUtil;

    /**
     * 获取锁
     */
    public boolean tryLock(
            String key,
            String value,
            long timeout
    ) {

        return redisUtil.setIfAbsent(
                key,
                value,
                timeout,
                TimeUnit.SECONDS
        );
    }

    /**
     * 释放锁
     */
    public boolean releaseLock(
            String key,
            String value
    ) {

        String script =
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) " +
                "else " +
                "return 0 " +
                "end";

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

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

秒杀案例(防止超卖)

业务流程

用户秒杀
     ↓
获取分布式锁
     ↓
检查库存
     ↓
扣减库存
     ↓
创建订单
     ↓
释放锁

实现代码

@Service
public class SeckillService {

    @Resource
    private ProductMapper productMapper;

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private DistributedLockService lockService;

    public String seckill(
            Long productId,
            Long userId
    ) {

        String lockKey =
                "lock:product:" + productId;

        String lockValue =
                UUID.randomUUID().toString();

        try {

            if (
                !lockService.tryLock(
                        lockKey,
                        lockValue,
                        30
                )
            ) {

                return "系统繁忙,请稍后再试";
            }

            // 查询库存
            Product product =
                    productMapper.selectById(productId);

            if (product.getStock() <= 0) {

                return "商品已售罄";
            }

            // 扣减库存
            product.setStock(
                    product.getStock() - 1
            );

            productMapper.updateById(product);

            // 创建订单
            Order order = new Order();

            order.setProductId(productId);

            order.setUserId(userId);

            orderMapper.insert(order);

            return "秒杀成功";

        } finally {

            lockService.releaseLock(
                    lockKey,
                    lockValue
            );
        }
    }
}

本课知识总结

模块 核心内容
Cache Redis缓存
Cache Penetration 缓存穿透
Cache Breakdown 缓存击穿
Cache Avalanche 缓存雪崩
Distributed Lock 分布式锁
Seckill 秒杀系统

课后练习

练习 1

实现完整用户缓存功能:

  • 查询缓存
  • 查询数据库
  • 回写缓存
  • 删除缓存

练习 2

实现缓存穿透解决方案:

  • 缓存空对象
  • 布隆过滤器

练习 3

实现缓存击穿解决方案:

  • 热点数据永不过期
  • Redis 互斥锁

练习 4

实现缓存雪崩解决方案:

  • 随机过期时间
  • Redis 集群
  • 熔断降级

练习 5

使用 Redis 分布式锁实现秒杀系统:

要求:

  • 防止超卖
  • UUID 唯一标识
  • Lua 原子释放锁
  • 支持高并发测试