Redis的list专题

一、定义

        List类型是一个双端链表的结构,容量是2的32次方减1个元素,即40多亿个;其主要功能有push、pop、获取元素等;一般应用在栈、队列、消息队列等场景。

二、redis 常用的命令

1)[LR]PUSH key value1 [value2 ...]

//lpush  左侧插入,插入的数据以堆的形式体现

本地:0>lpush  lxh  1 2  3 4 5 6
"6"

本地:0>lrange lxh 0 -1
 1)  "6"
 2)  "5"
 3)  "4"
 4)  "3"
 5)  "2"
 6)  "1"

// RPUSH key value1 [value2 ...]   右侧插入,插入的数据以对列的形式体现,先插入的前面

本地:0>rpush  lxh1   1 2  3 4 5 6
"6"

本地:0>lrange lxh1 0  -1
 1)  "1"
 2)  "2"
 3)  "3"
 4)  "4"
 5)  "5"
 6)  "6"

2、LINSERT key BEFORE pivot value       在列表前插入元素

3、LINSERT key  AFTER   pivot value       在列表后插入元素

本地:0>lrange lxh  0  -1
 1)  "6"
 2)  "5"
 3)  "4"
 4)  "3"
 5)  "2"
 6)  "A"
 7)  "C"
 8)  "1"
 9)  "B"

本地:0>linsert  lxh  before 6 6A  
"10"

本地:0>lrange  lxh 0 -1
 1)  "6A"
 2)  "6"
 3)  "5"
 4)  "4"
 5)  "3"
 6)  "2"
 7)  "A"
 8)  "C"
 9)  "1"
 10)  "B"


本地:0>linsert  lxh after 6 6B
"11"

本地:0>lrange   lxh  0  -1
 1)  "6A"
 2)  "6"
 3)  "6B"
 4)  "5"
 5)  "4"
 6)  "3"
 7)  "2"
 8)  "A"
 9)  "C"
 10)  "1"
 11)  "B"

4、LLEN key         获取列表长度

本地:0>lrange lxh  0  -1
 1)  "6A"
 2)  "6"
 3)  "6B"
 4)  "5"
 5)  "4"
 6)  "3"
 7)  "2"
 8)  "A"
 9)  "C"
 10)  "1"
 11)  "B"


本地:0>llen lxh
"11"

5、LINDEX key index           通过索引获取列表中的元素

本地:0>lrange lxh  0 -1
 1)  "6A"
 2)  "6"
 3)  "6B"
 4)  "5"
 5)  "4"
 6)  "3"
 7)  "2"
 8)  "A"
 9)  "C"
 10)  "1"
 11)  "B"


本地:0>lindex lxh 1
"6"

6、LSET key index value    通过索引设置列表元素

本地:0>lrange lxh 0 -1
 1)  "6A"
 2)  "6"
 3)  "6B"
 4)  "5"
 5)  "4"
 6)  "3"
 7)  "2"
 8)  "A"
 9)  "C"
 10)  "1"
 11)  "B"

本地:0>lset lxh 0 6A--修改
"OK"

本地:0>lrange  lxh  0  -1
 1)  "6A--修改"
 2)  "6"
 3)  "6B"
 4)  "5"
 5)  "4"
 6)  "3"
 7)  "2"
 8)  "A"
 9)  "C"
 10)  "1"
 11)  "B"

7、LTRIM key start end     -------截取队列指定区间内的元素,其余元素都删除

本地:0>lrange  lxh  0  -1
 1)  "6A--修改"
 2)  "6"
 3)  "6B"
 4)  "5"
 5)  "4"
 6)  "3"
 7)  "2"
 8)  "A"
 9)  "C"
 10)  "1"
 11)  "B"

本地:0>ltrim lxh 0  1
"OK"

本地:0>lrange lxh  0  -1
 1)  "6A--修改"
 2)  "6"

8、LREM key count value--------- 移除列表元素(从列表前面开始计算移除)

本地:0>lrange lxh  0 -1
 1)  "6"
 2)  "6"
 3)  "6"
 4)  "6A--修改"
 5)  "6"

本地:0>lrem  lxh  2 6
"2"

本地:0>lrange  lxh  0  -1
 1)  "6"
 2)  "6A--修改"
 3)  "6"

9、LPOP key ------从队列的头弹出节点元素(返回该元素并从队列中删除)

本地:0>lrange lxh 0 -1
 1)  "6"
 2)  "6A--修改"
 3)  "6"
 4)  "1"
 5)  "2"
 6)  "3"
 7)  "4"
 8)  "5"

本地:0>lpop  lxh
"6"

本地:0>lrange  lxh  0 -1
 1)  "6A--修改"
 2)  "6"
 3)  "1"
 4)  "2"
 5)  "3"
 6)  "4"
 7)  "5"

10、RPOP  key  ------从队列的尾弹出节点元素(返回该元素并从队列中删除)

本地:0>lrange  lxh  0 -1
 1)  "6A--修改"
 2)  "6"
 3)  "1"
 4)  "2"
 5)  "3"
 6)  "4"
 7)  "5"


本地:0>rpop lxh
"5"

本地:0>lrange lxh  0 -1
 1)  "6A--修改"
 2)  "6"
 3)  "1"
 4)  "2"
 5)  "3"
 6)  "4"

11、RPOPLPUSH source destination 

                -------移除列表的最后一个元素,并将该元素添加到另一个列表并返回

本地:0>lrange lxh  0 -1
 1)  "6A--修改"
 2)  "6"
 3)  "1"
 4)  "2"
 5)  "3"
 6)  "4"


本地:0>lrange 1 0 -1
 1)  "2"
 2)  "3"
 3)  "4"


本地:0>RpopLpush lxh 1 
"4"


本地:0>lrange lxh  0 -1
 1)  "6A--修改"
 2)  "6"
 3)  "1"
 4)  "2"
 5)  "3"


本地:0>lrange 1 0 -1
 1)  "4"
 2)  "2"
 3)  "3"
 4)  "4"

12、BLPOP key1 [key2 ...] timeout

              ----------- 移出并获取列表的第一个或最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

本地:0>lrange lxh  0  -1
 1)  "6A--修改"
 2)  "6"
 3)  "1"
 4)  "2"
 5)  "3"


本地:0>blpop  lxh  10
 1)  "lxh"
 2)  "6A--修改"

本地:0>lrange lxh  0 -1
 1)  "6"
 2)  "1"
 3)  "2"
 4)  "3"

        BRPOP key1 [key2 ...] timeout

              ----------- 移出并获取列表的第一个或最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

本地:0>lpush lxh  0 1 2 3 4 5 6
"7"

本地:0>lrange lxh  0 -1
 1)  "6"
 2)  "5"
 3)  "4"
 4)  "3"
 5)  "2"
 6)  "1"
 7)  "0"


本地:0>brpop lxh 10
 1)  "lxh"
 2)  "0"

本地:0>brpop lxh  10
 1)  "lxh"
 2)  "1"

本地:0>brpop lxh 10
 1)  "lxh"
 2)  "2"

本地:0>brpop lxh 10
 1)  "lxh"
 2)  "3"

本地:0>brpop lxh 10
 1)  "lxh"
 2)  "4"

本地:0>brpop lxh 10
 1)  "lxh"
 2)  "5"

本地:0>brpop lxh 10
 1)  "lxh"
 2)  "6"

三、Redis的list的代码实例

需求一:实现淘宝类似的聚划算功能

需求分析:淘宝的聚划算功能特点是高并发,因为聚划算的产品相对比较少,这样就出现了大量的用户访问,出现了数据量少,高并发场景。

方案制定:使用redis的list数据结构来存储,来完成分页。 redisde list数据结构天然支持这种高并发的分页查询功能。

核心代码:

package com.redis.list;

import com.redis.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/*
 * 本类用来实现redis的list
 *
 * 1、需求1----高并发分页场景,例如淘宝聚划算
 *
 *
 *
 * */
@RestController
@RequestMapping("redisList")
@Slf4j
public class ListRedisController {


    @Autowired
    private RedisTemplate redisTemplate;


    @PostMapping("/findData")
    public List<User> findData(int page, int size) {
        // 定时器刷新到redis
        log.info("开始读取数据");
        long start = (page - 1) * size;
        long end = start + size - 1;
        List<User> users = redisTemplate.opsForList().range("lxh-juhuasuan", start, end);
        if (CollectionUtils.isEmpty(users)) {
            // TODO 读取数据库;
        }

        log.info("读取Redis数据成功");
        return users;
    }


    /*
     * 具体的技术方案采用list 的lpush 和 lrange来实现。
     * lpush  ---- 读取DB数据插入到redis的list集合
     * lrange来实现分页------该命令根据索引读取list数据
     * */
    @PostMapping("/refreshData")
    @Scheduled(cron = "0 */3 * * * ?")
    public String redisList() {
        // 定时器刷新到redis
        log.info("开始将输入刷新到redis");
        new Thread(() -> {
            refreshRedis();
        }).start();
        log.info("刷新Redis成功,开始开始查询");
        return "数据刷新成功";
    }

    // 读取DB数据,刷新到redis的list集合
    public void refreshRedis() {
        // 读取数据库的数据,放入一个集合里面
        List<User> users = products();
        //先删除数据
        redisTemplate.delete("lxh-juhuasuan");
        // 数据库数据一一读取,插入reids
        redisTemplate.opsForList().leftPushAll("lxh-juhuasuan", users);
    }


    /**
     * 模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
     */
    public List<User> products() {
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Random rand = new Random();
            int id = rand.nextInt(10000);
            User user = User
                    .builder()
                    .id(id)
                    .age(id)
                    .name("name--" + id)
                    .userId("userId---" + id)
                    .build();
            list.add(user);
        }
        return list;
    }


}

代码BUG:我们上述代码逻辑有个明显的漏洞,就是缓存击穿。

什么是缓存击穿?

        在高并发的系统中,大量的请求同时查询一个key时,如果这个key正好失效或删除,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。

解释:

        假设我们在刷新redis的时候会先将reids 里面的数据删除,然后再读取DB设置最新redis,此时在删除之后但还未刷新成功的时候,大量请求来请求,发现这个redis已过期或不存在,此时大量的请求会打到DB数据库,此时就发生了缓存击穿的情况。

解决方案:

核心代码:

package com.redis.list;

import com.redis.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/*
 * 本类用来实现redis的list
 *
 * 1、需求1----高并发分页场景,例如淘宝聚划算
 *
 *
 *
 * */
@RestController
@RequestMapping("redisList")
@Slf4j
public class ListRedisController {


    @Autowired
    private RedisTemplate redisTemplate;


    @PostMapping("/findData")
    public List<User> findData(int page, int size) {
        // 定时器刷新到redis
        log.info("开始读取数据");
        long start = (page - 1) * size;
        long end = start + size - 1;
        List<User> users = redisTemplate.opsForList().range("lxh-juhuasuan", start, end);
        if (CollectionUtils.isEmpty(users)){
            users = redisTemplate.opsForList().range("lxh-juhuasuan--B", start, end);
        }
        if (CollectionUtils.isEmpty(users)) {
            // TODO 读取数据库;
        }
        log.info("读取Redis数据成功");
        return users;
    }


    /*
     * 具体的技术方案采用list 的lpush 和 lrange来实现。
     * lpush  ---- 读取DB数据插入到redis的list集合
     * lrange来实现分页------该命令根据索引读取list数据
     * */
    @PostMapping("/refreshData")
    @Scheduled(cron = "0 */3 * * * ?")
    public String redisList() {
        // 定时器刷新到redis
        log.info("开始将输入刷新到redis");
        new Thread(() -> {
            refreshRedis();
        }).start();
        log.info("刷新Redis成功,开始开始查询");
        return "数据刷新成功";
    }

    // 读取DB数据,刷新到redis的list集合
    public void refreshRedis() {
        // 读取数据库的数据,放入一个集合里面
        List<User> users = products();
        //先删除数据
        redisTemplate.delete("lxh-juhuasuan");
        // 数据库数据一一读取,插入reids
        redisTemplate.opsForList().leftPushAll("lxh-juhuasuan", users);

        //先删除数据
        redisTemplate.delete("lxh-juhuasuan--B");
        // 数据库数据一一读取,插入reids
        redisTemplate.opsForList().leftPushAll("lxh-juhuasuan--B", users);
    }


    /**
     * 模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
     */
    public List<User> products() {
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Random rand = new Random();
            int id = rand.nextInt(10000);
            User user = User
                    .builder()
                    .id(id)
                    .age(id)
                    .name("name--" + id)
                    .userId("userId---" + id)
                    .build();
            list.add(user);
        }
        return list;
    }


}

需求二:二级缓存的高并发微信文章的阅读量PV

需求分析:现在有10w篇文章 ,这些文章的都有10w的阅读量。如果这时候让统计文章的阅读总量,此时使用Redis的incrby命令进行累加的话,相当于10亿的计算量。假设这10亿的数量是1天内的阅读数量,此时每秒钟的数量是10w左右,此时Redis的扛不住的。

方案设计:我们采取的是多级缓存计算,由JVM缓存读取一定时间段内的阅读数,然后存储起来,之后存入Redis里面,再有Redis读取数据库。

    步骤1:模拟大量读取文章

package com.redis.list.pv;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/*
 * 开始使用多级缓存来解决  高并发量的问题
 *
 * */
@RestController
@RequestMapping("/reidsList")
@Slf4j
public class PVRedisController {

    public static final String CACHE_PV_LIST = "pv:list";

    public static final String CACHE_ARTICLE = "article:";

    /**
     * Map<时间块,Map<文章Id,访问量>>
     * =Map<2020-01-12 15:30:00到 15:59:00,Map<文章Id,访问量>>
     * =Map<438560,Map<文章Id,访问量>>
     */
    public static final Map<Long, Map<Integer, Integer>> PV_MAP = new ConcurrentHashMap();
    @Autowired
    private RedisTemplate redisTemplate;


    @PostConstruct
    public void initPV() {
        log.info("启动模拟大量PV请求 定时器..........");
        new Thread(() -> runArticlePV()).start();
    }

    private void runArticlePV() {
        while (true) {
            this.batchAddArticle();
            try {
                //5秒执行一次
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 对1000篇文章,进行模拟请求PV
     * <p>
     * 假设
     */
    public void batchAddArticle() {
        for (int i = 0; i < 1000; i++) {
            this.addPV(new Integer(i));
        }
    }

    /**
     * 那如何切割时间块呢? 如何把当前的时间切入时间块中?
     * 例如,我们要计算“小时块”,先把当前的时间转换为为毫秒的时间戳,然后除以一个小时,
     * 即当前时间T/1000*60*60=小时key,然后用这个小时序号作为key。
     * 例如:
     * 2020-01-12 15:30:00=1578814200000毫秒 转换小时key=1578814200000/1000*60*60=438560
     * 2020-01-12 15:59:00=1578815940000毫秒 转换小时key=1578815940000/1000*60*60=438560
     * 2020-01-12 16:30:00=1578817800000毫秒 转换小时key=1578817800000/1000*60*60=438561
     * 剩下的以此类推
     * <p>
     * 每一次PV操作时,先计算当前时间是那个时间块,然后存储Map中。
     */
    private void addPV(Integer id) {
        // 读取1分钟的时间块,计算得到key值
        long m1 = System.currentTimeMillis() / (1000 * 60 * 1);

        Map<Integer, Integer> mMap = PV_MAP.get(m1);

        if (CollectionUtils.isEmpty(mMap)) {
            mMap = new ConcurrentHashMap();
            mMap.put(id, new Integer(1));
            //<1分钟的时间块,Map<文章Id,访问量>>
            PV_MAP.put(m1, mMap);
        } else {
            //通过文章id 取出浏览量
            Integer value = mMap.get(id);
            if (value == null) {
                mMap.put(id, new Integer(1));
            } else {
                mMap.put(id, value + 1);
            }
        }
    }

}

步骤2:由于此时的时间块(特定时间内)读取的数据都在JVM缓存里面,由于当前项目是分布式,多节点部署。将所有的数据都读取到Redis里面

package com.redis.list.pv;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/*
 * 开始使用多级缓存来解决  高并发量的问题
 *
 * */
@RestController
@RequestMapping("/reidsList")
@Slf4j
public class RedisGetPVController {

    public static final String CACHE_PV_LIST = "pv:list";

    public static final String CACHE_ARTICLE = "article:";

    /**
     * Map<时间块,Map<文章Id,访问量>>
     * =Map<2020-01-12 15:30:00到 15:59:00,Map<文章Id,访问量>>
     * =Map<438560,Map<文章Id,访问量>>
     */
    public static final Map<Long, Map<Integer, Integer>> PV_MAP = new ConcurrentHashMap();

    @Autowired
    private RedisTemplate redisTemplate;


    @RequestMapping("getPVJvm")
    @Scheduled(cron = "0 */2 * * * ?")
    public void getPVJvm() {
        new Thread(() -> {
            getPv();
        }).start();
    }

    /*
     * 获取JVM内部的文章阅读量
     * */
    private void getPv() {
        // 读取数据
        //为了方便测试 改为1分钟 时间块
        long m1 = System.currentTimeMillis() / (1000 * 60 * 1);

        // 存储的是所有的时间块的数据,开始遍历读取
        Iterator<Long> iterators = PV_MAP.keySet().iterator();
        while (iterators.hasNext()) {
            //取出map的时间块
            Long key = iterators.next();
            // 查看当前时间块。小于当前的分钟时间块key ,就消费
            if (key < m1) {
                //先push
                Map<Integer, Integer> map = PV_MAP.get(key);
                //push到reids的list数据结构中,list的存储的书为Map<文章id,访问量PV>即每个时间块的pv数据
                this.redisTemplate.opsForList().leftPush(CACHE_PV_LIST, map);
                //后remove
                PV_MAP.remove(key);
                log.info("push进{}", map);
            }

        }
    }

}

步骤3:此时Redis的List集合已经汇总了所有的点赞的数据,此时需要将Redis的数据进行汇总处理。

package com.redis.list.pv;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Iterator;
import java.util.Map;

@RestController
@RequestMapping("/reidsList")
@Slf4j
public class RedisFinalController {

    public static final String CACHE_PV_LIST = "pv:list";

    public static final String CACHE_ARTICLE = "article:";

    @Autowired
    private RedisTemplate redisTemplate;

    // 读取Redis的二级缓存,计算出文章的整体阅读数量
    public void RedisFinal() {
        Map<Integer, Integer> map = (Map<Integer, Integer>) redisTemplate.opsForList().leftPop(CACHE_PV_LIST);
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            Integer value = map.get(key);
            long n = redisTemplate.opsForValue().increment(CACHE_ARTICLE + key, value);


            // 存入 DB
        }

    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值