Java项目大赏(实习版)

最近在准备实习,找一些烂大街经典项目练练手.

苍穹外卖

一个项目通常包含公共类(常量,工具以及异常)部分以及实体类部分

image-20250422144407360

此外还有service,controller,mapper(repository)层以及一些配置类,拦截器等

Jwt登录验证

  1. 用户登录请求

客户端(通常是浏览器或App)发送包含用户名和密码的登录请求到后端。

  1. 服务端验证身份

后端接收请求,验证用户名和密码是否正确:

  • 正确:生成 JWT,返回给客户端
  • 错误:返回认证失败响应
  1. 服务端生成 JWT

服务端使用 密钥 对 payload 进行签名,生成一个完整的 token:

1
2
3
bashCopyEditeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.    # Header
eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJ0b20iLCJleHAiOjE3MTM1NjgwMDB9. # Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # Signature

服务端此后 不再保存用户状态,所有认证信息都由 token 自带。

  1. 客户端保存 JWT

客户端收到 token 后,通常将其存储在:

  • localStorage / sessionStorage
  • cookie(慎用,需设置 HttpOnlySecure
  1. 客户端携带 JWT 访问资源

客户端每次请求受保护的资源时,在请求头中携带 token:

1
Authorization: Bearer <token>
  1. 服务端验证 JWT
  • 服务端提取 token,验证签名是否合法、是否过期。
  • 若合法,解析 payload,拿到 userId 等信息,并执行业务逻辑。

🔐 JWT 的结构

JWT 是一个由三部分组成的字符串,用 . 分隔:

  1. Header(头部)

描述签名的算法及类型,通常是这样的:

1
2
3
4
jsonCopyEdit{
"alg": "HS256",
"typ": "JWT"
}
  1. Payload(有效载荷)

存放业务数据,不应包含敏感信息,因为它是明文的。常见字段:

字段含义
sub主题(Subject)
exp过期时间(Expiration Time)
iat签发时间(Issued At)
userId自定义字段,通常是用户唯一标识
roles自定义字段,表示用户权限角色

示例:

1
2
3
4
5
{
"userId": 123,
"username": "tom",
"exp": 1713568000
}
  1. Signature(签名)

由 header 和 payload 使用密钥 secret 签名生成,用于防篡改。

1
2
3
4
javaCopyEditHMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)

🧾 JWT 优点

  • 无需在服务端存储 Session,实现 无状态认证
  • 可跨服务、跨域使用(适合微服务)
  • 自带用户信息,减少查库压力
  • 易扩展,可加入权限、组织、平台等字段

⚠️ 安全建议

  • token 不要放敏感信息(明文可读)
  • 设置合理的 过期时间
  • 通过 HTTPS 传输,防止中间人攻击
  • 使用 HttpOnly + Secure 的 cookie 保存(如 SSR)

接口文档

开放接口规范有Swagger(springfox)和OpenAPI(目前常用).

可以使用springdoc-openapi或Knife4j工具通过添加注解生成规范

springdoc/springdoc-openapi: Library for OpenAPI 3 with spring-boot

快速开始 | Knife4j

image-20250422195017608

新增员工

增加mapper的插入语句增加员工信息,注意插入错误处理.

以及通过interceptor,threadlocal存储登录信息.

分页查询员工

利用mybatis的pagehelper插件,其通过拦截执行的查询语句修改其中的LIMIT返回结果.首先设置页大小和需要查询的页.

  1. PageHelper.startPage(pageNum, pageSize)

用于设置当前页码和每页条数,必须在执行查询语句之前调用

1
2
3
PageHelper.startPage(1, 10); // 第1页,每页10条
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
  1. PageHelper.offsetPage(offset, limit)

按偏移量方式分页,适合流式加载等场景。

1
2
PageHelper.offsetPage(20, 10); // 跳过前20条,查询10条
List<User> users = userMapper.selectAll();

然后在mapper中的sql语句中直接写查询条件,返回Page结果.

问题/注意点说明
startPage 必须紧跟查询语句否则分页不起作用(建议不要有中间处理逻辑)
不支持多线程共享分页上下文每次分页只作用于当前线程

Page<T>:继承自 ArrayList<T>,直接包含结果数据 + 分页信息;

PageInfo<T>:是一个额外封装类,包含分页信息(适合返回给前端);

POJO中日期序列化

image-20250423140945684

在 Spring Boot 项目中,如果你使用的是 Jackson(Spring Boot 默认的 JSON 序列化库),可以通过配置 ObjectMapperapplication.yml 来自定义 LocalDateTime / LocalDate / LocalTime 的序列化格式

✅ 方法一:在全局 ObjectMapper 中注册时间模块(推荐)

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
@Configuration
public class JacksonConfig {

@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();

JavaTimeModule javaTimeModule = new JavaTimeModule();
// LocalDateTime
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

// LocalDate
javaTimeModule.addSerializer(LocalDate.class,
new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

// LocalTime
javaTimeModule.addSerializer(LocalTime.class,
new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

mapper.registerModule(javaTimeModule);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 防止序列化为时间戳

return mapper;
}
}

✅ 方法二:使用 @JsonFormat 注解在字段上局部配置

适合只对个别字段格式化时使用:

1
2
3
4
5
6
7
8
9
@Data
public class MyDto {

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createdTime;

@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate date;
}

Spring Cache

image-20250429162721846

Spring Task

image-20250430210633413

Websocket主动推送订单消息

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

ServerEndpointExporter 会自动扫描所有 @ServerEndpoint 注解的类。

注册到 Servlet 容器的 WebSocket 运行时(ServerContainer)

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
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
@ServerEndpoint("/ws/students")
public class StudentWebSocketEndpoint {
// 存储所有连接的会话(线程安全)
private static final CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>();
@Autowired
private StudentService studentService;

@OnOpen
public void onOpen(Session session) {
sessions.add(session);
try {
session.getBasicRemote().sendText("Connected to Student WebSocket");
} catch (IOException e) {
e.printStackTrace();
}
}

@OnMessage
public void onMessage(String message, Session session) throws IOException {
// 收到客户端消息,广播给所有连接
for (Session s : sessions) {
s.getBasicRemote().sendText("Message: " + message);
}
}

@OnClose
public void onClose(Session session) {
sessions.remove(session);
}

@OnError
public void onError(Session session, Throwable throwable) {
throwable.printStackTrace();
}

// 广播学生更新
public void broadcastStudentUpdate(String message) {
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

Apache Echarts展示信息

Apache POI

黑马点评

缓存作用: 降低后端负载,提升读写速度

开发成本和维护一致性问题

Redis学习

Jedis guide (Java) | Docs

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.2.0</version>
</dependency>
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
public class JedisTest {
private Jedis jedis;

@BeforeEach
public void setUp() {
// 初始化 Jedis 连接
jedis = new Jedis("localhost", 6379); // 假设 Redis 服务运行在本地,默认端口为 6379
System.out.println("Connected to Redis");
// 清空 Redis 数据库,确保测试环境干净
jedis.flushAll();
}

// 在每个测试方法执行之后运行
@AfterEach
public void tearDown() {
// 关闭 Jedis 连接
if (jedis != null) {
jedis.close();
System.out.println("Disconnected from Redis");
}
}

@Test
public void testString() {
String result = jedis.set("name","proanimer");
System.out.println("result = " + result);
String name = jedis.get("name");
System.out.println("name = " + name);
}

@Test
public void testHash() {
jedis.hset("user:1","name","proanimer");
jedis.hset("user:2", "age", "24");
}

}

image-20250412221454173

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
//配置连接池
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMinIdle(0);
jedisPoolConfig.setMaxWait(Duration.of(10, ChronoUnit.SECONDS));
// 创建连接池
jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379);
}

public static Jedis getJedis() {
return jedisPool.getResource();
}
}

Spring Data Redis

Spring Data Redis

image-20250413141133598

序列化器

在使用 Spring Data Redis 时,序列化器(Serializer)用于将 Java 对象转换为适合存储在 Redis 中的格式(如字节数组),并在从 Redis 读取数据时将其反序列化回 Java 对象。选择合适的序列化器对于确保数据正确性以及优化性能非常重要。默认序列化器是JDK序列化器.

1. JdkSerializationRedisSerializer

  • 描述:这是默认的序列化器,使用 Java 的序列化机制来处理对象。
  • 优点:支持任意类型的 Java 对象。
  • 缺点:生成的数据较大,效率较低,并且只有在同一 JVM 环境下才能正确反序列化。
1
2
3
4
5
6
7
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new JdkSerializationRedisSerializer());
return template;
}

2. StringRedisSerializer

  • 描述:专门用于字符串的序列化器,能够高效地处理字符串类型的数据。
  • 优点:简单、快速,适用于大多数键值对场景。
  • 缺点:仅限于字符串类型的数据。
1
2
3
4
5
6
@Bean
public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(factory);
return template;
}

3. GenericJackson2JsonRedisSerializer

  • 描述:使用 Jackson 库将对象序列化为 JSON 格式。
  • 优点:易于阅读和调试,支持复杂对象结构。
  • 缺点:相对于其他二进制格式(如 Protocol Buffers),JSON 的体积更大,解析速度较慢。
1
2
3
4
5
6
7
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}

4. Jackson2JsonRedisSerializer

  • 描述:类似于 GenericJackson2JsonRedisSerializer,但它允许你指定序列化的具体类型。
  • 优点:可以更精确地控制序列化过程。
  • 缺点:需要提前知道序列化对象的确切类型。
1
2
3
4
5
6
7
8
@Bean
public RedisTemplate<String, MyObject> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, MyObject> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<MyObject> serializer = new Jackson2JsonRedisSerializer<>(MyObject.class);
template.setValueSerializer(serializer);
return template;
}

5. OxmSerializer

  • 描述:用于 XML 数据的序列化/反序列化。
  • 优点:适用于需要以 XML 格式存储数据的场景。
  • 缺点:XML 数据通常比 JSON 更大,处理速度也较慢。
1
2
3
4
5
6
7
8
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
OxmSerializer serializer = new Jaxb2Marshaller(); // 示例使用 JAXB
template.setValueSerializer(serializer);
return template;
}

6. 自定义序列化器

根据业务需求,你也可以实现自己的序列化器,只需要实现 RedisSerializer<T> 接口即可。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomRedisSerializer implements RedisSerializer<MyCustomType> {

@Override
public byte[] serialize(MyCustomType t) throws SerializationException {
// 实现序列化逻辑
return new byte[0];
}

@Override
public MyCustomType deserialize(byte[] bytes) throws SerializationException {
// 实现反序列化逻辑
return null;
}
}

// 在配置中使用自定义序列化器
@Bean
public RedisTemplate<String, MyCustomType> customRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, MyCustomType> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new CustomRedisSerializer());
return template;
}

image-20250413160821439

基于Session的登陆

image-20250320175751672

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
 @Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验
/* String phoneRegex = "^1[3-9]\\d{9}$";
if (!phone.matches(phoneRegex)) {
return Result.fail("手机号格式错误");
}*/
boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
// 2.如果不符合
if (phoneInvalid) {
return Result.fail("手机号格式错误");
}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到session
session.setAttribute("code", code);
// 5.发送验证码
log.debug(StrUtil.format("发送验证码成功,验证码:{}", code));
// 返回ok
return Result.ok();
}

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号和验证码
String phone = loginForm.getPhone();
boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
if (phoneInvalid) {
return Result.fail("手机号格式错误");
}
String code = loginForm.getCode();
String codeInSession = (String) session.getAttribute("code");
if (!code.equals(codeInSession)) {
return Result.fail("验证码错误");
}
// 2.查询用户
User user = query().eq("phone", phone).one();
if (user == null) {
// 不存在 创建用户
user = createUserWithPhone(phone);
}
// 3.保存用户信息到session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}

private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(8));
save(user);
return user;
}

集群的session共享问题

image-20250414154105418

基于Redis的短信登陆

image-20250414161242250

优化拦截器

image-20250414210106336

原本的拦截器只拦截需要权限的controller,但是如果已经有cookie的用户只访问不需要权限的controller就不会更新redis.

image-20250414210226312

商户查询缓存

image-20250414220321389

image-20250414233753517

缓存更新策略

image-20250415103603707

image-20250415103926298

策略读操作写操作优点缺点适用场景
Cache Aside先查缓存,再查数据库更新数据库后删除缓存简单、灵活、一致性较好存在短暂不一致、未命中时性能较差数据读多写少、一致性要求不高
Read/Write Through缓存负责未命中处理缓存负责同步到数据库透明性好、一致性好复杂性高、可能成为性能瓶颈数据一致性要求高
Write Behind Caching先查缓存,再查数据库异步批量写回数据库写性能高、吞吐量大数据丢失风险、一致性差数据写多读少、一致性要求低

image-20250415105910579

image-20250415111429325

image-20250415111703513

名称触发场景结果常见解决方案
缓存穿透请求的数据本就不存在(DB 也无)每次请求都打到数据库缓存空值、布隆过滤器
缓存击穿某个热点 key 恰好过期了大量请求同时访问 DB,瞬时压力大加互斥锁、热点预热
缓存雪崩大量 key 在同一时间过期缓存失效,数据库压力激增加随机过期时间、限流、降级

布隆过滤器是基于一个 bit 数组 + 多个 哈希函数

  1. 初始创建一个很大的 bit 数组(如 1 亿位,全是 0)。
  2. 插入元素时,用多个哈希函数对元素哈希,得到多个下标位置,把这些位置设为 1。
  3. 查询时,对待查元素用相同的哈希函数求下标:
    • 若所有对应 bit 位都是 1 → 可能存在
    • 有任意一个 bit 是 0 → 一定不存在

image-20250415133050673

image-20250415143221507

image-20250415144016106

image-20250415144408811

image-20250415144944179

image-20250415145118864

优惠券秒杀

全局ID生成器,在分布式系统下用来生成全局唯一ID的工具.

满足:唯一性,高可用,高性能,递增性,安全性.

image-20250415194042557

image-20250415231649294

image-20250416162534054

悲观锁 乐观锁

image-20250416192913692

乐观锁的关键是判断之前查询得到的数据是否被修改过.常见方式:

  1. 版本号法

给数据添加版本号,每次更新的时候查询数据对应的版本,如果版本号跟之前的不同则表明更新过了.

image-20250416194812142

  1. CAS法

image-20250416212059026

一人一单,使用悲观锁,加锁. 但在分布式系统下,多个实例下进程不相干,无法进行线程同步,需要实现分布式锁.

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

image-20250417095719500

基于Redis的分布式锁

使用setnx

image-20250417124148443

注意如果出现业务耗时超过key的ttl,导致其他线程拿到锁,在删除锁时检查value是否一致。

redis lua脚本

image-20250417133641854

image-20250417143320701

Redisson可重入锁

image-20250417152425433

可重试/更新超时时间

image-20250417181249028

image-20250417181558824

所以利用redis缓存作分布式锁的需要核心解决的可重入超时重试机制.

主从一致性问题

一、单机多实例(适合开发和测试环境)

配置不同的实例端口,多个配置文件启动多个实例.

二、多机集群,在每台服务器上创建一个 Redis 配置文件(如 redis-cluster.conf),并添加以下内容:

1
2
3
4
5
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
  • cluster-enabled yes:启用集群模式。
  • cluster-config-file nodes.conf:指定集群节点配置文件。
  • cluster-node-timeout 5000:设置节点超时时间(毫秒)

image-20250417232039727

image-20250418111401540

image-20250418143910127

image-20250418182224084

Redis消息队列

image-20250418200244539

基于list数据结构

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)一个列表最多可以包含 2^32^ -1个元素。主要利用BRPOP移除列表元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止. 同时通过LPUSH添加值.

image-20250418203901472

pubsub 点对点消息消息模型

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

Redis 客户端可以订阅任意数量的频道。

img

img

image-20250418210559430

image-20250418210832702

Stream

Redis Stream 是 Redis 5.0 版本新增加的数据结构。

Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃

简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。

而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

img

  • Stream: 在 Redis 中,一个 Stream 就是一个追加日志类型的键值对集合。
  • Entry: 每个流中的元素称为 Entry 或者 Message,由唯一标识符(ID)和数据字段组成。
  • Consumer Group: 允许不同的消费者组从同一个流中读取消息,每个组可以独立地跟踪自己已经消费的消息位置。
  • ID: 每条消息都有一个唯一的 ID,格式为 <timestamp>-<sequence>,其中时间戳是消息添加时的时间,序列号用于区分同一毫秒内添加的消息。

image-20250418223626722

image-20250418224505431

基于Stream的消息队列-消费者组

image-20250418225321965

给消费者分类,消息漏读,消息确认避免消息丢失.

命令作用
XADD添加消息到 Stream
XRANGE / XREVRANGE范围读取消息(正/反向)
XREAD阻塞或非阻塞读取消息
XGROUP CREATE创建消费者组
XREADGROUP按消费者组读取消息
XACK确认消息已处理
XPENDING查看待处理(未 ack)消息
XDEL删除指定消息
XTRIM裁剪旧消息,控制 Stream 大小
XLEN获取 Stream 长度
XINFO获取 Stream / Consumer 详细信息

创建组

1
XGROUP CREATE mystream mygroup $ MKSTREAM

创建名为 mygroup 的消费者组,$ 从最新消息开始消费,MKSTREAM 可自动创建 Stream。

读取消息

1
XREADGROUP GROUP mygroup consumer1 COUNT 2 STREAMS mystream >

> 表示读取尚未分配的消息(新消息)。

消息确认

1
XACK mystream mygroup 1686900000000-0

查看未确认消息

1
XPENDING mystream mygroup
命令说明
XINFO STREAM mystream查看 stream 本体信息
XINFO GROUPS mystream查看所有消费者组信息
XINFO CONSUMERS mystream mygroup查看某个消费者组中各个消费者状态

image-20250418233032176

消费者组中的多消费者争抢消息体现在

在 Redis Stream 中使用 消费者组(Consumer Group) 时,有个关键的机制是:

同一个消费者组内,一个消息只会被分配给一个消费者处理。

这意味着:

  • 如果消费者 A 已经读取并 ack(确认)了一条消息,那么:

    • 同组内的消费者 B 是 无法再读取这条消息 的。
    • 除非你专门指定消息 ID 重新读取(例如用 XREADGROUP + 指定 ID)。
  • 如果你希望 消费者 B 能“读到之前被其他消费者已处理的消息”,你必须显式指定 ID,并该消息未被 ack或使用 XPENDING 查找。

    ✅如何让消费者读取“历史消息”?

    场景 1:消息还未被 ack(pending)

    可以通过 XPENDING + XCLAIM 把消息“抢过来”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    javaCopyEditPendingMessages pending = stringRedisTemplate.opsForStream()
    .pending("mystream", "mygroup", Range.unbounded(), 10);

    for (PendingMessage message : pending) {
    // 把未 ack 的消息交给当前消费者
    List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().claim(
    Consumer.from("mygroup", "consumer2"),
    Duration.ofSeconds(5),
    message.getId()
    );
    }

    场景 2:消息已被 ack,想重复读取

    Redis 默认设计下是不会让你“重复消费”被 ack 的消息的,但你可以手动读取它(不是 group 模式)

    1
    2
    3
    // 不使用消费者组,直接用 XREAD + ID
    List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream()
    .read(StreamOffset.fromStart("mystream")); // 或者用具体 ID

    也可以用 XRANGE 来精确读取:

    1
    2
    3
    // 获取某条历史消息
    stringRedisTemplate.opsForStream()
    .range("mystream", Range.closed("1682390889639-0", "1682390889639-0"));

    | 场景 | 是否能重新读取 |
    | —————————————————- | ——————————————— |
    | 消费者组内,消息已被 ack | ❌(除非用非 group 方式手动读) |
    | 消费者组内,消息未被 ack(pending) | ✅(可以用 XCLAIM 抢回来) |
    | 想让多个消费者都能读一条消息 | ❌(组内不支持;需非 group 读) |

image-20250418233143096

image-20250419131039088

image-20250419131059130

达人探店

点赞功能

image-20250420143555600

点赞排行榜

image-20250420144640978

好友关注

写两个接口,一个查看是否关注,另一个进行关注或取关.

关注的数据表设计为user_id和follower_id. 为一个关注记录

image-20250420200822771

image-20250420200856032

共同关注

image-20250420200600057

在新增关注时添加缓存,同时利用redis中的set交集操作在缓存中得到共同关注

关注推送

image-20250420220334492

image-20250421092155097

通过推模式,通过分页滚动读取关注用户发布的博客数据. 用户发布博客时将博客id加入关注自己的粉丝的收件箱, 使用sorted set,以时间戳为score(即推模式)

image-20250421224913326

关键是利用最新的时间戳去拿最新的博客id,同时利用偏移量滤去相同的时间戳(默认不会重复). 如果考虑发布时间重复,也可以在存储score时在时间戳基础上加一个随机值避免score重复.

附近商户

Redis GEO | 菜鸟教程

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。

Redis GEO 操作方法有:

  • geoadd:添加地理位置的坐标。
  • geopos:获取地理位置的坐标。
  • geodist:计算两个位置之间的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:返回一个或多个位置对象的 geohash 值。

geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。

geoadd 语法格式如下:

1
GEOADD key longitude latitude member [longitude latitude member ...]
  • m :米,默认单位。
  • km :千米。
  • mi :英里。
  • ft :英尺。
  • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
  • WITHCOORD: 将位置元素的经度和纬度也一并返回。
  • WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
  • COUNT 限定返回的记录数。
  • ASC: 查找结果根据距离从近到远排序。
  • DESC: 查找结果根据从远到近排序。

用户签到

image-20250422105634699

image-20250422111632294

1
2
3
4
5
6
7
8
9
10
LocalDateTime now = LocalDateTime.now();
int dayOfYear = now.getDayOfYear();
int year = now.getYear();
String key = SIGN_KEY + UserHolder.getUser().getId()+ ":" + year;
Boolean signSuccess = stringRedisTemplate.opsForValue().setBit(key, dayOfYear-1, true);
if (BooleanUtil.isTrue(signSuccess)) {
return Result.ok();
}else{
return Result.fail("签到失败");
}

签到统计

使用bitfield查询一个范围内的二进制返回十进制数据.

image-20250422121102888

UV统计 HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

image-20250422121540587

image-20250422122336390

EasyChat

登录注册

image-20250502155041889

image-20250502195913653

群组管理

image-20250502195944284

image-20250502200016447

添加联系人

image-20250505174711397

联系人详情,删除和拉黑联系人

获取用户信息、修改密码与退出登录

后台管理

用户管理 靓号管理 群组管理

EasyLive

EasyPan

03.Java项目创建

IDE设置

JDK位置设置

image-20250514230635404

编译器自动构建与热交换

image-20250514230730743

image-20250514230903350

image-20250514230936534

设置Maven位置

image-20250514231336188

设置文件编码

image-20250514231119345

创建工程

image-20250514231258500

或创建Spring Boot项目

配置文件

POM.xml

项目基本信息
  • <modelVersion>:指定 POM 模型的版本,通常为 4.0.0
  • <groupId>:定义项目所属的组织或公司,通常使用反向域名表示。
  • <artifactId>:项目的唯一标识符,通常对应项目名称。
  • <version>:项目的当前版本号。
  • <packaging>:指定项目的打包方式,如 jarwar 等。
  • <name>:项目的名称。
  • <description>:项目的简要描述。
继承与模块管理
  • <parent>:指定当前项目继承的父 POM,便于共享统一的配置和依赖管理。
  • <modules>:在多模块项目中,列出所有子模块的目录名称。
依赖管理
  • <dependencies>:列出项目所需的所有依赖项。
  • <dependencyManagement>:用于统一管理依赖的版本信息,子项目可以引用而无需指定版本。
  • <repositories>:指定额外的远程仓库地址,以获取依赖。
构建配置
  • <build>:定义项目的构建相关配置。

    • <sourceDirectory>:指定源代码目录。
    • <outputDirectory>:指定编译输出目录。
    • <plugins>:配置构建过程中使用的插件,如 maven-compiler-plugin 等。

    默认目录结构

    • 主源代码目录src/main/java
    • 主资源目录src/main/resources
    • 测试源代码目录src/test/java
    • 测试资源目录src/test/resources
    • 构建输出目录target/
    • 主类输出目录target/classes
    • 测试类输出目录:`target/test-classes

    这些默认设置源自于 Maven 的 Super POMMaven Model Builder – Super POM,所有项目在未显式配置的情况下都会继承这些设置

    自定义输入目录

    如果项目结构不同于 Maven 的默认结构,可以在 pom.xml 中自定义输入目录。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <build>
    <sourceDirectory>src/my-src</sourceDirectory>
    <testSourceDirectory>src/my-test</testSourceDirectory>
    <resources>
    <resource>
    <directory>src/my-resources</directory>
    </resource>
    </resources>
    </build>

    上述配置将主源代码目录更改为 src/my-src,测试源代码目录更改为 src/my-test,资源目录更改为 src/my-resources

属性定义
  • <properties>:定义可在 POM 中引用的变量,便于统一管理版本号等信息。
构建环境与发布配置
  • <profiles>:定义不同的构建配置,便于在不同环境下使用。
  • <distributionManagement>:配置项目的发布信息,如部署到的仓库地址等。

Introduction to the POM – Maven

日志记录

Spring Boot 使用 Commons Logging 进行所有内部日志记录,但底层日志实现保持开放。为 Java Util Logging、Log4j2 和 Logback 提供了默认配置。在每种情况下,日志记录器都预先配置为使用控制台输出,同时也可以选择文件输出。

img

当前版本logback中重要组件包括appender,也就是配置日志的输出目的地,通过 name 属性指定名字,通过 class 属性指定目的地:

  • ch.qos.logback.core.ConsoleAppender:输出到控制台。
  • ch.qos.logback.core.FileAppender:输出到文件。
  • ch.qos.logback.core.rolling.RollingFileAppender:文件大小超过阈值时产生一个新文件。

encoder,logger以及root,它只支持一个属性——level,值可以为:TRACE、DEBUG、INFO、WARN、ERROR、ALL、OFF.

<property>:定义的变量可以在整个配置文件中通过 ${} 引用,便于维护和修改。

<appender>:定义日志的输出方式。

  • ConsoleAppender:将日志输出到控制台。
  • RollingFileAppender:将日志输出到文件,并支持按时间滚动生成新文件。

<logger>:为特定的包或类设置日志级别和输出方式。

<root>:定义默认的日志级别和输出方式,适用于未被其他 logger 捕获的日志。

pattern 用来指定日志的输出格式:

  • %d:输出的时间格式。
  • %thread:日志的线程名。
  • %-5level:日志的输出级别,填充到 5 个字符。比如说 info 只有 4 个字符,就填充一个空格,这样日志信息就对齐了。
  • %logger{length}:logger 的名称,length 用来缩短名称。没有指定表示完整输出;0 表示只输出 logger 最右边点号之后的字符串;其他数字表示输出小数点最后边点号之前的字符数量。
  • %msg:日志的具体信息。
  • %n:换行符。
  • %relative:输出从程序启动到创建日志记录的时间,单位为毫秒。

logback-spring.xml提供了<springProperty>以及<springProfile>可以读取springBoot配置文件中的属性.

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
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="stdot" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss,GMT+8} [%p][%c][%M][%L]-> %m%n</pattern>
</encoder>
</appender>

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/${LOG_FOLDER}/${LOG_FILE_NAME}</file>
<!-- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">-->
<!-- <fileNamePattern>${log.path}/${LOG_FOLDER}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i</fileNamePattern>-->
<!-- <totalSizeCap>5G</totalSizeCap>-->
<!-- <maxHistory>30</maxHistory>-->
<!-- </rollingPolicy>-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>${log.path}/${LOG_FOLDER}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i</fileNamePattern>
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss,GMT+8} [%p][%c][%M][%L]-> %m%n</pattern>
</encoder>
</appender>


<springProperty scope="context" name="log.path" source="project.folder"/>
<springProperty scope="context" name="log.root.level" source="log.root.level"/>

<property name="LOG_FOLDER" value="logs"/>
<property name="LOG_FILE_NAME" value="easypan.log"/>

<!-- <logger name="top.sekyoro.easypan" level="${log.root.level}">-->
<!-- <appender-ref ref="stdot"/>-->
<!-- <appender-ref ref="file"/>-->
<!-- </logger> -->
<!-- -->
<root level="${log.root.level}">
<appender-ref ref="stdot"/>
<appender-ref ref="file"/>
</root>
</configuration>

application.properties

服务器配置
  • server.port=8080:设置应用的端口号。
  • server.servlet.context-path=/api:设置应用的上下文路径。
应用信息
  • spring.application.name=myapp:设置应用的名称。
数据源配置(以 MySQL 为例)
  • spring.datasource.url=jdbc:mysql://localhost:3306/db_example
  • spring.datasource.username=root
  • spring.datasource.password=secret
  • spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  • spring.jpa.hibernate.ddl-auto=update:设置 JPA 的 DDL 策略。
日志配置
  • logging.level.root=INFO:设置根日志级别。
  • logging.level.com.example=DEBUG:设置特定包的日志级别。
  • logging.file.name=logs/app.log:设置日志文件名称。
  • logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n:设置控制台日志输出格式
邮件配置
  • spring.mail.host=smtp.example.com
  • spring.mail.port=587
  • spring.mail.username=user@example.com
  • spring.mail.password=secret
  • spring.mail.properties.mail.smtp.auth=true
  • spring.mail.properties.mail.smtp.starttls.enable=true
安全配置
  • spring.security.user.name=admin
  • spring.security.user.password=secret
  • spring.security.user.roles=USER,ADMIN
缓存配置
  • spring.cache.type=simple:设置缓存类型。
  • spring.cache.cache-names=users,transactions:定义缓存名称。
国际化配置
  • spring.messages.basename=messages:设置消息资源文件的基础名称。
  • spring.messages.encoding=UTF-8:设置消息资源文件的编码。
测试配置
  • spring.main.allow-bean-definition-overriding=true:允许覆盖 Bean 定义。
  • spring.profiles.active=dev:设置活动的配置文件。

注意:1.新版本中,spring.mvc.throw-exception-if-no-handler-found 属性已被弃用,建议不再使用。默认情况下,Spring Boot 会返回 404 响应,无需额外配置。

2.spring.mvc.favicon.enable=false配置属性已弃用。此外,Spring Boot 不再提供默认的 favicon,因为此图标可被视为信息泄露,可以增加对应handler.

1
2
3
4
5
6
7
8
@Configuration
public class FaviconConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/favicon.ico")
.addResourceLocations("classpath:/static/");
}
}

Spring Boot中favicon的指南 | Baeldung中文网

相关文档

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
# 应用服务 WEB 访问端口
server.port=7090
server.servlet.context-path=/api
#session过期时间 60M 一个小时
server.servlet.session.timeout=PT60M
#处理favicon
#spring.mvc.favicon.enable=false
#异常处理
#spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false
#数据库配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/easypan?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.pool-name=HikariCPDatasource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=180000
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
#发送邮件配置相关
# 配置邮件服务器的地址 smtp.qq.com
spring.mail.host=smtp.qq.com
# 配置邮件服务器的端口(465或587)
spring.mail.port=465
# 配置用户的账号
spring.mail.username=test@qq.com
# 配置用户的密码
spring.mail.password=123456
# 配置默认编码
spring.mail.default-encoding=UTF-8
# SSL 连接配置
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# 开启 debug,这样方便开发者查看邮件发送日志
spring.mail.properties.mail.debug=true
#邮件配置结束
#Spring redis配置
# Redis数据库索引(默认为0)
spring.data.redis.database=0
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制)
spring.data.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.data.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.data.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.data.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.data.redis.timeout=2000

参考博主

程序员老罗的个人空间-程序员老罗个人主页-哔哩哔哩视频

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道