Redis的实战Demo

Springboot+Redis的一个小Demo,边实战边学习

前言

实践是检验真理的唯一标准,比起背八股学理论知识,不如先在网上找个小Demo看看怎么个事(前提是你了解过SpringBoot和Mysql,对这类后端+数据库有一定理解)。

前面我们也了解到,Redis与Mysql最大的不同就是它的键值对存储方式,这也将在下面的代码中得以显现。本文将围绕Demo中的代码,尽可能去解释代码都干了什么,并基于Demo提炼其中的知识点,提供照葫芦画瓢的“葫芦”。

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
springboot-redis-demo
├── src/main/java/com/example/demo/
│ ├── config/
│ │ └── RedisConfig.java
│ ├── controller/
│ │ └── UserController.java
│ ├── entity/
│ │ └── User.java
│ ├── service/
│ │ ├── UserService.java
│ │ └── impl/
│ │ └── UserServiceImpl.java
│ └── DemoApplication.java
├── src/main/resources/
│ └── application.yml
└── pom.xml

详细代码

RedisConfig

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
package com.redis_demo.config;

// import xxx

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// 配置Jackson序列化器
ObjectMapper objectMapper = new ObjectMapper();
// 注册JavaTimeModule以支持LocalDateTime等Java 8日期时间类型
objectMapper.registerModule(new JavaTimeModule());
// 开启默认类型识别,解决反序列化类型问题
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);

GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);

//序列化就是将实体类转换为能够被存储的json,xml等形式

// 使用StringRedisSerializer来序列化和反序列化redis的key值
//键的
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());

// 使用配置好的GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value值
//值的
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);

template.afterPropertiesSet();
return template;
}
}

RedisConfig是 Redis 的核心配置类,用于自定义 RedisTemplate(Spring Data Redis 的核心操作工具),解决 Java 对象与 Redis 存储格式的转换问题。

说白了就是不同于Mysql的关系型数据库,一个select和多个join下来才能出一个类对象,Redis是键值对的存储形式,也就是说value不能是一个类对象,而可以是json,RedisConfig的作用就是进行二者之间的格式转换。

application.properties

1
2
3
4
5
6
7
8
9
10
11
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0

spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-wait=-1ms

server.port=8080

UserController UserService User类

和正常的SpringBoot项目流程一样,没有区别:

UserController

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
package com.redis_demo.controller;

import com.redis_demo.entity.User;
import com.redis_demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
import java.util.UUID;

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

@PostMapping
public User createUser(@RequestBody User user) {
return userService.saveUser(user);
}

@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}

@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.updateUser(user);
}

@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return "用户删除成功";
}

@PostMapping("/{userId}/browse/{productId}")
public String addBrowseHistory(@PathVariable Long userId, @PathVariable String productId) {
userService.addBrowseHistory(userId, productId);
return "浏览记录添加成功";
}

@GetMapping("/{userId}/browse-history")
public List<String> getBrowseHistory(@PathVariable Long userId,
@RequestParam(defaultValue = "10") int count) {
return userService.getBrowseHistory(userId, count);
}

@PostMapping("/{username}/online")
public String userOnline(@PathVariable String username) {
((com.redis_demo.service.impl.UserServiceImpl) userService).userOnline(username);
return "用户上线成功";
}

@PostMapping("/{username}/offline")
public String userOffline(@PathVariable String username) {
((com.redis_demo.service.impl.UserServiceImpl) userService).userOffline(username);
return "用户下线成功";
}

@GetMapping("/online")
public Set<String> getOnlineUsers() {
return userService.getOnlineUsers();
}

@GetMapping("/count")
public Long getUserCount() {
return userService.getUserCount();
}

// 分布式锁测试接口
@PostMapping("/lock-test")
public String testDistributedLock() {
String lockKey = "test_task";
String requestId = UUID.randomUUID().toString();

try {
// 尝试获取锁,有效期10秒
if (userService.tryLock(lockKey, requestId, 10)) {
// 模拟业务处理
Thread.sleep(3000);
return "业务处理完成";
} else {
return "获取锁失败,请重试";
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "处理中断";
} finally {
// 释放锁
userService.releaseLock(lockKey, requestId);
}
}
}

UserService

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
package com.redis_demo.service;

import com.redis_demo.entity.User;
import java.util.List;
import java.util.Set;

public interface UserService {

// 用户相关操作
User saveUser(User user);
User getUserById(Long id);
void deleteUser(Long id);
User updateUser(User user);

// 浏览记录操作
void addBrowseHistory(Long userId, String productId);
List<String> getBrowseHistory(Long userId, int count);

// 分布式锁示例
boolean tryLock(String key, String value, long expireTime);
boolean releaseLock(String key, String value);

// 统计用户数量
Long getUserCount();

// 获取在线用户
Set<String> getOnlineUsers();
}

User

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
package com.redis_demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public LocalDateTime getCreateTime() {
return createTime;
}

public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}

private Long id;
private String username;
private String email;
private Integer age;
private LocalDateTime createTime;

public User(Long id, String username, String email, Integer age) {
this.id = id;
this.username = username;
this.email = email;
this.age = age;
this.createTime = LocalDateTime.now();
}
}

UserServiceImpl

到了这里就要真正去了解Redis的交互操作了。
在使用Mysql作为数据库的项目中,impl层的书写无非就是写写SQL语句,做一做变量的映射、类的处理。
然而在Redis中,正如前面说的它是基于键值对的数据库,存储的信息其实是高度集成(且冗余?)的,更多的操作是简单的基于key的增删改查以及设置过期时间等,并且事实上Redis也就用于处理这些,作为项目中主数据库的缓存去做简单而又大量的查找和删除工作。

我们的这个Demo是以Redis为主数据库的,只是单纯方便学习,如果不做持久化配置,数据可能丢失。并且也几乎不会有以Redis为项目主数据库的应用场景。Redis更多作为项目某一模块的数据库使用。

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package com.redis_demo.service.impl;

import com.redis_demo.entity.User;
import com.redis_demo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class UserServiceImpl implements UserService {

private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

@Autowired
private RedisTemplate<String, Object> redisTemplate;

// Key前缀常量
private static final String USER_KEY_PREFIX = "user:";
private static final String BROWSE_HISTORY_KEY_PREFIX = "browse:history:";
private static final String USER_COUNT_KEY = "user:count";
private static final String ONLINE_USERS_KEY = "online:users";

@Override
public User saveUser(User user) {
String key = USER_KEY_PREFIX + user.getId();

// 存储用户信息到Redis
redisTemplate.opsForValue().set(key, user);

// 设置过期时间(24小时)
redisTemplate.expire(key, 24, TimeUnit.HOURS);

// 增加用户计数
redisTemplate.opsForValue().increment(USER_COUNT_KEY);

logger.info("用户保存成功: {}", user.getUsername());
return user;
}

@Override
public User getUserById(Long id) {
String key = USER_KEY_PREFIX + id;
User user = (User) redisTemplate.opsForValue().get(key);

if (user != null) {
logger.info("从Redis缓存获取用户: {}", user.getUsername());
} else {
logger.info("用户不存在: {}", id);
}
return user;
}

@Override
public void deleteUser(Long id) {
String key = USER_KEY_PREFIX + id;
Boolean deleted = redisTemplate.delete(key);

if (Boolean.TRUE.equals(deleted)) {
// 减少用户计数
redisTemplate.opsForValue().decrement(USER_COUNT_KEY);
logger.info("用户删除成功: {}", id);
}
}

@Override
public User updateUser(User user) {
String key = USER_KEY_PREFIX + user.getId();

// 更新用户信息
redisTemplate.opsForValue().set(key, user, 24, TimeUnit.HOURS);

logger.info("用户更新成功: {}", user.getUsername());
return user;
}

@Override
public void addBrowseHistory(Long userId, String productId) {
String key = BROWSE_HISTORY_KEY_PREFIX + userId;

// 使用ZSet存储浏览记录,score为时间戳,实现按时间排序
double score = System.currentTimeMillis();

// 添加浏览记录
redisTemplate.opsForZSet().add(key, productId, score);

// 只保留最近50条记录
redisTemplate.opsForZSet().removeRange(key, 0, -51);

// 设置过期时间(30天)
redisTemplate.expire(key, 30, TimeUnit.DAYS);

logger.info("添加浏览记录: 用户{}, 商品{}", userId, productId);
}

@Override
public List<String> getBrowseHistory(Long userId, int count) {
String key = BROWSE_HISTORY_KEY_PREFIX + userId;

// 按score倒序获取浏览记录(最新的在前面)
Set<ZSetOperations.TypedTuple<Object>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, count - 1);

List<String> history = new ArrayList<>();
if (tuples != null) {
for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
history.add((String) tuple.getValue());
}
}

return history;
}

@Override
public boolean tryLock(String key, String value, long expireTime) {
String lockKey = "lock:" + key;

// 使用SETNX命令实现分布式锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);

if (Boolean.TRUE.equals(success)) {
logger.info("获取锁成功: {}", lockKey);
return true;
}

logger.info("获取锁失败: {}", lockKey);
return false;
}

@Override
public boolean releaseLock(String key, String value) {
String lockKey = "lock:" + key;

// 验证锁的值是否匹配,防止误删其他客户端的锁
Object currentValue = redisTemplate.opsForValue().get(lockKey);
if (value.equals(currentValue)) {
Boolean deleted = redisTemplate.delete(lockKey);
if (Boolean.TRUE.equals(deleted)) {
logger.info("释放锁成功: {}", lockKey);
return true;
}
}

logger.info("释放锁失败: {}", lockKey);
return false;
}

@Override
public Long getUserCount() {
Object count = redisTemplate.opsForValue().get(USER_COUNT_KEY);
return count != null ? Long.valueOf(count.toString()) : 0L;
}

@Override
public Set<String> getOnlineUsers() {
// 使用Set存储在线用户
Set<Object> members = redisTemplate.opsForSet().members(ONLINE_USERS_KEY);
Set<String> onlineUsers = new HashSet<>();

if (members != null) {
for (Object member : members) {
onlineUsers.add((String) member);
}
}

return onlineUsers;
}

// 用户上线
public void userOnline(String username) {
redisTemplate.opsForSet().add(ONLINE_USERS_KEY, username);
logger.info("用户上线: {}", username);
}

// 用户下线
public void userOffline(String username) {
redisTemplate.opsForSet().remove(ONLINE_USERS_KEY, username);
logger.info("用户下线: {}", username);
}
}

其实和正常的数据库操作逻辑是相通的

下面一部分来列举redisTemplate的操作

redisTemplate

RedisTemplate 是 Spring Data Redis 提供的一个核心类,它封装了 Redis 的各种操作,提供了更高级的抽象来与 Redis 交互。
下面列举常用的几个方法(具体的查手册吧,说不完,记不完,学不完):

String

设置值

1
2
3
redisTemplate.opsForValue().set("key", "value");
redisTemplate.opsForValue().set("key", "value", 10, TimeUnit.MINUTES); // 带过期时间
redisTemplate.opsForValue().multiSet(map); //批量Set,Map本身也是键值对

获取值

1
2
3
4
Object value = redisTemplate.opsForValue().get("key");   //键值对的值可以是很多数据类型
User user = (User) redisTemplate.opsForValue().get("key");
//在此时,从Redis拿数据时(get)已经经历了一次反序列化,将数据还原为了原来的数据类型
List<Object> values = redisTemplate.opsForValue().multiGet(Arrays.asList("key1", "key2")); //批量Get

数值操作

1
2
redisTemplate.opsForValue().increment("counter", 1);      // 增加1
redisTemplate.opsForValue().decrement("counter", 1); // 减少1

删除值

1
2
redisTemplate.delete(key);
redisTemplate.delete(keys); //批量删除,keys为Collection<K> keys

Hash

设置值

1
2
3
redisTemplate.opsForHash().put("user:1", "name", "张三");
redisTemplate.opsForHash().put("user:1", "age", "25");
redisTemplate.opsForHash().putAll("user:2", userMap); //userMap为<属性,属性值>的键值对

获取值

1
2
3
4
5
Object name = redisTemplate.opsForHash().get("user:1", "name");
String age = (String) redisTemplate.opsForHash().get("user:1", "age");
Map<Object, Object> allFields = redisTemplate.opsForHash().entries("user:1"); //获取所有字段
Set<Object> fields = redisTemplate.opsForHash().keys("user:1"); //获取所有字段名
List<Object> values = redisTemplate.opsForHash().values("user:1"); //获取所有字段值

删除字段

1
redisTemplate.opsForHash().delete("user:1", "age");

数值操作

1
2
redisTemplate.opsForHash().increment("user:1", "age", 1); // 年龄+1
redisTemplate.opsForHash().decrement("user:1", "age", 1); // 年龄-1

List

添加元素

1
2
3
4
5
6
// 从左端添加
redisTemplate.opsForList().leftPush("list1", "value1");
redisTemplate.opsForList().leftPushAll("list1", "value2", "value3", "value4");
// 从右端添加
redisTemplate.opsForList().rightPush("list1", "value5");
redisTemplate.opsForList().rightPushAll("list1", Arrays.asList("value6", "value7"));

弹出元素

1
2
3
4
Object leftPop = redisTemplate.opsForList().leftPop("list1");     // 左端弹出
Object rightPop = redisTemplate.opsForList().rightPop("list1"); // 右端弹出
// 阻塞弹出(等待时间)
Object leftPop = redisTemplate.opsForList().leftPop("list1", 10, TimeUnit.SECONDS);

获取元素

1
2
Object first = redisTemplate.opsForList().index("list1", 0); // 获取索引0的元素
List<Object> range = redisTemplate.opsForList().range("list1", 0, 2); // 获取0-2的元素

在指定元素前后插入

1
2
redisTemplate.opsForList().leftPush("list1", "pivot", "newValue"); // 在pivot前插入
redisTemplate.opsForList().rightPush("list1", "pivot", "newValue"); // 在pivot后插入

Set

添加元素

1
redisTemplate.opsForSet().add("set1", "a", "b", "c", "d");

获取所有元素

1
Set<Object> members = redisTemplate.opsForSet().members("set1");

移除元素

1
redisTemplate.opsForSet().remove("set1", "a", "b");

集合运算

1
2
3
Set<Object> union = redisTemplate.opsForSet().union("set1", "set2");        // 并集
Set<Object> intersect = redisTemplate.opsForSet().intersect("set1", "set2"); // 交集
Set<Object> difference = redisTemplate.opsForSet().difference("set1", "set2"); // 差集

ZSet

添加元素

1
2
redisTemplate.opsForZSet().add("zset1", "member1", 100.0);
redisTemplate.opsForZSet().add("zset1", "member2", 200.0);

批量添加

1
2
3
4
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
tuples.add(new DefaultTypedTuple<>("member3", 300.0));
tuples.add(new DefaultTypedTuple<>("member4", 400.0));
redisTemplate.opsForZSet().add("zset1", tuples);

获取单个元素的分数

1
Double score = redisTemplate.opsForZSet().score("zset1", "member1");

获取元素

1
2
3
4
5
6
7
8
// 按分数范围获取
Set<Object> range = redisTemplate.opsForZSet().range("zset1", 0, -1); // 所有元素
Set<Object> rangeByScore = redisTemplate.opsForZSet().rangeByScore("zset1", 100, 300);
// 按分数范围获取(带分数)
Set<ZSetOperations.TypedTuple<Object>> tuplesWithScores =
redisTemplate.opsForZSet().rangeWithScores("zset1", 0, -1);
// 倒序获取
Set<Object> reverseRange = redisTemplate.opsForZSet().reverseRange("zset1", 0, 2); // 前3名

获取排名

1
2
Long rank = redisTemplate.opsForZSet().rank("zset1", "member1");        // 正序排名(从0开始)
Long reverseRank = redisTemplate.opsForZSet().reverseRank("zset1", "member1"); // 倒序排名

移除元素

1
2
3
redisTemplate.opsForZSet().remove("zset1", "member1");
redisTemplate.opsForZSet().removeRange("zset1", 0, 2); // 移除排名0-2的元素
redisTemplate.opsForZSet().removeRangeByScore("zset1", 0, 100); // 移除分数0-100的元素