Redis入门+排行榜+购物车+锁+Cache缓存

Redis缓存

  • 为什么使用redis
  • redis有什么作用
  • 缓存由什么操作的
1.为DB缓存,减轻DB服务器的压力,本质上重请求不在到DB中而是到缓存中

2.缓存是系统快速响应的关键技术之一,以空间换时间的一种技术

3.Redis一款高性能的缓存技术,他是非关系型数据库

缓存的优劣

  • 优势:

    • 提供用户体验度
    • 减轻服务器DB压力
    • 提升系统性能
  • 劣势:

    • 硬件成本高
    • 缓存击穿,穿透,雪崩
    • 缓存与数据库同步
    • 缓存并竞争

缓存中数据类型

基本数据类型

list 关注列表,粉丝列表
set 共同关注,共同喜欢
string 记录关注数,粉丝数
sortedSet 使用跳表的的结构高效查询
hash 用于存储用户信息

特殊数据类型

GEO 可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作
Hyperloglog Hyperloglog基数统计的算法。不过会有些许误差,如果允许容错,使用Hyperloglog,不允许容错的话使用set
Bitmap Bitmap就是通过一个bit位来表示某个元素对应的值或者状态。Bitmaps位图,只有0和1两个状态。 位存储。可以用来统计用户信息,登陆,未登录;打卡。

Redis中过期删除策略

set  username king  PX(秒)  6000   NX(没有则创建) XX (有则创建)

定期删除:

所谓定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。
假设redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那redis基本上就死了,
cpu负载会很高的,消耗在你的检查过期key上了。
注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。
实际上redis是每隔100ms随机抽取一些key来检查和删除的.

惰性删除

所以就是惰性删除。这就是说,在你获取某个key的时候,redis会检查一下,
这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下。

# 缓存

  • 什么是缓存

    • 缓存就是在服务器端中客户端请求落到数据库中,造成的数据库压力过大。而缓存就是解决数据库压力大 的临时仓库
  • 缓存的执行流程

    • 客户端请求到服务器中,服务器首先请求到客户端的本地缓存中判断是否存在数据库,不存在则去数据库中查询,若是存在则在缓存中查询
  • 缓存的作用

    • 减轻数据库访问的压力,从而让用户更方便,更快捷的得到想要的数据

Redis各种数据类型操作干货…………………………………………

Set实现排行榜功能

package cn.chy.com.controller;

import com.alibaba.fastjson.JSON;
import io.lettuce.core.dynamic.annotation.Command;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @Description: TODO
 * @author: 颜景琦
 * @date: 2021年11月26日 16:34
 */
@RestController
public class RedisController {

    @Autowired
    private RedisTemplate redisTemplate;

    public static final String SCORE_RANK = "score_rank";

    /**
     * 批量新增
     */
    @GetMapping("/info")
    public String goInfo() {

        // redisTemplate.opsForValue().set("23","2432");
        Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            DefaultTypedTuple<String> tuple = new DefaultTypedTuple<>("张三" + i, 1D + i);
            tuples.add(tuple);
        }
        System.out.println("循环时间:" + (System.currentTimeMillis() - start));
        Long num = redisTemplate.opsForZSet().add(SCORE_RANK, tuples);
        System.out.println("批量新增时间:" + (System.currentTimeMillis() - start));
        System.out.println("受影响行数:" + num);

        return "1";
    }

    //    private RedisTemplate redisTemplate;
//
//    public static final String SCORE_RANK = "score_rank";
//
//    @Test
//    public void contextLoads() {
//        // redisTemplate.opsForValue().set("23","2432");
//        Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
//        long start = System.currentTimeMillis();
//        for (int i = 0; i < 100000; i++) {
//            DefaultTypedTuple<String> tuple = new DefaultTypedTuple<>("张三" + i, 1D + i);
//            tuples.add(tuple);
//        }
//        System.out.println("循环时间:" + (System.currentTimeMillis() - start));
//        Long num = redisTemplate.opsForZSet().add(SCORE_RANK, tuples);
//        System.out.println("批量新增时间:" + (System.currentTimeMillis() - start));
//        System.out.println("受影响行数:" + num);
//    }

    /**
     * 获取排行列表
     */
    @GetMapping("getList")
    public Map<String, Object> selectList() {
        Map<String, Object> map = new HashMap<>();
        Set<String> range = redisTemplate.opsForZSet().reverseRange(SCORE_RANK, 0, 10);
        System.out.println("获取到的排行列表:" + JSON.toJSONString(range));
        Set<ZSetOperations.TypedTuple<String>> rangeWithScores =
                redisTemplate.opsForZSet().reverseRangeWithScores(SCORE_RANK, 0, 10);
        System.out.println("获取到的排行和分数列表:" + JSON.toJSONString(rangeWithScores));
        map.put("range", range);
        map.put("rangeWithScores", rangeWithScores);

        return map;
    }

    /**
     * 单个新增
     */
    @GetMapping("add")
    public String add() {

        redisTemplate.opsForZSet().add(SCORE_RANK, "颜淮川", 100);
        return "";
    }

    /**
     * 获取单个的排行
     */
    @GetMapping("find")
    public Map<String, Object> findList() {
        Map<String, Object> map = new HashMap<>();
        Long rankNum = redisTemplate.opsForZSet().reverseRank(SCORE_RANK, "颜淮川");
        System.out.println("李四的个人排名:" + rankNum);

        Double score = redisTemplate.opsForZSet().score(SCORE_RANK, "颜淮川");
        System.out.println("李四的分数:" + score);
        map.put("range", rankNum);
        map.put("rangeWithScores", score);
        return map;
    }

    /**
     * 统计两个分数之间的人数
     */
    @GetMapping("get")
    public String count() {
        Long count = redisTemplate.opsForZSet().count(SCORE_RANK, 8001, 9000);
        return "统计8001-9000之间的人数:" + count;
    }


    /**
     * 获取整个集合的基数(数量大小)
     */
    @GetMapping("card")
    public String zCard() {
        Long aLong = redisTemplate.opsForZSet().zCard(SCORE_RANK);
        return "集合的基数为:" + aLong;
    }

    /**
     * 使用加法操作分数
     */
    @GetMapping("incrementScore")
    public String incrementScore() {
        Double score = redisTemplate.opsForZSet().incrementScore(SCORE_RANK, "颜淮川", 1000);
        return "李四分数+1000后:" + score;
    }
}

Hash实现购物车功能

package cn.chy.com.shopproject.controller;

import cn.chy.com.shopproject.util.JwtUtils;
import cn.chy.com.shopproject.util.RedisUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.api.ApiController;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.chy.com.shopproject.pojo.Shop;
import cn.chy.com.shopproject.service.ShopService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;

import org.springframework.beans.BeanUtils;
import cn.chy.com.shopproject.dto.ShopDTO;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.chy.com.shopproject.vo.CommonsResponse;
import io.swagger.annotations.ApiImplicitParam;
import org.springframework.web.client.RestTemplate;

/**
 * (Shop)表控制层
 *
 * @author 颜景琦
 * @since 2021-11-07 19:44:59
 */
@RestController
@RequestMapping("shop")
@Api(description = "Shop控制层")
@CrossOrigin
public class ShopController {
    /**
     * 服务对象
     */
    @Resource
    private ShopService shopService;

    @Resource
    private RedisUtil redisUtil;

    /**
     * 分页查询所有数据
     *
     * @return 所有数据
     */
    @GetMapping("/list")
    @ApiOperation(value = "分页查询所有数据", produces = "application/json;charset=utf-8",
            httpMethod = "GET", response = CommonsResponse.class)
    public List<Shop> selectAll(@RequestParam String jwtId) {
        System.out.println(jwtId);

        QueryWrapper<Shop> queryWrapper = new QueryWrapper<>();

        // 获取jwt中id
        String memberIdByJwtToken = JwtUtils.getMemberIdByJwtToken1(jwtId);


        System.out.println(memberIdByJwtToken);
        queryWrapper.eq("userId", memberIdByJwtToken);

        return shopService.list(queryWrapper);

    }

    @GetMapping("/saveCart")
    @ApiOperation(value = "分页查询所有数据", produces = "application/json;charset=utf-8",
            httpMethod = "GET", response = CommonsResponse.class)
    public boolean saveCart(
            @RequestParam String id,
            @RequestParam String nums) {

        Shop byId = shopService.getById(id);
        boolean carts = redisUtil.hset("carts", id, byId);

        Long count = redisTemplate.opsForHash().lengthOfValue("carts", id);
        if (count < 1) {
            return false;
        }

        redisTemplate.opsForHash().increment("carts", id, Long.parseLong(nums));
        return carts;
    }

    @Resource
    private RedisTemplate redisTemplate;


    @GetMapping("/getCart")
    @ApiOperation(value = "分页查询所有数据", produces = "application/json;charset=utf-8",
            httpMethod = "GET", response = CommonsResponse.class)
    public Map<String, Object> getCart(@RequestParam String jwtId) {
        Map<String, Object> map = new HashMap<>();

        List<Shop> shops = new ArrayList<>();

        // 遍历结合
        List<Shop> cards = redisTemplate.opsForHash().values("carts");
        for (Shop card : cards) {
            // 根据指定用户id进行判断
            if (!card.getUserId().equals(JwtUtils.getMemberIdByJwtToken1(jwtId))) {
                shops.add(card);
                continue;
            }
        }

        map.put("cardsList", shops);
        map.put("cardsNum", shops.size());
        return map;
    }
}

Set类型做抽奖系统

package cn.chy.com.shopproject.controller;

import cn.chy.com.shopproject.util.RedisUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Description: 抽奖系统
 * @author: 颜景琦
 * @date: 2021年12月05日 15:42
 */
@RestController
@CrossOrigin
public class LuckyDrawController {

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private RedisTemplate redisTemplate;


    /*
        获取所有抽奖的属性
     */
    @GetMapping("getInfo")
    public Object getInfo() {
        return redisTemplate.opsForSet().members("userInfoLucky");
    }

    /*
        新增抽奖信息
     */
    @PostMapping
    public boolean saveInfo(String name) {

        redisTemplate.opsForSet().add("userInfoLucky", name);
        
        return false;
    }

    /*
        开始抽奖
        保存原有的值
     */
    @GetMapping("getLucky")
    public Object getLucky() {
        // 默认抽取一个 参数后面加数字可以抽取多个
        return redisTemplate.opsForSet().randomMember("userInfoLucky");
    }

    /*
       开始抽奖
       不保存值
    */
    @GetMapping("getDraw")
    public Object getDraw() {
        // 默认抽取一个 参数后面加数字可以抽取多个
        return redisTemplate.opsForSet().pop("userInfoLucky", 2);
    }
}


缓存穿透

  • 什么是缓存穿透

    • 缓存穿透是在缓存和数据库中都没有数据库从而客户端的大量请求落入到数据库中造成缓存穿透

    • 原因是,读取缓存没有数据,数据库中也取不到数据

  • 如何解决缓存穿透

解决方案:
	1.
	对缓存结果进行空置处理,并且追加上缓存时间(设置短一些),又可以在key对应数据insert了后清除缓存。 但是空值缓存也会浪费一部分内存空间
	
	2.
	布隆过滤器,他的原理是:当某一个元素被添加到集合时,通过k这个hash函数将这个元素的映射成二进制向量列表,检索时,判断是中的值是否是1,如果是则表示数据库不存在如果这一条节点中任何一个0的操作都可以被识别为不存在数据库中。 这就是布隆过滤器的基本思想
	问题:由于是多个hash映射可能会存在一致的状况,那么这时候就要扩充元素空间
	
缺点:会产生误报率,集合长度和随机函数的数量决定报错率,而元素的删除是比较复杂的。
Category category = null;
try {
    // 读锁
    rLock.lock();
    // 互斥锁
    lock.lock();
    category =
        (Category) redisUtil.get("categoryInfo_" + id);
    if (category != null) {
        return category;
    }
    // 按照id查询
    category = this.getById(id);
    int time = -1;
    if (category == null) {
        category = Category.builder().catId(id).build();
        time = 10;
    }
    // 设置 redis内容
    redisUtil.set("categoryInfo_" + id, category, time);
} finally {
    try {
        // 释放互斥锁
        lock.unlock();

    } finally {
        // 释放读锁
        rLock.unlock();
    }
}
return category;

缓存雪崩

  • 什么是缓存雪崩

    在同一段时间的内reids中大量的key过期的了,而查询的数据库量特别大,引起数据库压力过大,甚至down级,和缓存击穿不同的是,缓存击穿是在在同一个时间下并发查询同一条数据,缓存雪崩不同,在大批量的key过期的时候所有的查询都指往数据库倒置宕机。
    
  • 缓存雪崩的解决策略

数据key的过期时间设置偏长,或者时间随机,在这就是永不过期
数据库是分布式 集群部署,将热点事件数据均匀部分在不同的缓存中。
设置热点数据永不过期

缓存击穿

  • 什么是缓存击穿
缓存击穿是在redis中 缓存中没有在数据库中是有的,在此时用户并发特别多,由于同时没有读取缓存中的数据,请求进入数据库,引起数据库的压力剧增 差生缓存击穿

高并发情况下,缓存中没有数据从数据库中读取,但是并发 场景下,线程1读取到数据库中数据,线程2也在请求没有读取到缓存数据,进入数据库依次类推到n,因此产生了并发问题
  • 缓存击穿的解决策略
1.设置热点数据过期时间偏长,或者永不过期
2.加上互斥锁, 线程2 必须等待线程1执行完成后,再次从缓存中查询,如果有就拿缓存没有数据库
RLock lock = redissonClient.getLock(LockConstants.CATEGORY_LOCK_ID);
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(LockConstants.CATEGORY_LOCK_R_W);
RLock rLock = readWriteLock.readLock();
Category category = null;
try {
    // 读锁
    rLock.lock();
    // 互斥锁
    lock.lock();
    category =
        (Category) redisUtil.get("categoryInfo_" + id);
    if (category != null) {
        return category;
    }
    // 按照id查询
    category = this.getById(id);
    int time = -1;
    if (category == null) {
        category = Category.builder().catId(id).build();
        time = 10;
    }
    // 设置 redis内容
    redisUtil.set("categoryInfo_" + id, category, time);
} finally {
    try {
        // 释放互斥锁
        lock.unlock();

    } finally {
        // 释放读锁
        rLock.unlock();
    }
}
return category;

Redis 分布式锁

1.本地锁

客户端访问,到本地服务中并上锁,访问redis(redis没有进入) > 数据库

分布式锁

  1. 方案1

    • 属性为(有则不创建)nx 没有就创建,之后设置过期时间
  2. 方案2

    • 在操作的时候自定义一个lock key拼接uuid, 在设置过程中如果uuid正确表示 删除锁,不正确就不删除
  3. 方案3

    • 使用lua脚本将redis语句原子化

Redisson

Redisson中可重入锁(分布式锁)

  • 可重入锁就是某个线程可以再次获取锁而不出现死锁。可重入锁的意义在与放置死锁

sync和reentrantlock都是可重入锁

  • redisson自动的锁名师my-lock

  • 调用lock 默认加锁时间是30s 如果超过时间30s redisson会自动给锁续上30s 业余执行完毕不会续加时间,如果没有手动释放锁,默认30s自动删除

  • lock.unlock 释放锁

分布式锁

注入RedissonClient对象

RLock lock= redissonClient.getLock("my-lock");// 默认是my-lock

// 上锁
lock.lock();

// 是否锁
lock.unlock()

读写锁


注入RedissonClient对象
    
RReadWriteLock readWrite = redissonClient.getReadWriteLock("rw-lock");

RLock rlock = readWrite.writeLock() || readWrite.readLock()  // 返回一个锁对象
    

// 上锁 默认时间是30s 若是业务执行超过30s会自动续时
rlock.lock();

// 释放锁  是否过程中上锁时间是不会续时的
// 读写锁最先释放
rlock.uhLock();

读写锁核心

1. 写+写: 必须等待前一个写锁执行完毕
1. 写+读: 必须等待前一个写锁执行完毕
1. 读+写: 必须等待前一个读锁执行完毕
1. 读+读: 两种没有互斥关系,没锁的限制

信号量:

使用release 方法释放信号量,redis中key值+1

使用acquire 方法获取信号量,redis中key值-1 key值为0等待

使用 tryacquire 获取信号量 ,redis中key值-1 key值为0不等待

例子:

有一个车库3个车位,
场景描述:停车场有3个车位 存在redis的park key为3
调用park方法一次 redis的park -1 当park变成0时,park()方法就无法占用成功
调用 go方法一次 redis的park就 +1
    

主要使用的还是其中的线程同步关系

数据库缓存数据库不一致处理:

  1. 数据库与缓存双写

  2. 更新数据库后删除缓存

    问题

    // 当读请求过来以后有数据则查询并显示
    // 同时update数据库
    // del了缓存
    // 在并发场景下del后读取的还是上一回数据,由于不能达到最终一致性问题所以 获取数据会有误差
    
    

解决方案

- 缓存设置过期时间和nx
- 使用分布式锁
- canal中间件读取binlog日志
- 使用消息队列方式提供者向消费者发送消息,消费者消费消息后同步数据库最后将原子的操作返回给提供者

SpringCache 缓存

解决套餐:
    
    1.缓存穿透解决了
    
    2.缓存击穿:解决了不是特别好,实现使用了sync方式 只能锁当前的jvm虚拟机
    
    3.缓存雪崩解决了
    
数据库缓存没有解决一致性问题
    
springcache 适用于读多写少, 如果是写,建议使用原生的

缓存注解的使用:

  • @Cacheable 标注类和注解都是支持缓存的 下次访问spring根据标记找到缓存的方法

  • @CachePut的使用:用于在数据库更新层面上使用

  • @CatchEvict : 清除指定条件的缓存

  • @CatchConfig: 可以添加在类上 这个类的所有方法的缓存名称都是配置的名称

// 使用缓存 key是参数,sync是否异步
@Cacheable(value = "skuOne",key = "#skuId",sync = true)
// 清除cache中key的skuId的缓存信息
@CacheEvict(value = "skuOne",key = "#skuId")

Canal 解决数据不一致问题

Canal 是什么

他是阿里的
主要目的监听的数据库的通过,从而让数据或或者DB修改

Canal同步原理

canal 模拟 mysql slave的交互协议,伪装自己为mysql salve 
mysql master 收到dump请求,开始推送binary log 给slave
canal解析binary log 对象实现同步
posted @ 2021-12-25 16:40  淮川  阅读(271)  评论(0)    收藏  举报