Redis笔记(黑马点评)


实战篇-10.短信登录-基于Redis:实现短信登录

1.自己构建的构造函数使用StringRedisTemplate,不能使用依赖注入

需要在原来的构造函数里注入StringRedisTemplate

2.将Long数据类型转化为String存储到Map中

Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
              CopyOptions.create()
                      .setIgnoreNullValue(true)
                      .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

实战篇-11.短信登录-解决状态登录刷新的问题

使用两个拦截器,一个登录拦截器,一个token刷新的拦截器,使用order排序执行顺序

// 登录拦截器
     registry.addInterceptor(new LoginInterceptor())
             .excludePathPatterns(
                     "/shop/**",
                     "/voucher/**",
                     "/shop-type/**",
                     "/upload/**",
                     "/blog/hot",
                     "/user/code",
                     "/user/login"
             ).order(1);
     // token刷新的拦截器
     registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);

实战篇-商户查询缓存-02.添加商户缓存

实战篇-商户查询缓存-03.缓存练习题分析

@Override
    public Result queryTypeList() {
        String key="shop-type";
        Long size=stringRedisTemplate.opsForList().size(key);
        // 1. 从redis中查询商铺类型列表
        List<String> categoryList=stringRedisTemplate.opsForList().range(key,0,size);
        // 2. 查询成功,直接将结构放到List中返回
        if(categoryList.size()!=0){
            List<ShopType> arr=new ArrayList<>();
            for (int i = 0; i <size ; i++) {
                 String cacheCatogery=categoryList.get(i);
                 arr.add(JSONUtil.toBean(cacheCatogery,ShopType.class));
            }
            return  Result.ok(arr);
        }
        // 3. 不存在,查询数据库
        List<ShopType> typeList = query().orderByAsc("sort").list();
        // 4. 不存在返回错误
       if(typeList.size()==0){
           return Result.fail("没有该数据");
       }
       //5.存在写入Redis
        for(ShopType shopType:typeList){
            stringRedisTemplate.opsForList().rightPush(key,JSONUtil.toJsonStr(shopType));
        }
        return  Result.ok(typeList);
    }

实战篇-商户查询缓存-05.实现商铺缓存与数据库的双写一致性

修改ShopController中的业务逻辑,满足下面的需求:
①根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
②根据id修改店铺时,先修改数据库,再删除缓存

@Override
@Transactional
public Result update(Shop shop) {
    Long id = shop.getId();
    if (id == null) {
        return Result.fail("店铺id不能为空");
    }
    // 1.更新数据库
    updateById(shop);
    // 2.删除缓存
    stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
    return Result.ok();
}

缓存穿透

存取null值的解决方法

@override
public Result queryById(Long id){
String key CACHE_SHOP_KEY+id;
  //1.从redis查询商铺缓存
String shopJson stringRedisTemplate.opsForValue().get(key);
  //2,判断是否存在
if (Strutil.isNotBlank(shopJson)){
  //3.存在,直接返回
    Shopshop JSONUtil.toBean(shopJson,Shop.class);
    return Result.ok(shop);
}
   //判断命中的是否是空值
    if (shopJson!=null){
    //返回一个错误信息
    return Result,fail("店铺信息不存在!");
    }
    //4.不存在,根据id查询数据库
    Shop shop getById(id);
    //5.不存在,返回错误
    if (shop =null)
    //将空值写入redis    stringRedisTemplate.opsForValue().set(key,value:""CACHE_NULL_TTL,TimeUnit.MINUTES);
    //返回错误信息
    return Result.fail("店铺不存在!");
   }
    //6,存在,写入redis   stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonstr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
    //7.返回
    return Result.ok(shop);
}
缓存穿透产生的原因是什么?

用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

缓存穿透的解决方案有哪些?
  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

互斥锁方法解决缓存击穿
    @Override
    public Result queryById(Long id) {      
        // 互斥锁解决缓存击穿
        Shop shop = cacheClient.queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
       
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }
    
    public queryWithMutex(Long id){
             string key CACHE_SHOP_KEY +id;
            //1,从redis查询商铺缓存
            String shopJson stringRedisTemplate.opsForvalue().get(key);
            //2,判断是否存在
            if (Strutil.isNotBLank(shopJson)){
                //3.存在,直接返回
                return JSONUtil.toBean(shopJson,Shop.class);
            }
            //判断命中的是否是空值
            if (shopJson !null){
            //返回一个销误信息
            return null;
            }            
            //4,实现缓存重建      
            //4.1.获取互斥锁
            String lockKey ="lock:shop":id;
            Shop shop null;
            try{
            boolean isLock tryLock(lockKey);
            /4,2,判断是否获取成功
            if(!isLock){
                //4.3.失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
              }
            //4.4,成功,根据1d查询数掘库
            shop getById(id);
            //5.不存在,返回销误
            if (shop =null){
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key,value:""CACHE_NULL_TTL,TimeUnit.MINUTES);
            //返回错误信,息
            return null;
            }
            //6,存在,写入redis
            stringRedisTemplate.opsForValue().set key,JSONUtil.toJsonstr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
            catch (InterruptedException e){
            throw new RuntimeException(e);
            }finally
            //7.释放互斥锁
            unlock(lockKey);
    }
    
    //加互斥锁
 private boolean tryLock(String key){
     Boolean flag=stringRedisTemplate.opsForValue()
                      .setIfAbsent(key,value:"1,timeout:10,TimeUnit.SECONDS);
       return BooleanUtil.isTrue(flag);
}
//释放互斥锁
private void unlock(String key){
  stringRedisTemplate.delete(key);
}

逻辑过期方式解决缓存击穿
```

![](https://zscblog.oss-cn-hangzhou.aliyuncs.com/img/image-20230805001321944.png)

### P46实战篇-商户查询缓存-12.封装Redis工具类

@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}


逻辑过期
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
    // 设置逻辑过期
    RedisData redisData = new RedisData();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    // 写入Redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}

// 解决缓存穿透

public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}

    // 4.不存在,根据id查询数据库
    R r = dbFallback.apply(id);
    // 5.不存在,返回错误
    if (r == null) {
        // 将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 返回错误信息
        return null;
    }
    // 6.存在,写入redis
    this.set(key, r, time, unit);
    return r;
}

// 逻辑过期解决缓存击穿
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}

// 互斥锁解决缓存击穿
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}

    // 4.实现缓存重建
    // 4.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    R r = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2.判断是否获取成功
        if (!isLock) {
            // 4.3.获取锁失败,休眠并重试
            Thread.sleep(50);
            return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
        }
        // 4.4.获取锁成功,根据id查询数据库
        r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        // 7.释放锁
        unlock(lockKey);
    }
    // 8.返回
    return r;
}

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

### 全局ID生成器

@Component
public class RedisIdWorker {
/**
* 开始时间戳
/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/
*
* 序列号的位数
*/
private static final int COUNT_BITS = 32;

private StringRedisTemplate stringRedisTemplate;

public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
}

public long nextId(String keyPrefix) {
    // 1.生成时间戳
    LocalDateTime now = LocalDateTime.now();
    long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    long timestamp = nowSecond - BEGIN_TIMESTAMP;

    // 2.生成序列号
    // 2.1.获取当前日期,精确到天
    String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    // 2.2.自增长
    long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

    // 3.拼接并返回
    return timestamp << COUNT_BITS | count;
}

}


### 实现优惠券秒杀下单

private void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
// 创建锁对象
RLock redisLock = redissonClient.getLock(“lock:order:” + userId);
// 尝试获取锁
boolean isLock = redisLock.tryLock();
// 判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
log.error(“不允许重复下单!”);
return;
}

    try {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            log.error("不允许重复下单!");
            return;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足!");
            return;
        }

        // 7.创建订单
        save(voucherOrder);
    } finally {
        // 释放锁
        redisLock.unlock();
    }
}

![](https://zscblog.oss-cn-hangzhou.aliyuncs.com/img/image-20230805171054030.png)

### 超卖问题

使用乐观锁解决超卖问题

            // 6.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock - 1") // set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0

private void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
// 创建锁对象
RLock redisLock = redissonClient.getLock(“lock:order:” + userId);
// 尝试获取锁
boolean isLock = redisLock.tryLock();
// 判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
log.error(“不允许重复下单!”);
return;
}

    try {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            log.error("不允许重复下单!");
            return;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足!");
            return;
        }

        // 7.创建订单
        save(voucherOrder);
    } finally {
        // 释放锁
        redisLock.unlock();
    }
}

### 一人一单

先获取锁再下订单

Long userId UserHolder.getUser().getId();
synchronized (userId.tostring().intern()){
return createVoucherorder(voucherId);
}


下单
@Transactional
public Result createVoucherOrder(Long voucherId) {
    // 5.一人一单
    Long userId = UserHolder.getUser().getId();

    synchronized (userId.toString().intern()) {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    }
}

但是在高并发情况下会出现安全问题



### 使用分布式锁实现一人一单





### 使用Redis实现分布式锁

![](https://zscblog.oss-cn-hangzhou.aliyuncs.com/img/image-20230805200850006.png)

- 获取锁

#添加锁,NX是互斥、EX是设置超时时间
SET lock thread1 NX EX 10


- 释放锁

#释放锁,删除即可
DEL key


### 基于Redis实现分布式锁初级版本

@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail(“秒杀尚未开始!”);
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail(“秒杀已经结束!”);
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail(“库存不足!”);
}
Long userId = UserHolder.getUser().getId();
//创建锁对象(新增代码)
SimpleRedisLock lock = new SimpleRedisLock(“order:” + userId, stringRedisTemplate);
//获取锁对象
boolean isLock = lock.tryLock(1200);
//加锁失败
if (!isLock) {
return Result.fail(“不允许重复下单”);
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}

private static final String KEY_PREFIX=”lock:”
@Override
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() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}


###  利用Java代码调用Lua脚本改造分布式锁

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

public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}



  目录