9.30更新:这本书应该不会再看下去了,本来想的是中文书自己没有基础可以一看,但发现干货真的不多,全文都是在贴代码,并且文字有点冗余。人生短暂,及时止损,把时间花在更难更有价值的书籍上吧。
缓存中间件Redis
Redis使用
Redis自定义注入Bean组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class CommonConfig {
/**
* Redis链接工厂
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 缓存RedisTemplate的自定义配置
*
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//TODO:指定Key序列化策略为为String序列化,Value为JDK自带的序列化策略
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
//TODO:指定HashKey序列化策略为String序列化-针对Hash散列存储
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
/**
* 缓存StringRedisTemplate
*
* @return
*/
@Bean
public StringRedisTemplate stringRedisTemplate() {
//采用默认配置即可-后续有自定义配置时则在此处添加即可
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
|
RedisTemplate实战
(1)使用RedisTemplate将字符串信息写入缓存中,并读取出来展示到控制台上。
(2)使用RedisTemplate将对象信息序列化为JSON格式的字符串后写入缓存中,然后将其读取出来,最后反序列化解析其中的内容并展示在控制台上。
StringRedisTemplate实战
(1)使用StringRedisTemplate将字符串信息写入缓存中,并读取出来展示到控制台上。
(2)使用StringRedisTemplate将对象信息序列化为JSON格式的字符串后写入缓存中,然后将其读取出来,最后反序列化解析其中的内容并展示在控制台上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisTest {
private static final Logger log = LoggerFactory.getLogger(RedisTest.class);
@Autowired
private RedisTemplate redisTemplate;
//采用RedisTemplate将字符串信息写入缓存中,并读取出来
@Test
public void one() {
log.info("------开始RedisTemplate操作组件实战----");
//定义字符串内容以及存入缓存的key
final String key = "redis:template:one:string";
final String content = "RedisTemplate实战字符串信息";
//Redis通用的操作组件
ValueOperations valueOperations = redisTemplate.opsForValue();
//将字符串信息写入缓存中
log.info("写入缓存中的内容:{} ", content);
valueOperations.set(key, content);
//从缓存中读取内容
Object result = valueOperations.get(key);
log.info("读取出来的内容:{} ", result);
}
@Autowired
private ObjectMapper objectMapper;
//采用RedisTemplate将对象信息序列化为JSOn格式字符串后写入缓存中,
//然后将其读取出来,最后反序列化解析其中的内容并展示在控制台
@Test
public void two() throws Exception {
log.info("------开始RedisTemplate操作组件实战----");
//构造对象信息
User user = new User(1, "debug", "阿修罗");
//Redis通用的操作组件
ValueOperations valueOperations = redisTemplate.opsForValue();
//将序列化后的信息写入缓存中
final String key = "redis:template:two:object";
final String content = objectMapper.writeValueAsString(user);
valueOperations.set(key, content);
log.info("写入缓存对象的信息:{} ", user);
//从缓存中读取内容
Object result = valueOperations.get(key);
if (result != null) {
User resultUser = objectMapper.readValue(result.toString(), User.class);
log.info("读取缓存内容并反序列化后的结果:{} ", resultUser);
}
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
//采用StringRedisTemplate将字符串信息写入缓存中,并读取出来
@Test
public void three() {
log.info("------开始StringRedisTemplate操作组件实战----");
//定义字符串内容以及存入缓存的key
final String key = "redis:three";
final String content = "StringRedisTemplate实战字符串信息";
//Redis通用的操作组件
ValueOperations valueOperations = stringRedisTemplate.opsForValue();
//将字符串信息写入缓存中
log.info("写入缓存中的内容:{} ", content);
valueOperations.set(key, content);
//从缓存中读取内容
Object result = valueOperations.get(key);
log.info("读取出来的内容:{} ", result);
}
//采用StringRedisTemplate将对象信息序列化为JSON格式字符串后写入缓存中,
//然后将其读取出来,最后反序列化解析其中的内容并展示在控制台
@Test
public void four() throws Exception {
log.info("------开始StringRedisTemplate操作组件实战----");
//构造对象信息
User user = new User(2, "SteadyJack", "阿修罗");
//Redis通用的操作组件
ValueOperations valueOperations = redisTemplate.opsForValue();
//将序列化后的信息写入缓存中
final String key = "redis:four";
final String content = objectMapper.writeValueAsString(user);
valueOperations.set(key, content);
log.info("写入缓存对象的信息:{} ", user);
//从缓存中读取内容
Object result = valueOperations.get(key);
if (result != null) {
User resultUser = objectMapper.readValue(result.toString(), User.class);
log.info("读取缓存内容并反序列化后的结果:{} ", resultUser);
}
}
}
|
Redis数据结构
字符串String
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//字符串类型
@Test
public void one() throws Exception{
//构造用户个人实体对象
Person person=new Person(10013,23,"修罗","debug","火星");
//定义key与即将存入缓存中的value
final String key="redis:test:1";
String value=objectMapper.writeValueAsString(person);
//写入缓存中
log.info("存入缓存中的用户实体对象信息为:{} ",person);
redisTemplate.opsForValue().set(key,value);
//从缓存中获取用户实体信息
Object res=redisTemplate.opsForValue().get(key);
if (res!=null){
Person resP=objectMapper.readValue(res.toString(),Person.class);
log.info("从缓存中读取信息:{} ",resP);
}
}
|
列表List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
//列表类型
@Test
public void two() throws Exception {
//构造已经排好序的用户对象列表
List<Person> list = new ArrayList<>();
list.add(new Person(1, 21, "修罗", "debug", "火星"));
list.add(new Person(2, 22, "大圣", "jack", "水帘洞"));
list.add(new Person(3, 23, "盘古", "Lee", "上古"));
log.info("构造已经排好序的用户对象列表: {} ", list);
//将列表数据存储至Redis的List中
final String key = "redis:test:2";
ListOperations listOperations = redisTemplate.opsForList();
for (Person p : list) {
//往列表中添加数据-从队尾中添加
listOperations.leftPush(key, p);
}
//获取Redis中List的数据-从队头中获取
log.info("--获取Redis中List的数据-从队头中获取--");
Object res = listOperations.rightPop(key);
Person resPerson;
while (res != null) {
resPerson = (Person) res;
log.info("当前数据:{} ", resPerson);
res = listOperations.rightPop(key);
}
}
|
Redis的列表List类型特别适用于“排名”“排行榜”“近期访问数据列表”等业务场景。
集合Set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
//集合类型
@Test
public void three() throws Exception {
//构造一组用户姓名列表
List<String> userList = new ArrayList<>();
userList.add("debug");
userList.add("jack");
userList.add("修罗");
userList.add("大圣");
userList.add("debug");
userList.add("jack");
userList.add("steadyheart");
userList.add("修罗");
userList.add("大圣");
log.info("待处理的用户姓名列表:{} ", userList);
//遍历访问,剔除相同姓名的用户并塞入集合中,最终存入缓存中
final String key = "redis:test:3";
SetOperations setOperations = redisTemplate.opsForSet();
for (String str : userList) {
setOperations.add(key, str);
}
//从缓存中获取已剔除的用户集合
Object res = setOperations.pop(key);
while (res != null) {
log.info("从缓存中获取的用户集合-当前用户:{} ", res);
res = setOperations.pop(key);
}
}
|
在实际应用中,Redis的Set类型常用于解决重复提交、剔除重复ID等业务场景。
有序集合SortedSet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
//有序集合
@Test
public void four() throws Exception {
//构造一组无序的用户手机充值对象列表
List<PhoneUser> list = new ArrayList<>();
list.add(new PhoneUser("103", 130.0));
list.add(new PhoneUser("101", 120.0));
list.add(new PhoneUser("102", 80.0));
list.add(new PhoneUser("105", 70.0));
list.add(new PhoneUser("106", 50.0));
list.add(new PhoneUser("104", 150.0));
log.info("构造一组无序的用户手机充值对象列表:{}", list);
//遍历访问充值对象列表,将信息塞入Redis的有序集合中
final String key = "redis:test:4";
//因为zSet在add元素进入缓存后,下次就不能进行更新了,故而为了测试方便,
//进行操作之前先清空该缓存(当然实际生产环境中不建议这么使用)
redisTemplate.delete(key);
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
for (PhoneUser u : list) {
zSetOperations.add(key, u, u.getFare());
}
//前端获取访问充值排名靠前的用户列表
Long size = zSetOperations.size(key);
//从小到大排序
Set<PhoneUser> resSet = zSetOperations.range(key, 0L, size);
//从大到小排序
//Set<PhoneUser> resSet=zSetOperations.reverseRange(key,0L,size);
for (PhoneUser u : resSet) {
log.info("从缓存中读取手机充值记录排序列表,当前记录:{} ", u);
}
}
|
Redis的有序集合SortedSet常用于充值排行榜、积分排行榜、成绩排名等应用场景。
哈希Hash存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
//Hash哈希存储
@Test
public void five() throws Exception {
//构造学生对象列表,水果对象列表
List<Student> students = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();
students.add(new Student("10010", "debug", "大圣"));
students.add(new Student("10011", "jack", "修罗"));
students.add(new Student("10012", "sam", "上古"));
fruits.add(new Fruit("apple", "红色"));
fruits.add(new Fruit("orange", "橙色"));
fruits.add(new Fruit("banana", "黄色"));
//分别遍历不同对象队列,并采用Hash哈希存储至缓存中
final String sKey = "redis:test:5";
final String fKey = "redis:test:6";
HashOperations hashOperations = redisTemplate.opsForHash();
for (Student s : students) {
hashOperations.put(sKey, s.getId(), s);
}
for (Fruit f : fruits) {
hashOperations.put(fKey, f.getName(), f);
}
//获取学生对象列表与水果对象列表
Map<String, Student> sMap = hashOperations.entries(sKey);
log.info("获取学生对象列表:{} ", sMap);
Map<String, Fruit> fMap = hashOperations.entries(fKey);
log.info("获取水果对象列表:{} ", fMap);
//获取指定的学生对象、水果对象
String sField = "10012";
Student s = (Student) hashOperations.get(sKey, sField);
log.info("获取指定的学生对象:{} -> {} ", sField, s);
String fField = "orange";
Fruit f = (Fruit) hashOperations.get(fKey, fField);
log.info("获取指定的水果对象:{} -> {} ", fField, f);
}
|
在实际互联网应用,当需要存入缓存中的对象信息具有某种共性时,为了减少缓存中Key的数量,应考虑采用Hash哈希存储。
Key失效
1、调用SETEX方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//Key失效一
@Test
public void six() throws Exception {
//构造key与redis操作组件
final String key1 = "redis:test:6";
ValueOperations valueOperations = redisTemplate.opsForValue();
//第一种方法:在往缓存中set数据时,提供一个TTL,表示TTL时间一到,缓存中的key将自动失效,即被清理
//在这里TTL是10秒
valueOperations.set(key1, "expire操作1", 10L, TimeUnit.SECONDS);
//等待5秒-判断key是否还存在
Thread.sleep(5000);
Boolean existKey1 = redisTemplate.hasKey(key1);
Object value = valueOperations.get(key1);
log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey1, value);
//再等待5秒-再判断key是否还存在
Thread.sleep(5000);
existKey1 = redisTemplate.hasKey(key1);
value = valueOperations.get(key1);
log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey1, value);
}
|
2、使用RedisTemplate操作组件的Expire()方法指定失效的Key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//Key失效二
@Test
public void seven() throws Exception {
//构造key与redis操作组件
final String key2 = "redis:test:7";
ValueOperations valueOperations = redisTemplate.opsForValue();
//第二种方法:在往缓存中set数据后,采用redisTemplate的expire方法失效该key
valueOperations.set(key2, "expire操作2");
redisTemplate.expire(key2, 10L, TimeUnit.SECONDS);
//等待5秒-判断key是否还存在
Thread.sleep(5000);
Boolean existKey = redisTemplate.hasKey(key2);
Object value = valueOperations.get(key2);
log.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey, value);
//再等待5秒-再判断key是否还存在
Thread.sleep(5000);
existKey = redisTemplate.hasKey(key2);
value = valueOperations.get(key2);
log.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey, value);
}
|
使缓存中的Key失效与判断 Key是否存在,在实际业务场景中是很常用的,最常见的场景包括:
(1)将数据库查询到的数据缓存一定的时间 TTL,在 TTL时间内前端查询访问数据列表时,只需要在缓存中查询即可,从而减轻数据库的查询压力。
(2)将数据压入缓存队列中,并设置一定的TTL时间,当TTL时间一到,将触发监听事件,从而处理相应的业务逻辑。
Redis缓存穿透
如果前端频繁发起访问请求时,恶意提供数据库中不存在的Key,则此时数据库中查询到的数据将永远为Null。由于Null的数据是不存入缓存中的,因而每次访问请求时将查询数据库,如果此时有恶意攻击,发起“洪流”式的查询,则很有可能会对数据库造成极大的压力,甚至压垮数据库。这个过程称之为“缓存穿透”。
解决方案:当查询数据库时如果没有查询到数据,则将 Null返回给前端用户,同时将该Null数据塞入缓存中,并将对应的Key设置一定的过期时间TTL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/**
* 缓存穿透实战
**/
@RestController
public class CachePassController {
private static final Logger log = LoggerFactory.getLogger(CachePassController.class);
private static final String prefix = "cache/pass";
@Autowired
private CachePassService cachePassService;
/**
* 获取热销商品信息
*
* @param itemCode
* @return
*/
@RequestMapping(value = prefix + "/item/info", method = RequestMethod.GET)
public Map<String, Object> getItem(@RequestParam String itemCode) {
Map<String, Object> resMap = new HashMap<>();
resMap.put("code", 0);
resMap.put("msg", "成功");
try {
resMap.put("data", cachePassService.getItemInfo(itemCode));
} catch (Exception e) {
resMap.put("code", -1);
resMap.put("msg", "失败" + e.getMessage());
}
return resMap;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/**
* 缓存穿透service
*/
@Service
public class CachePassService {
private static final Logger log= LoggerFactory.getLogger(CachePassService.class);
@Autowired
private ItemMapper itemMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
private static final String keyPrefix="item:";
/**
* 获取商品详情-如果缓存有,则从缓存中获取;如果没有,则从数据库查询,并将查询结果塞入缓存中
* @param itemCode
* @return
* @throws Exception
*/
public Item getItemInfo(String itemCode) throws Exception{
Item item=null;
final String key=keyPrefix+itemCode;
ValueOperations valueOperations=redisTemplate.opsForValue();
if (redisTemplate.hasKey(key)){
log.info("---获取商品详情-缓存中存在该商品---商品编号为:{} ",itemCode);
//从缓存中查询该商品详情
Object res=valueOperations.get(key);
if (res!=null && !Strings.isNullOrEmpty(res.toString())){
item=objectMapper.readValue(res.toString(),Item.class);
}
}else{
log.info("---获取商品详情-缓存中不存在该商品-从数据库中查询---商品编号为:{} ",itemCode);
//从数据库中获取该商品详情
item=itemMapper.selectByCode(itemCode);
if (item!=null){
valueOperations.set(key,objectMapper.writeValueAsString(item));
}else{
//过期失效时间TTL设置为30分钟-当然实际情况要根据实际业务决定
valueOperations.set(key,"",30L, TimeUnit.MINUTES);
}
}
return item;
}
}
|
Redis缓存雪崩
缓存雪崩:指的是在某个时间点,缓存中的Key集体发生过期失效致使大量查询数据库的请求都落在了DB(数据库)上,导致数据库负载过高,压力暴增,甚至有可能“压垮”数据库。
解决方案:为这些Key设置不同的、随机的TTL,从而错开缓存中Key的失效时间点,可以在某种程度上减少数据库的查询压力。
Redis缓存击穿
缓存击穿:指缓存中某个频繁被访问的Key(“热点Key”),在不停地扛着前端的高并发请求,当这个Key突然在某个瞬间过期失效时,持续的高并发访问请求就“穿破”缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。
解决方案:应该设置这个热点Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。
不管是缓存穿透、缓存雪崩还是缓存击穿,其实它们最终导致的后果几乎都是一样的,即给DB(数据库)造成压力,甚至压垮数据库。而它们的解决方案也都有一个共性,那就是“加强防线”,尽量让高并发的读请求落在缓存中,从而避免直接跟数据库打交道。
Redis应用实战之抢红包系统
消息中间件RabbitMQ
死信队列/延迟队列
分布式锁
Redisson
Author
Anjana
LastMod
2022-09-01
License
原创文章,如需转载请注明作者和出处。谢谢!