RedisTemplate 是 Spring Data Redis 提供的一个高级抽象,用于简化 Redis 操作。它封装了底层的 Redis 客户端(如 Jedis 或 Lettuce),提供了丰富的 API,使得在 Spring 应用中操作 Redis 变得更加方便和安全。
主要特性
-
类型安全:
RedisTemplate支持泛型,可以指定键和值的类型,从而避免了类型转换的错误。
-
丰富的操作方法:
- 提供了丰富的操作方法,支持 Redis 的各种命令,包括字符串、哈希、列表、集合、有序集合等。
-
自动序列化和反序列化:
- 支持自定义的序列化器和反序列化器,可以方便地处理复杂的对象。
-
事务支持:
- 支持 Redis 的事务功能,可以通过
multi和exec命令来执行事务。
- 支持 Redis 的事务功能,可以通过
-
连接池管理:
- 集成了连接池,可以高效地管理 Redis 连接。
配置 RedisTemplate
在 Spring Boot 应用中,RedisTemplate 通常通过配置类进行配置。
黑马点评
导入直接进行一个视频的看

ThreadLocal 是 Java 中的一个非常有用的类,用于创建线程局部变量。每个线程都有自己的独立变量副本,这些变量对其他线程不可见。这在多线程编程中非常有用,尤其是在需要为每个线程维护独立状态的场景中。
主要特点
-
线程隔离:每个线程都有自己的变量副本,互不干扰。
-
非共享 :
ThreadLocal变量不是在多个线程之间共享的。 -
生命周期 :
ThreadLocal变量的生命周期与线程的生命周期相关联。
常见用途
-
用户上下文信息 :在多用户环境中,可以使用
ThreadLocal来存储当前用户的信息。 -
数据库连接:为每个线程分配独立的数据库连接。
-
事务管理:在事务处理中,为每个线程维护独立的事务状态。
-
日志记录 :为每个线程维护独立的日志记录。
1.验证码功能
java
//controller
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// TODO 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
//impl
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(RegexUtils.isPhoneInvalid( phone)){
//2.不符合,错误返回
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存到session
session.setAttribute("code",code);
//5.发送验证码
// return ok
log.debug("发送验证码成功,验证码:{}",code);
return Result.ok();
}
2.登录功能
java
//controller
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
return userService.login(loginForm,session);
}
impl
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid( phone)){
//2.不符合,错误返回
return Result.fail("手机号格式错误");
}
//2.验证码
Object cachecode = session.getAttribute("code");
String code = loginForm.getCode();
if(cachecode == null || !cachecode.toString().equals(code)){
//3.不一致报错
return Result.fail("验证码错误");
}
//4.一致,保存用户信息到session并返回结果
User user = query().eq("phone",phone).one();
//5.判断用户是否存在
if(user == null){
//6.不存在,创建新会员保存用户信息到session并返回结果
user = creatUserWithPhone(phone);
}
//7.保存用户信息到session并返回结果
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
private User creatUserWithPhone(String phone){
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
save(user);
return user;
}
-
session.setAttribute:-
session是HttpSession对象,用于在不同请求之间共享数据。 -
setAttribute方法将指定的值与指定的名称相关联,存储在HttpSession中。 -
在这个例子中,用户的
UserDTO对象被存储到会话中,键为"user"。
javaHttpSession session = request.getSession(); session.setAttribute("user", userDTO); -
使用场景
这种做法通常用于在用户登录后,将用户信息存储在会话中,以便在后续的请求中可以方便地访问这些信息。
MvcConfig.java
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns("/user/code", "/user/login","blog/hot",
"shop/**","shop-type/**","/voucher/**");
}
}
LoginInterceptor
java
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取session
HttpSession session = request.getSession();
//2.获取session的用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if(user == null) {
//4.不存在拦截
response.setStatus(401);
return false;
}
//5.存在保存到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
3.集群的session问题

Session集群共享问题
-
什么是Session集群共享问题?
在分布式集群环境中,会话(Session)共享是一个常见的挑战。默认情况下,Web 应用程序的会话是保存在单个服务器上的,当请求不经过该服务器时,会话信息无法被访问。
-
Session集群共享问题造成哪些问题?
- 服务器之间无法实现会话状态的共享。比如:在当前这个服务器上用户已经完成了登录,Session中存储了用户的信息,能够判断用户已登录,但是在另一个服务器的Session中没有用户信息,无法调用显示没有登录的服务器上的服务
-
如何解决Session集群共享问题?
-
方案一 :Session拷贝(不推荐)
Tomcat提供了Session拷贝功能,通过配置Tomcat可以实现Session的拷贝,但是这会增加服务器的额外内存开销,同时会带来数据一致性问题
-
方案二 :Redis缓存(推荐)
Redis缓存具有Session存储一样的特点,基于内存、存储结构可以是key-value结构、数据共享
-
-
Redis缓存相较于传统Session存储的优点:
- 高性能和可伸缩性:Redis 是一个内存数据库,具有快速的读写能力。相比于传统的 Session 存储方式,将会话数据存储在 Redis 中可以大大提高读写速度和处理能力。此外,Redis 还支持集群和分片技术,可以实现水平扩展,处理大规模的并发请求。
- 可靠性和持久性:Redis 提供了持久化机制,可以将内存中的数据定期或异步地写入磁盘,以保证数据的持久性。这样即使发生服务器崩溃或重启,会话数据也可以被恢复。
- 丰富的数据结构:Redis 不仅仅是一个键值存储数据库,它还支持多种数据结构,如字符串、列表、哈希、集合和有序集合等。这些数据结构的灵活性使得可以更方便地存储和操作复杂的会话数据。
- 分布式缓存功能:Redis 作为一个高效的缓存解决方案,可以用于缓存会话数据,减轻后端服务器的负载。与传统的 Session 存储方式相比,使用 Redis 缓存会话数据可以大幅提高系统的性能和可扩展性。
- 可用性和可部署性:Redis 是一个强大而成熟的开源工具,有丰富的社区支持和活跃的开发者社区。它可以轻松地与各种编程语言和框架集成,并且可以在多个操作系统上运行。
基于Redis实现短信验证码登录

java
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
private final StringRedisTemplate stringRedisTemplate;
public UserServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(RegexUtils.isPhoneInvalid( phone)){
//2.不符合,错误返回
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code, 2,TimeUnit.MINUTES);
//5.发送验证码
log.debug("发送验证码成功,验证码:{}",code);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid( phone)){
//2.不符合,错误返回
return Result.fail("手机号格式错误");
}
//2.验证码校验
String cachecode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if(cachecode == null || !cachecode.toString().equals(code)){
//3.不一致报错
return Result.fail("验证码错误");
}
//4.一致,保存用户信息到session并返回结果
User user = query().eq("phone",phone).one();
//5.判断用户是否存在
if(user == null){
//6.不存在,创建新会员保存用户信息到session并返回结果
user = creatUserWithPhone(phone);
}
//7.保存用户信息到redis并返回结果
//7.1Token生成作为领票
String token = UUID.randomUUID().toString(true);
//7.2USer转Hash
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue( true)
.setFieldValueEditor((filedName,filedValue) -> filedValue.toString()));
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);
//7.3存储到redis
stringRedisTemplate.expire(LOGIN_USER_TTL + token,30,TimeUnit.MINUTES);
return Result.ok(token);
}
private User creatUserWithPhone(String phone){
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
save(user);
return user;
}
拦截链
单独配置一个拦截器用户刷新Redis中的token:在基于Session实现短信验证码登录时,我们只配置了一个拦截器,这里需要另外再配置一个拦截器专门用户刷新存入Redis中的 token,因为我们现在改用Redis了,为了防止用户在操作网站时突然由于Redis中的 token 过期,导致直接退出网站,严重影响用户体验。那为什么不把刷新的操作放到一个拦截器中呢,因为之前的那个拦截器只是用来拦截一些需要进行登录校验的请求,对于哪些不需要登录校验的请求是不会走拦截器的,刷新操作显然是要针对所有请求比较合理,所以单独创建一个拦截器拦截一切请求,刷新Redis中的Key。
RefreshInterceptor.java
java
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import com.hmdp.dto.UserDTO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class RefreshTokenInterceptor implements HandlerInterceptor {
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头token
String token = request.getHeader("Authorization");
if(StrUtil.isBlank( token)){
return true;
}
//2.基于token 获取redis的用户
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3.判断用户是否存在
if(userMap.isEmpty()) {
return true;
}
//哈希转为userDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
//5.存在保存到ThreadLocal
UserHolder.saveUser(userDTO);
//刷新token有效期
stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
//6.放行
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
Logininterceptor
java
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
public LoginInterceptor() {
this.stringRedisTemplate = stringRedisTemplate;
}
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否需要拦截 threadLocal是否有用户
if (UserHolder.getUser() == null) {
// 拦截状态码
response.setStatus(401);
return false;
}//拦截
return true;
//6.放行
}
}