Redis基础文档-01-安装

Redis基础文档-01-安装

参考文档-Redis快速入门指南-中文
参考文档-Redis 教程

一、启动并连接

本地启动一个redis或者用redis cloud免费账户,都可以。

language-bash
1
docker run --name CommonTrain -p 6379:6379 -itd redis:7.2

然后下载REDISINSIGHT

二、支持的数据类型

  • 字符串string
  • 哈希hash
  • 列表list
  • 集合set
  • 有序集合sorted set
  • 位图bitmaps
  • 基数统计hyperLogLogs
Redis之缓存击穿问题解决方案

Redis之缓存击穿问题解决方案

一、书接上文

Redis之缓存雪崩问题解决方案

二、介绍

缓存击穿就是大量并发访问同一个热点数据,一旦这个热点数据缓存失效,则请求压力都来到数据库。

三、解决方案

1. 单例双检锁

language-java
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
@Override
public CoursePublish getCoursePublishCache(Long courseId) {

String key = "content:course:publish:" + courseId;
//布隆过滤器
boolean contains = bloomFilter.contains(key);
if (!contains){

return null;
}
//先查询redis
Object object = redisTemplate.opsForValue().get(key);
if (object != null){

String string = object.toString();
CoursePublish coursePublish = JSON.parseObject(string, CoursePublish.class);
return coursePublish;
}else {

//后查询数据库
//加锁,防止缓存击穿
synchronized (this){

//单例双检锁
object = redisTemplate.opsForValue().get(key);
if (object != null){

String string = object.toString();
CoursePublish coursePublish = JSON.parseObject(string, CoursePublish.class);
return coursePublish;
}
CoursePublish coursePublish = getCoursePublish(courseId);
if (coursePublish != null) {

bloomFilter.add(key);
redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish));
} else {

int timeout = 10 + new Random().nextInt(20);
redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish), timeout, TimeUnit.SECONDS);
}
return coursePublish;
}
}
}

2. 缓存预热和定时任务

使用缓存预热,把数据提前放入缓存,然后根据过期时间,发布合理的定时任务,主动去更新缓存,让热点数据永不过期。

Redis之缓存雪崩问题解决方案

Redis之缓存雪崩问题解决方案

一、书接上文

Redis之缓存穿透问题解决方案实践SpringBoot3+Docker

二、介绍

缓存雪崩,指大量的缓存失效,大量的请求又同时落在数据库。主要的一种诱因是key设置的过期时间都一样。

三、解决方案

1. 锁

加锁,每次只让一个线程可以访问数据库,随后存入缓存。性能太差。

2. 不同的过期时间

最简单有效的解决办法是设置不同的过期时间。比如

language-java
1
2
int timeout = 10 + new Random().nextInt(20);
redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish), timeout, TimeUnit.SECONDS);

3. 缓存预热和定时任务

使用缓存预热,把数据提前放入缓存,然后根据过期时间,发布合理的定时任务,主动去更新缓存。
缓存预热参考代码如下。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class RedisHandler implements InitializingBean {

@Autowired
RedisTemplate redisTemplate;
@Autowired
CoursePublishMapper coursePublishMapper;

@Override
public void afterPropertiesSet() throws Exception {

List<CoursePublish> coursePublishList = coursePublishMapper.selectList(new LambdaQueryWrapper<CoursePublish>());
//缓存预热
coursePublishList.forEach(coursePublish -> {

String key = "content:course:publish:" + coursePublish.getId();
redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish));
});
}
}

至于定时任务,可以使用xxl-job。具体使用方法,可以参考这个文章
Docker部署xxl-job调度器并结合SpringBoot测试

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

环境如下

Version
SpringBoot 3.2.1
spring-amqp 3.1.1
RabbitMq 3-management

一、起因

老版本的spring-amqpCorrelationData上设置ConfirmCallback。但是今天却突然发现correlationData.getFuture()没有addCallback函数了。

查询文档和帖子后,发现ConfirmCallbackReturnsCallback都需要在RabbitTemplate中设置,同时ConfirmCallback中默认无法得到消息内容,如果想在ConfirmCallback中把消息内容存到数据库等地方进行记录,怎么办呢?

参考手册

二、代码

1. 定义exchange和queue

language-java
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
@Slf4j
@Configuration
public class PayNotifyConfig{


//交换机
public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";
//支付通知队列
public static final String PAYNOTIFY_QUEUE = "paynotify_queue";
//支付结果通知消息类型
public static final String MESSAGE_TYPE = "payresult_notify";


//声明交换机,且持久化
@Bean(PAYNOTIFY_EXCHANGE_FANOUT)
public FanoutExchange paynotify_exchange_fanout() {

// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);
}
//支付通知队列,且持久化
@Bean(PAYNOTIFY_QUEUE)
public Queue paynotify_queue() {

return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();
}

//交换机和支付通知队列绑定
@Bean
public Binding binding_paynotify_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {

return BindingBuilder.bind(queue).to(exchange);
}
}

2. RabbitTemplate

在上面的类中继续添加RabbitTemplate ,并设置ConfirmCallbackReturnsCallback

language-java
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
@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {

final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//设置confirm callback
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {

String body = "1";
if (correlationData instanceof EnhancedCorrelationData) {

body = ((EnhancedCorrelationData) correlationData).getBody();
}
if (ack) {

//消息投递到exchange
log.debug("消息发送到exchange成功:correlationData={},message_id={} ", correlationData, body);
System.out.println("消息发送到exchange成功:correlationData={},message_id={}"+correlationData+body);
} else {

log.debug("消息发送到exchange失败:cause={},message_id={}",cause, body);
System.out.println("消息发送到exchange失败:cause={},message_id={}"+cause+body);
}
});

//设置return callback
rabbitTemplate.setReturnsCallback(returned -> {

Message message = returned.getMessage();
int replyCode = returned.getReplyCode();
String replyText = returned.getReplyText();
String exchange = returned.getExchange();
String routingKey = returned.getRoutingKey();
// 投递失败,记录日志
log.error("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
});
return rabbitTemplate;
}

3. EnhancedCorrelationData

原始的CorrelationData,目前已经无法从中获取消息内容,也就是说现在的ConfirmCallback无法获取到消息的内容,因为设计上只关注是否投递到exchange成功。如果需要在ConfirmCallback中获取消息的内容,需要扩展这个类,并在发消息的时候,放入自定义数据。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EnhancedCorrelationData extends CorrelationData {

private final String body;

public EnhancedCorrelationData(String id, String body) {

super(id);
this.body = body;
}

public String getBody() {

return body;
}
}

4. 发送消息

EnhancedCorrelationData把消息本身放进去,或者如果你有表记录消息,你可以只放入其id。这样触发ConfirmCallback的时候,就可以获取消息内容。

language-java
1
2
3
4
5
6
7
8
9
public void notifyPayResult() {

String message = "TEST Message";
Message message1 = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
CorrelationData correlationData = new EnhancedCorrelationData(UUID.randomUUID().toString(), message.toString());
rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", message1, correlationData);
}
SpringBoot使用当前类代理类(内部事务)解决方案

SpringBoot使用当前类代理类(内部事务)解决方案

Spring Boot 开发中,我们时常遇到需要在一个类的内部调用自己的其他方法,并且这些方法可能需要事务支持。这种场景通常发生在业务逻辑较为复杂的服务类中,其中一些操作需要确保数据的一致性和完整性。本文将以 MediaFileServiceImpl 类为例,探讨如何在 Spring Boot 中有效地使用当前类的代理类来处理内部事务。

一、场景描述

考虑一个典型的例子:在 MediaFileServiceImpl 服务类中,upload 方法需要调用 upload2Mysql 方法。这里,upload2Mysql 方法是事务性的,意味着它涉及到数据库操作,这些操作需要在一个事务中被处理。如果直接在 upload 方法中调用 upload2Mysql,由于 Spring 的代理方式,事务管理可能不会被正确应用,因为实际上是在同一个实例内部进行方法调用,绕过了 Spring 的代理。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Lazy
@Autowired
private MediaFileService mediaFileService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
mediaFileService.upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

二、解决方案

1. 使用 @Lazy(推荐)

MediaFileServiceImpl 类中使用 @Lazy 注解解决循环依赖问题。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Lazy
@Autowired
private MediaFileService mediaFileService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
mediaFileService.upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

2. 使用方法注入

方法注入是一种允许在运行时动态注入方法实现的技术。这里,我们通过一个简单的例子来说明如何应用方法注入。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@Slf4j
public abstract class MediaFileServiceImpl implements MediaFileService {


@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
getMediaFileService().upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}

@Lookup
protected abstract MediaFileService getMediaFileService();
}

3. 使用 ApplicationContext

这种方法通过 ApplicationContext 获取当前类的代理。

language-java
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
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Autowired
private ApplicationContext context;

private MediaFileService getMediaFileService() {

return context.getBean(MediaFileService.class);
}

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
getMediaFileService().upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

4. 分离服务层

将事务性方法移至另一个服务类中。

language-java
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
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Autowired
private MediaFileTransactionalService transactionalService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
transactionalService.upload2Mysql();
// ... 其他业务逻辑 ...
}
}

@Service
@Transactional
class MediaFileTransactionalService {


public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

5. AspectJ 代理模式

使用 AspectJ 代理模式而不是默认的 JDK 动态代理。

language-java
1
2
3
4
5
6
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Configuration
public class AppConfig {

// ... 其他配置 ...
}

然后,您的 MediaFileServiceImpl 类保持不变。