后端杂七杂八系列篇三
① Spring Event用法
实际业务开发过程中,业务逻辑可能非常复杂,核心业务 + N个子业务。如果都放到一块儿去做,代码可能会很长,耦合度也会很高。
MQ解决这个问题,但是在业务不是特别复杂的情况下,我们可以使用观察者设计模式来完成。Spring Event(Application Event)其实就是一个观察者设计模式。
① 同步代码的用法
① 自定义事件
// 定义事件,继承 ApplicationEvent 的类成为一个事件类
@Data
@ToString
public class OrderProductEvent extends ApplicationEvent {
/** 该类型事件携带的信息 */
private String orderId;
public OrderProductEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
}
② 定义监听器
// 监听并处理事件,实现 ApplicationListener 接口或者使用 @EventListener 注解
@Slf4j
@Component
public class OrderProductListener implements ApplicationListener<OrderProductEvent> {
/** 使用 onApplicationEvent 方法对消息进行接收处理 */
@SneakyThrows
@Override
public void onApplicationEvent(OrderProductEvent event) {
String orderId = event.getOrderId();
long start = System.currentTimeMillis();
Thread.sleep(2000);
long end = System.currentTimeMillis();
log.info("{}:校验订单商品价格耗时:({})毫秒", orderId, (end - start));
}
}
③ 定义发布者
// 发布事件,通过 ApplicationEventPublisher 发布事件
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
/** 注入ApplicationContext用来发布事件 */
private final ApplicationContext applicationContext;
/**
* 下单
*
* @param orderId 订单ID
*/
public String buyOrder(String orderId) {
long start = System.currentTimeMillis();
// 1.查询订单详情
// 2.检验订单价格 (同步处理)
applicationContext.publishEvent(new OrderProductEvent(this, orderId));
// 3.短信通知(异步处理)
long end = System.currentTimeMillis();
log.info("任务全部完成,总耗时:({})毫秒", end - start);
return "购买成功";
}
}
④ 发布消息后,接口收到消息
// 发布消息后,接口收到消息
@SpringBootTest
public class OrderServiceTest {
@Autowired private OrderService orderService;
@Test
public void buyOrderTest() {
orderService.buyOrder("732171109");
}
}
② 异步代码的用法
有些业务场景不需要在一次请求中同步完成,比如邮件发送、短信发送等。
① 开启异步
// 新增@EnableAsync注解
@EnableAsync
@SpringBootApplication
public class MingYueSpringbootEventApplication {
public static void main(String[] args) {
SpringApplication.run(MingYueSpringbootEventApplication.class, args);
}
}
② 自定义事件
@Data
@AllArgsConstructor
public class MsgEvent {
/** 该类型事件携带的信息 */
public String orderId;
}
③ 自定义监听器(推荐使用 @EventListener 注解),使用@Async注解
// 使用@EventListener 注解
@Slf4j
@Component
public class MsgListener {
@Async
@SneakyThrows
@EventListener(MsgEvent.class)
public void sendMsg(MsgEvent event) {
String orderId = event.getOrderId();
long start = System.currentTimeMillis();
log.info("开发发送短信");
log.info("开发发送邮件");
Thread.sleep(4000);
long end = System.currentTimeMillis();
log.info("{}:发送短信、邮件耗时:({})毫秒", orderId, (end - start));
}
}
④ 定义发布者
public String buyOrder(String orderId) {
long start = System.currentTimeMillis();
// 1.查询订单详情
// 2.检验订单价格 (同步处理)
applicationContext.publishEvent(new OrderProductEvent(this, orderId));
// 3.短信通知(异步处理)
applicationContext.publishEvent(new MsgEvent(orderId));
long end = System.currentTimeMillis();
log.info("任务全部完成,总耗时:({})毫秒", end - start);
return "购买成功";
}
⑤ 发布消息后,观察同步与异步消息
@Test
public void buyOrderTest() {
orderService.buyOrder("732171109");
}
② SpringBoot+Redis BitMap 实现签到与统计功能
① 什么是Redis BitMap ?
在数据处理和分析中,常常需要对大量的数据进行统计和计算。
当数据量达到亿级别时
,传统的数据结构和算法已经无法胜任这个任务。Bitmap(位图)
是一种适合于大规模数据统计的数据结构,能够以较低的空间复杂度存储大规模数据,并且支持高效的位运算操作。本文将介绍 Bitmap 的基本概念、实现方式和在亿级数据计算中的应用。
② SpringBoot 整合 Redis 实现签到 功能
① 设计思路
我们可以把年和月
作为BitMap的key,然后保存到一个BitMap中,每次签到就到对应的位上把数字从0 变为1,只要是1,就代表是这一天签到了,反之咋没有签到。
比如 2024年1月1日的签到:
Key(202401) Value:1
比如 2024年1月2日的签到:
Key(202401) Value:1
比如 2024年1月3日的未签到:
Key(202401) Value:0
所以2024年一月份的签到状态可以表示为:
Key(202401) Value:1,1,0,1,0,1,1,1
为了区分用户,我们可以加一个用户标识。比如 202401:24ewe89 后面的这个24ewe89是用户的token
② 如何连续签到天数?
③ 如何得到本月到今天为止的所有签到数据?
④ 如何从后向前遍历每个Bit位?
br>
⑤ 代码Demo
// controller
@GetMapping("/signCount")
public Result signCount() {
return userService.signCount();
}
// service
public Result signCount() {
//1. 获取登录用户
Long userId = UserHolder.getUser().getId();
//2. 获取日期
LocalDateTime now = LocalDateTime.now();
//3. 拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = RedisConstants.USER_SIGN_KEY + userId + keySuffix;
//4. 获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
//5. 获取本月截至今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD sign:5:202301 GET u3 0
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
//没有任务签到结果
if (result == null || result.isEmpty()) {
return Result.ok(0);
}
Long num = result.get(0);
if (num == null || num == 0) {
return Result.ok(0);
}
//6. 循环遍历
int count = 0;
while (true) {
//6.1 让这个数字与1 做与运算,得到数字的最后一个bit位 判断这个数字是否为0
if ((num & 1) == 0) {
//如果为0,签到结束
break;
} else {
count ++;
}
num >>>= 1;
}
return Result.ok(count);
}
③ 基于Redis实现分布式锁(使用Redisson)
用法
1. pom文件
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
- 配置 Redisson 客户端
@Configuration
@SuppressWarnings("all")
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.88.130:6379")
.setPassword("root");
return Redisson.create(config);
}
}
- Redisson 的可重入锁
@Test
public void tesRedisson() throws InterruptedException {
// 获取可重入锁, 指定锁的名称
RLock lock = redissonClient.getLock("anLock");
// 尝试获取锁
// 参数1:获取锁的最大等待时间(期间会多次重试获取锁)
// 参数2:锁自动释放时间
// 参数3:时间单位
boolean isGetLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
if (isGetLock) {
try {
System.out.println("执行业务");
} finally {
lock.unlock();
}
}
}
⑤ redis 缓存穿透
什么是缓存穿透?
redis已经没有了,还查询mysql
解决方案:布隆过滤器
布隆过滤器主要是用于检索一个元素是否在一个集合中