一、定义
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
}
}
}