全新Redis6全部知识点,零基础入门2

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 全新Redis6全部知识点,零基础入门

4.String数据结构实战

4.1.图形验证码存入Redis实战

  • 背景:
  • 注册-登录-修改密码一版需要发送验证码,但是容易被攻击而已调用。
  • 如何避免自己的网站被刷呢
  • 增加图形验证码
  • 单IP请求次数限制
  • 限制号码发送

(1)Kaptcha框架介绍

  • 验证码的字体/大小/颜色
  • 验证码内容的范围(数字、字母、中文汉字)
  • 验证码图片的大小,边框,边框粗细,边框颜色
  • 验证码的干扰线,验证码的样式

(2)添加Kaptcha依赖

        <!--kaptcha依赖包 (图形验证码)-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>kaptcha-spring-boot-starter</artifactId>
            <version>1.0.0</version>
        </dependency>

(3)代码配置,编写CaptchaConfig类

@Configuration
public class CaptchaConfig {
    /**
     * 验证码配置
     * Kaptcha配置类名
     *
     * @return
     */
    @Bean
    @Qualifier("captchaProducer")
    public DefaultKaptcha kaptcha() {
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        //验证码个数
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
        //字体间隔
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");
        //干扰线颜色
        //干扰实现类
        properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
        //图片样式
        properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
        //文字来源
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

(4)编写统一返回工具类

public class JsonData {
    /**
     * 状态码 0 表示成功
     */
    private Integer code;
    /**
     * 数据
     */
    private Object data;
    /**
     * 描述
     */
    private String msg;
    public JsonData(int code,Object data,String msg){
        this.data = data;
        this.msg = msg;
        this.code = code;
    }
    /**
     * 成功,不传入数据
     * @return
     */
    public static JsonData buildSuccess() {
        return new JsonData(0, null, null);
    }
    /**
     *  成功,传入数据
     * @param data
     * @return
     */
    public static JsonData buildSuccess(Object data) {
        return new JsonData(0, data, null);
    }
    /**
     * 失败,传入描述信息
     * @param msg
     * @return
     */
    public static JsonData buildError(String msg) {
        return new JsonData(-1, null, msg);
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}
z

(5)编写CommonUtil工具类(获取前台请求ip和md5方法)

public class CommonUtil {
    /**
     * 获取ip
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        return ipAddress;
    }
    public static String MD5(String data)  {
        try {
            java.security.MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();
        } catch (Exception exception) {
        }
        return null;
    }
}

(6)编写生成验证码存入Redis的逻辑

@RestController
@RequestMapping("/api/v1/captcha")
public class CaptchaController{
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private Producer captchaProduct;
    @GetMapping("/get_captcha")
    public void getCaptcha(HttpServletRequest request,HttpServletResponse response){
       /**
         * 获取随机的验证码
         */
        String captchaProducerText = captchaProducer.createText();
        String key = getCaptchaKey(request);
        //放在Redis10分钟过期
        redisTemplate.opsForValue().set(key,captchaProducerText,10, TimeUnit.MINUTES);
        BufferedImage image = captchaProducer.createImage(captchaProducerText);
        ServletOutputStream outputStream = null;
        try{
            outputStream = response.getOutputStream();
            ImageIO.write(image,"jpg",outputStream);
            outputStream.flush();
            outputStream.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    @GetMapping("/send_code")
    public JsonData sendCode(@RequestParam(value = "to",required = true)String to,
                             @RequestParam(value = "captcha",required = true) String captcha,
                             HttpServletRequest request){
        String key = getCaptchaKey(request);
        String cacheCaptcha = redisTemplate.opsForValue().get(key);
        if(cacheCaptcha != null && captcha != null && cacheCaptcha.equalsIgnoreCase(captcha)){
            //匹配通过一定要删除当前key
            redisTemplate.delete(key);
            //TODO 发送验证码逻辑
            return JsonData.buildSuccess();
        }else{
            return JsonData.buildError("图形验证码不正确");
        }
    }
    /* *
       *  获取存在缓存中的key用请求的ip和请求头,md5加密
       */
    private  String getCaptchaKey(HttpServletRequest request){
        String ip = CommonUtil.getIpAddr(request);
        String userAgent = request.getHeader("User-Agent");
        String key = "user-service:captcha:"+CommonUtil.MD5(ip+userAgent);
        return key;
    }
}



d2680d60f4b542e88e45e8e496439862.jpg


f126c1781f1241aa869ccae464f94e0a.jpge86402dc3d9b4057a8d26961b9f2a90f.jpg

4.2.高并发商品首页热点数据开发实战

(1)热点数据

  • 经常会被查询,但是不经产被修改或者删除的数据
  • 首页-详情页

(2)链路逻辑

  • 检查缓存是否存在
  • 缓存不存在则查询数据库
  • 查询数据库的结果放到缓存中,设置过期时间
  • 下次访问则命中缓存

(3)接口开发

  • 实体类编写,商品项,商品卡片
//商品卡片实体类,里面有多个商品
public class VideoCardDO {
    private  String title;
    private int id;
    private int weight;
    List<VideoDO> videoDOList;
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getWeight() {
        return weight;
    }
    public void setWeight(int weight) {
        this.weight = weight;
    }
    public List<VideoDO> getVideoDOList() {
        return videoDOList;
    }
    public void setVideoDOList(List<VideoDO> videoDOList) {
        this.videoDOList = videoDOList;
    }
}
//商品实体类
public class VideoDO {
    private int id;
    private String title;
    private String img;
    private int price;
    public VideoDO() {
    }
    public VideoDO(String title, String img, int price) {
        this.title = title;
        this.img = img;
        this.price = price;
    }
    public VideoDO(int id, String title, String img, int price) {
        this.id = id;
        this.title = title;
        this.img = img;
        this.price = price;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getImg() {
        return img;
    }
    public void setImg(String img) {
        this.img = img;
    }
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
}
c
  • Dao层编写,这块采用模拟数据库查询
@Repository
public class VideoCardDao {
    public List<VideoCardDO> list(){
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<VideoCardDO> cardDOList = new ArrayList<>();
        VideoCardDO videoCardDO1 = new VideoCardDO();
        VideoDO videoDO1 = new VideoDO(1,"SpringCloud","xxxxxxxxxxxxx",1000);
        VideoDO videoDO2 = new VideoDO(2,"Netty","xxxxxxxxxxxxx",234);
        VideoDO videoDO3 = new VideoDO(3,"面试专题视频","xxxxxxxxxxxxx",3564);
        VideoDO videoDO4 = new VideoDO(4,"AlibabaCloud","xxxxxxxxxxxxx",123);
        VideoDO videoDO5 = new VideoDO(5,"Dubbo","xxxxxxxxxxxxx",445);
        videoCardDO1.setId(1);
        videoCardDO1.setTitle("热门视频");
        List<VideoDO> videoDOS = new ArrayList<>();
        videoDOS.add(videoDO1);
        videoDOS.add(videoDO2);
        videoDOS.add(videoDO3);
        videoDOS.add(videoDO4);
        videoDOS.add(videoDO5);
        videoCardDO1.setVideoDOList(videoDOS);
        cardDOList.add(videoCardDO1);
        VideoCardDO videoCardDO2 = new VideoCardDO();
        VideoDO videoDO6 = new VideoDO(1,"SpringCloud","xxxxxxxxxxxxx",1000);
        VideoDO videoDO7 = new VideoDO(2,"Netty","xxxxxxxxxxxxx",234);
        VideoDO videoDO8 = new VideoDO(3,"面试专题视频","xxxxxxxxxxxxx",3564);
        VideoDO videoDO9 = new VideoDO(4,"AlibabaCloud","xxxxxxxxxxxxx",123);
        VideoDO videoDO10 = new VideoDO(5,"Dubbo","xxxxxxxxxxxxx",445);
        videoCardDO1.setId(1);
        videoCardDO1.setTitle("项目实战");
        List<VideoDO> videoDOS2 = new ArrayList<>();
        videoDOS2.add(videoDO6);
        videoDOS2.add(videoDO7);
        videoDOS2.add(videoDO8);
        videoDOS2.add(videoDO9);
        videoDOS2.add(videoDO10);
        videoCardDO2.setVideoDOList(videoDOS2);
        cardDOList.add(videoCardDO2);
        return cardDOList;
    }
}
  • service层编写
public interface VideoCardService {
    List<VideoCardDO> list();
}
@Service
public class VideoCardServiceImpl implements VideoCardService {
    @Autowired
    private VideoCardDao videoCardDao;
    @Autowired
    private RedisTemplate redisTemplate;
    private static final String VIDEO_CARD_CACHE_KEY = "video:card:key";
    @Override
    public List<VideoCardDO> list() {
        Object cacheObj = redisTemplate.opsForValue().get(VIDEO_CARD_CACHE_KEY);
        if(cacheObj != null){
           return (List<VideoCardDO>)cacheObj;
        }else{
            List<VideoCardDO> list = videoCardDao.list();
            redisTemplate.opsForValue().set(VIDEO_CARD_CACHE_KEY,list,10,TimeUtil.MINUTES);
            return list;
        }
    }
}


  • Controller层
    @Autowired
    private VideoCardService videoCardService;
   /**
     * 缓存查找热点卡片
     * @return
     */
    @RequestMapping("/list_cache")
    public JsonData listCache(){
        List<VideoCardDO> list = videoCardService.list();
      return JsonData.buildSuccess(list);
    }

805cb5af8239428abfe2bbed208beef8.jpg


4.3.Redis6+Lua脚本实现原生分布式锁

(1)分布式锁简介

  • 简介:分布试锁核心知识介绍和注意事项
  • 背景:保证同一时间只有一个客户端可以对共享资源进行操作
  • 案例:优惠劵领券限制次数、商品库存超卖
  • 核心:
  • 为了防止分布式系统中的多个线程之间进行相互干扰,我们需要一种分布式协调技术来对这些进程进行调度
  • 利用互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
  • 避免共享资源并发操作导致数据问题
  • 加锁:
  • 本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
  • 分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程公用锁标记,可以用Redis、Zookeeper、MySql等都可以

c08dc03ff3c44963950a2fc47866394b.jpg

  • 设计分布式锁应该考虑的东西
  • 排他性:在分布式应用集群中,同一个方法在同一时间只能被一台机器的一个线程执行
  • 容错性:分布式锁一定能得到释放,比如客户端崩溃或者网络中断
  • 满足可重入、高性能、高可用
  • 注意分布式锁的开销、锁粒度

(2)基于Redis实现分布式锁的几种坑

  • 实现分布式锁可以用Redis、Zookeeper、MySql数据库这几种,性能最好的是Redis且最容易理解的
  • 分布式锁离不开key -value 设置
key是锁的唯一标识,一版按业务来决定命名,比如想要给一种优惠劵活动加锁,key命名为"coupon:id",value可以用固定值,比如设置成1
setnx 的含义就是SET if Not Exists,有两个参数setnx(key,value),该方法是原子性操作。
如果key不存在,则设置当前key成功,返回1。
如果当前key已经存在,则设置当前key失败,返回0。
  • 解锁del(key)
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用del(key)
  • 配置锁超时expire(key,30s)
客户端崩溃或者网络中断,资源将永会被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显示释放,这把锁也要在一定时间后自动释放。
  • 综合伪代码
methodA(){
  String key = "coupon_66"
  if(setnx(key,1) == 1){
        //注意设置时间和设置key不是原子性
    expire(key,30,TimeUnit.MILLISECONDS)
        try{
            //做对应的业务逻辑
            //查询用户是否已经领卷
        }finally{
            del(key)
        }
  }else{
        //睡眠100毫秒,然后自旋调用本方法
      methodA()
    }
}
  • 存在什么问题

多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且死机,则就是个死锁。

使用原子性命令:设置和配置过期时间 setnx|setex 
如:set key 1 ex 30 nx
redisTemplate.opsFosValue().setIfAbsent("seckill_1",success,30,TimeUnit.MILLISECONDS)

业务超时,存在其他线程误删,key30秒过期,假如线程A执行很慢超过30s,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没有执行完成,结果A把B加的锁给删掉了。


11de70d2ac5d468cab4127cc205bd721.jpg

  • 进一步细化误删
可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁,那value应该是存当前线程的标识或者uuid
methodA(){
  String key = "coupon_66"
  if(setnx(key,1) == 1){
        //注意设置时间和设置key不是原子性
    expire(key,30,TimeUnit.MILLISECONDS)
        try{
            //做对应的业务逻辑
            //查询用户是否已经领卷
        }finally{
            //删除锁操作判断是否为当前线程加的
            if(redisTemplate.get(key).equals(value)){
                //还在当前时间规定内
                del(key)
            }
        }
  }else{
        //睡眠100毫秒,然后自旋调用本方法
      methodA()
    }
}
  • 核心还是判断和删除命令不是原子性操作导致的
  • 总结
  • 加锁+配置过期时间:保证原子性操作
  • 解锁:防止误删除、也要保证原子性操作
  • 采用Lua脚本+redis,保证多个命令的原子性

(3)Lua脚本+Redis实现分布式锁的编码实现

//获取lock的值和传递的值⼀样,调⽤删除操作返回1,否则返回0
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//Arrays.asList(lockKey)是key列表,uuid是参数
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class),Arrays.asList(lockKey), uuid);
@Slf4j
@RestController
@RequestMapping("/api/v1/coupon")
public class CouponController {
    @Autowired
    private RedisTemplate redisTemplate;
    @GetMapping("/add")
    public JsonData save(@RequestParam(value = "coupon_id",required = true) int couponId){
        //防止其他线程误删
        String uuid = UUID.randomUUID().toString();
        String lockKey = "lock:coupon:"+couponId;
        lock(couponId,uuid,lockKey);
        return JsonData.buildSuccess();
    }
    private void lock(int couponId,String uuid,String lockKey) {
        Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofSeconds(30));
        log.info(uuid+"---加锁状态:"+nativeLock);
        //定义Lua脚本
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        if(nativeLock){
            //加锁成功,做相应的业务逻辑
            try{
                //核心业务逻辑
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //解锁操作
                Object result = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(lockKey), uuid);
                log.info("解锁结果:"+result);
            }
        }else{
            //加锁失败进入睡眠5s,然后在自旋调用
            try {
                log.info("加锁失败,睡眠5s,进入自旋");
                TimeUnit.MILLISECONDS.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock(couponId,uuid,lockKey);
        }
    }
}

438e5f3e35dd44c2a82ca4f86103713b.jpg

setIfAbsent():
execute():

5.List数据结构实战

5.1.昨日热销榜单实战

(1)需求:

  • 天猫每天的热销榜单,每天更新一次
  • 需要支持人工运营替换榜单的位置
  • (2)企业中流程:
  • 定时任务计算昨天那些商品出售的数量最多
  • 晚上12点到1点更新到榜单上
  • 预留一个接口,支持人工运营
  • (3)类似场景:
  • 京东:热销手机榜单、电脑榜单等
  • 百度:搜索热榜


b86bd9b2983746ce9be5d69ac3bee91b.jpg

(4)编码实战

  • 开发接口
@RequestMapping("rank")
public JsonData videoRank(){
  List<VideoDO> list = redisTemplate.opsForValue().range(RANK_KEY,0,-1);
    return JsonData.buildSuccess(list);
}
  • 测试数据
@Test
public void rankTest(){
    String RANK_KEY = "rank:video";
    VideoDO video1 = new VideoDO(3,"PaaS⼯业级微服务⼤课","xdclass.net",1099);
    VideoDO video2 = new VideoDO(5,"AlibabaCloud全家桶实战","xdclass.net",59);
    VideoDO video3 = new VideoDO(53,"SpringBoot2.X+Vue3综合实战","xdclass.net",49);
    VideoDO video4 = new VideoDO(15,"玩转23种设计模式+最近实战","xdclass.net",49);
    VideoDO video5 = new VideoDO(45,"Nginx⽹关+LVS+KeepAlive","xdclass.net",89);
    //leftPushAll向左边插入,所以放在最后一位的才是首个
    redisTemplate.opsForList().leftPush(RANK_KEY,video6,video5,video4,video3,video2,video1);
    //rightPushAll向右边插入,所以首个就是第一个
    //sTemplate.opsForList().leftPush(RANK_KEY,video1,video2,video3,video4,video5);
}


72e4cf53737d4d65b62a754b2c7e47a4.jpg




1b56d735bd3c483196430bc84af46bd2.jpg

6.Hash数据结构实战

6.1.购物车实现案例实战

(1)背景:

  • 电商购物车实现,支持买多见商品,每个商品不同数量
  • 支持高性能处理
  • (2)购物车常见的实现方式:
  • 实现方式一:存储到数据库
  • 性能存在瓶颈
  • 实现方式二:前端本地存储-localstorage,sessionstorage

  • localstorage在浏览器中存储key/value对,没有过期时间
  • sessionstorage在浏览器中存储key/value对,在关闭会话窗口后将会删除这些数据
  • 实现方式三:后端存储到缓存redis
  • 可以开启AOF持久化防止重启丢失(推荐)

(2)购物车数据结构介绍

  • 一个购物车里面,存在多个购物项
  • 所以购物车是一个双层的Map
  • Map<String,Map<String,String>>
  • 第一层Map,key是用户id
  • 第二层Map,key是购物车商品的id,值是购物项的数据

(3)对应redis里面的存储


65c18a6461c84c51bab6cf9793b7ec83.jpg

(4)编码实战

  • 实体类VideoDO、CartItemVO、CartVO
public class CartItemVO {
    /**
     * 商品id
     */
    private Integer productId;
    /**
     * 购买数量
     */
    private Integer buyNum;
    /**
     * 商品标题
     */
    private String productTitle;
    /**
     * 图片
     */
    private String productImg;
    /**
     * 商品单价
     */
    private int price;
    /**
     * 总价格
     */
    private int totalPrice;
    public Integer getProductId() {
        return productId;
    }
    public void setProductId(Integer productId) {
        this.productId = productId;
    }
    public Integer getBuyNum() {
        return buyNum;
    }
    public void setBuyNum(Integer buyNum) {
        this.buyNum = buyNum;
    }
    public String getProductTitle() {
        return productTitle;
    }
    public void setProductTitle(String productTitle) {
        this.productTitle = productTitle;
    }
    public String getProductImg() {
        return productImg;
    }
    public void setProductImg(String productImg) {
        this.productImg = productImg;
    }
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    public int getTotalPrice() {
        return totalPrice*buyNum;
    }
    public void setTotalPrice(int totalPrice) {
        this.totalPrice = totalPrice;
    }
}
public class CartVO {
    private List<CartItemVO> cartItemVOS;
    private Integer totalAmount;
    public List<CartItemVO> getCartItemVOS() {
        return cartItemVOS;
    }
    public void setCartItemVOS(List<CartItemVO> cartItemVOS) {
        this.cartItemVOS = cartItemVOS;
    }
    /**
     * 返回购物车总价格
     * @return
     */
    public Integer getTotalAmount() {
        //jdk8新语法
        return cartItemVOS.stream().mapToInt(CartItemVO::getTotalPrice).sum();
    }
    public void setTotalAmount(Integer totalAmount) {
        this.totalAmount = totalAmount;
    }
}
  • 模拟dao层,数据库根据id返回数据
@Repository
public class VideoDao {
    private static Map<Integer, VideoDO> map = new HashMap<>();
    static {
        map.put(1,new VideoDO(1,"工业级PaaS云平台SpringCloudAlibaba综合项⽬实战(完结)","https://xdclass.net",1099));
        map.put(2,new VideoDO(2,"玩转新版⾼性能RabbitMQ容器化分布式集群实战","https://xdclass.net",79));
        map.put(3,new VideoDO(3,"新版后端提效神器MybatisPlus+SwaggerUI3.X+Lombok","https://xdclass.net",49));
        map.put(4,new VideoDO(4,"玩转Nginx分布式架构实战教程 零基础到⾼级","https://xdclass.net",49));
        map.put(5,new VideoDO(5,"ssm新版SpringBoot2.3/spring5/mybatis3","https://xdclass.net",49));
        map.put(6,new VideoDO(6,"新⼀代微服务全家桶AlibabaCloud+SpringCloud实 战","https://xdclass.net",59));
    }
    /**
     * 模拟返回数据库资源
     * @param videoId
     * @return
     */
    public VideoDO findByVideoId(int videoId){
        return map.get(videoId);
    }
}
  • JsonUtil工具类开发
public class JsonUtil {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    /**
     * 对象转json字符串的方法
     * @param data
     * @return
     */
    public static String objectToJson(Object data){
        try{
            return MAPPER.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * json字符串转对象的方法
     * @param jsonData
     * @param beanType
     * @param <T>
     * @return
     */
    public static <T> T jsonToObject(String jsonData,Class<T> beanType){
        try {
            T t = MAPPER.readValue(jsonData, beanType);
            return t;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
}
  • 开发VideoCardController,购物车控制层
@RestController
@RequestMapping("/api/v1/cart")
@Slf4j
public class VideoCardController{
    @Autowired
    private VideoDao videoDao;
    @Autowired
    private RedisTemplate redisTemplate;
        /**
     * 添加到购物车
     * @param videoId
     * @param buyNum
     * @return
     */
    @RequestMapping("/add")
    public JsonData addCart(int videoId,int buyNum){
        /**
         * 获取购物车
         */
        BoundHashOperations<String, Object, Object> myCartOps = getMyCartOps();
        Object cacheObj = myCartOps.get(videoId + "");
        String result = "";
        //当购物车有这个商品,转化成字符串
        if(cacheObj != null){
            result = (String) cacheObj;
        }
        if(cacheObj == null){
            //购物车没这个商品,从数据库里拿出来,在放到缓存中
            CartItemVO cartItemVO = new CartItemVO();
            VideoDO videoDO = videoDao.findByVideoId(videoId);
            cartItemVO.setBuyNum(buyNum);
            cartItemVO.setPrice(videoDO.getPrice());
            cartItemVO.setProductId(videoDO.getId());
            cartItemVO.setProductImg(videoDO.getImg());
            cartItemVO.setProductTitle(videoDO.getTitle());
            cartItemVO.setTotalPrice(videoDO.getPrice()*buyNum);
            myCartOps.put(videoId+"", JsonUtil.objectToJson(cartItemVO));
        }else{
            //不为空就将字符串转成对象,增加商品购买数量,在转成字符串放到redis里
            CartItemVO cartItemVO = JsonUtil.jsonToObject(result, CartItemVO.class);
            cartItemVO.setBuyNum(cartItemVO.getBuyNum()+buyNum);
            myCartOps.put(videoId+"",JsonUtil.objectToJson(cartItemVO));
        }
        return JsonData.buildSuccess();
    }
   /**
     * 查看我的购物车
     * @return
     */
    @RequestMapping("/my-cart")
    public JsonData getMyCart(){
        //获取购物车
        BoundHashOperations<String, Object, Object> myCartOps = getMyCartOps();
        List<CartItemVO> cartItemVOS = new ArrayList<>();
        List<Object> itemList = myCartOps.values();
        for (Object item : itemList) {
            CartItemVO cartItemVO = JsonUtil.jsonToObject((String) item, CartItemVO.class);
            cartItemVOS.add(cartItemVO);
        }
        CartVO cartVO = new CartVO();
        cartVO.setCartItemVOS(cartItemVOS);
        return JsonData.buildSuccess(cartVO);
    }
    /**
     * 清空我的购物车
     * @return
     */
    @RequestMapping("/clear")
    public JsonData clear(){
        String cartKey = getCartKey();
        redisTemplate.delete(cartKey);
        return JsonData.buildSuccess();
    }
    /*******************通用的方法,获取购物购物车数据,获取当前key*****************/
    /**
     * 获取我的购物车通用方法
     * @return
     */
    private BoundHashOperations<String,Object,Object> getMyCartOps(){
        //获取定义在Hash里的key,指定方法拼接
        String key = getCartKey();
        //返回当前key的集合,没有则新建返回
        return redisTemplate.boundHashOps(key);
    }
    /**
     * 获取购物车的key,用前缀加上用户的id
     * @return
     */
    private String getCartKey(){
        //用户id,获取用户id,JWT解密后获取
        int userId = 88;
        String cartKey = String.format("video:cart:%s", userId);
        return cartKey;
    }
}

7.Set数据结构实战

7.1.大数据下的用户画像标签去重

(1)简介

  • 用户画像 英文User Profile,是根据用户基本属性、社会属性、行为属性、心理属性等真实信息抽象出的一个标签化的、虚拟的用户模型。“用户画像”的实质是对“人”的数字化。
  • 应用场景很多,比如个性化推荐、精准营销、金融风控、精细化运营等等,举个例子来理解用户画像的实际实用价值,我们经常用手机网购,淘宝里面的千人千面,通过“标签tag”来对用户的多维度特征进行提炼和标识,那灭个人的用户画像就需要存储,set集合就适合去重。
  • 用户画像不止针对某个人,也可以某一人群或行业的画像。

(2)案例

    /**
     * 用户画像去重
     */
    @Test
    public void userProfile(){
        BoundSetOperations operations = redisTemplate.boundSetOps("user:tags:1");
        operations.add("car","student","rich","dog","guangdong","rich");
        Set<String> set1 = operations.members();
        System.out.println(set1);
        operations.remove("dog");
        Set<String> set2 = operations.members();
        System.out.println(set2);
    }


880e71c9baf7457da0408886d76f7a16.jpg

7.2.关注、粉丝、共同好友

(1)背景

  • 社交应用里面的关注、粉丝、共同好友案例

(2)案例

public void testSet(){
  BoundSetOperations operationsLW = redisTemplate.boundSetOps("user:lw");
        operationsLW.add("A","B","C","D","E");
        System.out.println("LW的粉丝:"+operationsLW.members());
        BoundSetOperations operationsLX = redisTemplate.boundSetOps("user:lx");
        operationsLX.add("A","B","F","Z","H");
        System.out.println("LX的粉丝:"+operationsLX.members());
      //差集
        Set lwSet = operationsLW.diff("user:lx");
        System.out.println("lw的专属用户:"+lwSet);
        Set lxSet = operationsLX.diff("user:lw");
        System.out.println("lx的专属用户:"+lxSet);
      //交集
        Set intersectSet = operationsLW.intersect("user:lx");
        System.out.println("同时关注的用户:"+intersectSet);
        Set union = operationsLW.union("user:lx");
      //并集
        System.out.println("两个人的并集:"+union);
        Boolean a = operationsLW.isMember("A");
        System.out.println("用户A是否为lw的粉丝:"+a);
}

813167f6eed54d0d9c11360d84ccffd7.jpg

8.SortedSet数据结构实战

8.1.用户积分实时榜单

(1)背景

  • 用户玩游戏-积分实时榜单
  • IT视频热销实时榜单
  • 电商商品热销实时榜单
  • 一般的排行榜读多写少,可以对master进行写入操作做,然后多个slave进行读操作

(2)对象准备

public class UserPointVO {
    private String username;
    private String phone;
    public UserPointVO(String username, String phone) {
        this.username = username;
        this.phone = phone;
    }
    public UserPointVO() {
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserPointVO that = (UserPointVO) o;
        return Objects.equals(phone, that.phone);
    }
    @Override
    public int hashCode() {
        return Objects.hash(phone);
    }
}
@Test
    public void testData(){
        UserPointVO p1 = new UserPointVO("老王","13113");
        UserPointVO p2 = new UserPointVO("老A","324");
        UserPointVO p3 = new UserPointVO("老B","242");
        UserPointVO p4 = new UserPointVO("老C","542345");
        UserPointVO p5 = new UserPointVO("老D","235");
        UserPointVO p6 = new UserPointVO("老E","1245");
        UserPointVO p7 = new UserPointVO("老F","2356432");
        UserPointVO p8 = new UserPointVO("老G","532332");
        BoundZSetOperations boundZSetOperations = redisTemplate.boundZSetOps("point:rank:real");
        boundZSetOperations.add(p1,348);
        boundZSetOperations.add(p2,18);
        boundZSetOperations.add(p3,328);
        boundZSetOperations.add(p4,848);
        boundZSetOperations.add(p5,98);
        boundZSetOperations.add(p6,188);
        boundZSetOperations.add(p7,838);
        boundZSetOperations.add(p8,8828);
    }

(3)接口开发

  • 返回榜单-从大到小排序
    /**
     * 返回全部榜单从大到小
     * @return
     */
    @RequestMapping("/real-rank2")
    public JsonData rankList2(){
        Set set = redisTemplate.boundZSetOps("point:rank:real").reverseRange(0, -1);
        return JsonData.buildSuccess(set);
    }
  • 返回榜单-从小到大排序
    /**
     * 返回全部榜单从小到大
     * @return
     */
    @RequestMapping("/real-rank1")
    public JsonData rankList1(){
        Set range = redisTemplate.boundZSetOps("point:rank:real").range(0, -1);
        return JsonData.buildSuccess(range);
    }
  • 查询个人用户排名
    /**
     * 查询个人用户排名
     * @param username
     * @param phone
     * @return
     */
    @RequestMapping("find_my_rank")
    public JsonData find(String username,String phone){
        UserPointVO userPointVO = new UserPointVO(username,phone);
        Long rank = redisTemplate.boundZSetOps("point:rank:real").reverseRank(userPointVO);
        return JsonData.buildSuccess(++rank);
    }
  • 查看个人积分
    /**
     * 查看个人积分
     * @param username
     * @param phone
     * @return
     */
    @RequestMapping("find_my_score")
    public JsonData findMyScore(String username,String phone){
        UserPointVO userPointVO = new UserPointVO(username,phone);
        Double score = redisTemplate.boundZSetOps("point:rank:real").score(userPointVO);
        return JsonData.buildSuccess(score);
    }
  • 个人加积分
  /**
     * 加积分
     * @param username
     * @param phone
     * @return
     */
    @RequestMapping("add_score")
    public JsonData addScore(String username,String phone){
        UserPointVO userPointVO = new UserPointVO(username,phone);
        redisTemplate.boundZSetOps("point:rank:real").incrementScore(userPointVO,1000000);
        return JsonData.buildSuccess(redisTemplate.boundZSetOps("point:rank:real").reverseRange(0,-1));
    }


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
4月前
|
存储 NoSQL Java
深入学习Redis:从入门到实战
深入学习Redis:从入门到实战
|
5月前
|
NoSQL Linux Redis
Redis基础入门实践详解
Redis基础入门实践详解
51 1
|
7月前
|
存储 NoSQL Java
Redis基础入门
Redis基础入门
38899 11
|
7月前
|
NoSQL Linux Redis
[笔记]Redis入门-基本使用
[笔记]Redis入门-基本使用
|
9月前
|
存储 NoSQL 算法
Redis完全教程:全面学习指南(三)
Redis完全教程:全面学习指南(三)
49 0
Redis完全教程:全面学习指南(三)
|
9月前
|
存储 缓存 NoSQL
Redis完全教程:全面学习指南(六)
Redis完全教程:全面学习指南(六)
49 0
|
9月前
|
监控 NoSQL 关系型数据库
Redis完全教程:全面学习指南(四)
Redis完全教程:全面学习指南(四)
33 0
|
9月前
|
存储 监控 负载均衡
Redis完全教程:全面学习指南(五)
Redis完全教程:全面学习指南(五)
56 0
|
9月前
|
存储 SQL 缓存
Redis完全教程:全面学习指南(一)
Redis完全教程:全面学习指南
61 1
Redis完全教程:全面学习指南(一)
|
9月前
|
存储 NoSQL 安全
Redis完全教程:全面学习指南(二)
Redis完全教程:全面学习指南(二)
41 0