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
jsonCopyEdit{
"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;
}

黑马点评

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

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

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
2
3
4
5
bash


CopyEdit
XGROUP CREATE mystream mygroup $ MKSTREAM

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

读取消息

1
2
3
4
5
bash


CopyEdit
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

瑞吉外卖

谷粒商城

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

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