基于session实现登录

ThreadLocal:定义
ThreadLocal是 Java 中的一个线程局部变量工具类,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。可以把它类比成每个线程都有一个属于自己的“私人储物柜”,存放在里面的东西只有自己能访问和修改。
工作原理
- 数据存储:
ThreadLocal内部通过ThreadLocalMap来存储每个线程的变量副本。每个Thread对象都有一个ThreadLocalMap类型的成员变量,当线程使用ThreadLocal设置值时,实际上是将值存储到该线程的ThreadLocalMap中,其中ThreadLocal实例本身作为键,变量副本作为值。 - 访问机制:当线程需要获取
ThreadLocal变量的值时,它会先获取自身的ThreadLocalMap,然后以ThreadLocal实例为键从ThreadLocalMap中查找对应的值。
使用场景
- 数据库连接管理:在多线程环境下进行数据库操作时,每个线程都需要一个独立的数据库连接,以避免不同线程之间的连接干扰。使用
ThreadLocal可以为每个线程创建一个独立的数据库连接副本,确保线程安全。 - 会话管理:在 Web 应用中,每个用户的会话信息通常需要独立管理。可以使用
ThreadLocal来存储每个用户的会话信息,使得每个线程都能独立访问和操作自己的会话数据。 - 事务管理:在分布式系统中,每个事务可能需要独立的上下文信息。通过
ThreadLocal可以为每个事务线程提供独立的事务上下文,确保事务的独立性和一致性。
public class ThreadLocalExample {
// 创建一个 ThreadLocal 实例
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建两个线程
Thread thread1 = new Thread(() -> {
// 设置线程 1 的值
threadLocal.set(1);
System.out.println("Thread 1: " + threadLocal.get());
// 移除线程 1 的值
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
// 设置线程 2 的值
threadLocal.set(2);
System.out.println("Thread 2: " + threadLocal.get());
// 移除线程 2 的值
threadLocal.remove();
});
// 启动线程
thread1.start();
thread2.start();
}
}
在上述示例中,threadLocal是一个ThreadLocal实例,线程 1 和线程 2 分别设置了不同的值,并且可以独立地获取和移除自己的值,彼此之间不会相互影响。
注意事项
- 内存泄漏问题:由于
ThreadLocalMap中的键是ThreadLocal实例的弱引用,而值是强引用。如果ThreadLocal实例被垃圾回收,键为null,但值仍然存在,可能会导致内存泄漏。因此,在使用完ThreadLocal后,应该及时调用remove()方法清除数据。 - 使用范围:
ThreadLocal适用于每个线程需要独立维护自己的变量副本的场景,但不适合需要共享数据的场景。如果多个线程需要共享数据,应该使用其他并发工具,如Synchronized关键字或ReentrantLock。
ThreadLocalMap的remove并不会移除所有类型的值,而是移除该线性的Threadmap实例的值
注意:


threadLocal只能保存一个值,如果想要保存多个值可以用
ThreadLocal 实例在每个线程中只会关联一个值,而不是多个值。但如果你使用的是像 ThreadLocal> 这种方式,在 ThreadLocal 中存储了一个映射(Map),里面有多个键值对,那么可以按照下面的方法精准移除其中某个值。
移除 ThreadLocal> 中的特定值
import java.util.HashMap;
import java.util.Map;
public class ThreadLocalMapValueRemoval {
// 定义一个存储 Map 的 ThreadLocal 实例
private static final ThreadLocal<Map<String, String>> threadLocalMap = ThreadLocal.withInitial(HashMap::new);
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 获取当前线程的 Map
Map<String, String> map = threadLocalMap.get();
// 向 Map 中添加一些键值对
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
System.out.println("移除前的 Map: " + map);
// 精准移除特定键对应的值
removeValueFromThreadLocalMap("key2");
System.out.println("移除后的 Map: " + map);
// 清除 ThreadLocal 中的 Map
threadLocalMap.remove();
});
thread.start();
}
public static void removeValueFromThreadLocalMap(String key) {
// 获取当前线程的 Map
Map<String, String> map = threadLocalMap.get();
if (map != null) {
// 移除指定键对应的值
map.remove(key);
}
}
}
关于session:
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
// 1,校验手机号,这里验证的是无效的手机格式
if(RegexUtils.isPhoneInvalid(phone)){//如果是true说明无效
// 2,如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 符合,生成验证吗
String code = RandomUtil.randomNumbers(6);//生成一个6位随机数字
// 保存验证码到session
session.setAttribute("code",code);//保存到session的Api
// 发送验证码
log.info("发送成功{}",code);
// 返回ok
return Result.ok(code);
}
}

在你提供的 UserServiceImpl 类的 sendCode 方法中,会涉及到 HttpSession 的使用情况,下面详细分析是否会创建新的 HttpSession 以及整个方法的执行逻辑:
是否创建新的 HttpSession
当调用 session.setAttribute(“code”, code); 时,如果传入的 session 对象已经存在(即当前请求已经关联了一个 HttpSession),则不会创建新的 HttpSession,而是直接使用已有的 HttpSession 来存储验证码。
但如果当前请求还没有关联任何 HttpSession,在 Spring MVC 中,当你通过方法参数获取 HttpSession 时,实际上是通过 request.getSession() 来获取的,其等同于 request.getSession(true),这意味着如果当前请求没有对应的 HttpSession,服务器会创建一个新的 HttpSession 对象。所以,如果是首次请求且没有携带有效的 JSESSIONID,就会创建一个新的 HttpSession 并存储验证码。
Cookie和session的关系:
在Web开发中,session和cookie都是用于在客户端和服务器之间保存和传递信息的机制,二者紧密相关,同时又有明显区别,以下将从关联、区别、协作流程等方面详细介绍它们的关系:
- 实现会话跟踪:
session和cookie共同服务于Web应用的会话跟踪。session是服务器端跟踪用户会话状态的机制,而cookie是客户端存储信息的方式,二者相互配合,使得服务器能够识别不同的客户端,为用户提供连续的服务体验。例如,在一个电商网站中,用户登录后,服务器通过session记录用户的登录状态,同时借助cookie将会话标识传递给客户端,后续客户端每次请求时携带该标识,服务器就能识别出是哪个用户在操作。 - 依赖关系:通常情况下,
session的实现依赖于cookie。服务器在创建session后,会生成一个唯一的会话ID(如JSESSIONID),并通过Set - Cookie头信息将这个会话ID以cookie的形式发送给客户端。客户端在后续的请求中会自动携带这个cookie,服务器根据cookie中的会话ID来查找对应的session,从而实现会话的跟踪和管理。
区别
- 存储位置:
- session:
session数据存储在服务器端,服务器会为每个客户端创建一个独立的session对象,用于存储与该客户端相关的会话信息。例如,在一个在线论坛中,服务器会为每个登录的用户创建一个session,存储用户的用户名、权限等信息。 - cookie:
cookie数据存储在客户端的浏览器中。浏览器会按照服务器的指示,将cookie信息保存在本地文件或内存中。比如,当用户访问某个网站时,网站可能会在用户的浏览器中存储一个cookie,记录用户的偏好设置。
- session:
- 安全性:
- session:由于
session数据存储在服务器端,相对来说更安全。服务器可以对session数据进行加密、验证等操作,防止数据被篡改或窃取。例如,银行网站在处理用户的敏感信息时,通常会使用session来存储用户的登录状态和交易信息。 - cookie:
cookie存储在客户端,容易被用户或恶意程序访问和修改,安全性较低。例如,一些恶意网站可能会通过脚本读取用户浏览器中的cookie信息,从而获取用户的隐私数据。
- session:由于
- 数据大小限制:
- session:
session存储在服务器端,服务器对session数据的大小限制通常取决于服务器的配置和可用资源,一般来说可以存储相对较大的数据量。 - cookie:每个
cookie的大小通常限制在4KB左右,并且浏览器对每个域名下的cookie数量也有限制(一般为20个左右)。因此,cookie不适合存储大量的数据。
- session:
协作流程
- 客户端首次请求:客户端向服务器发送请求,服务器接收到请求后,发现客户端没有携带有效的会话ID(
cookie中没有JSESSIONID),于是创建一个新的session对象,并生成一个唯一的会话ID。 - 服务器发送
cookie:服务器在响应中通过Set - Cookie头信息将会话ID以cookie的形式发送给客户端。例如,响应头中可能会包含Set - Cookie: JSESSIONID = 1234567890abcdef; Path=/; HttpOnly。 - 客户端后续请求:客户端在接收到
cookie后,会将其保存到本地。在后续的请求中,客户端会自动在请求头中携带这个cookie信息。服务器接收到请求后,从cookie中获取会话ID,然后根据会话ID查找对应的session对象,从而获取该客户端的会话信息。
每一个到达tomcat的请求都是一个独立的线程
登录验证:

拦截器类需要实现一个接口HandlerInterceptor,实现接口方法是:ctrl+i
package com.hmdp.utils;
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 {
// 获取session
HttpSession session = request.getSession();
// 获取session中的用户
Object user = session.getAttribute("user");
// 判断用户是否存在
if(user==null){
// 不存在。拦截,返回一个401状态码
response.setStatus(401);
return false;
}
UserHolder.saveUser((UserDTO)user);//把user的信息保存到ThreadLocal
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户信息,避免泄露
UserHolder.removeUser();
}
}
集群的session共享问题

redis的数据结构:
- String(字符串)
- 描述:Redis最基本的数据类型,是二进制安全的,可存放任何数据,如经过base64编码的图片或序列化的对象等。不过,系统内存有限,且字符串类型的值最多能存储512MB字节的内容,所以不适合存入过大的文件。使用场景:适合用于缓存简单的键值对数据,如会话信息、计数器等;还能结合SETNX命令实现简单的分布式锁。特殊操作:有INCR、DECR等操作,可递增或递减数字型字符串的值。还包括set(设置指定的key值)、get(获取指定key的值)、getrange(返回key中字符串的子字符)等命令。
- 描述:一个键对应一个双向链表,支持从头部或尾部添加或弹出元素,同时Redis还支持阻塞操作,在没有元素时可以阻塞等待。使用场景:适用于消息队列、最近最少使用(LRU)缓存、历史记录、动态数据流等场景。比如在异步任务分发系统中,可将任务放入队列,由多个消费者消费。特殊操作:LPUSH、RPUSH可在列表头部或尾部添加元素;LPOP、RPOP能从列表头部或尾部弹出元素;还有BLPOP、BRPOP等阻塞操作命令。
- 描述:一个键对应一个无序、不重复的字符串集合,支持交、并、差等集合运算。使用场景:适合用于存储唯一元素的集合,如标签、好友列表等。在博客或商品系统中,可用Set存储文章或商品标签,方便分类和搜索;在社交应用中,可利用Set存储用户好友列表,通过求交集找出共同好友。特殊操作:SADD可添加元素到集合;SMEMBERS用于获取集合的所有元素;SINTER、SUNION、SDIFF可进行集合的交集、并集、差集运算。
- 描述:一个键对应一个字段 – 值对的映射,类似于字典或关联数组。使用场景:适合用于存储结构化的数据,如用户信息、产品详情、配置项等。可以将用户ID作为键,用户的属性(姓名、年龄、性别等)作为字段,避免将整个用户对象序列化成字符串。特殊操作:HSET、HGET可设置或获取哈希中的字段值;HGETALL用于获取哈希中所有字段的值。
- 描述:类似于集合,但每个成员都关联了一个分数,用于排序。使用场景:适合用于排行榜、评分系统、时间序列数据等。例如在游戏中可根据玩家分数进行排名。特殊操作:ZADD可添加元素到有序集合;ZRANGE、ZREVRANGE用于获取排序后的元素;ZSCORE可获取元素的分数。
- String(字符串)

相关代码:
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;//注入redis操作的api
@Override
public Result sendCode(String phone, HttpSession session) {
// 1,校验手机号,这里验证的是无效的手机格式
if(RegexUtils.isPhoneInvalid(phone)){//如果是true说明无效
// 2,如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 符合,生成验证吗
String code = RandomUtil.randomNumbers(6);//生成一个6位随机数字
// 保存验证码到session
// session.setAttribute("code",code);//保存到session的Api
// 保存验证码到redis,并设置有效期
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY+phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 发送验证码
log.info("发送成功{}",code);
// 返回ok
return Result.ok(code);
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1 校验手机号
String phone = loginForm.getPhone();
// 1,校验手机号,这里验证的是无效的手机格式
if(RegexUtils.isPhoneInvalid(phone)){//如果是true说明无效
// 2,如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 2 校验验证码
// Object cacheCode = session.getAttribute("code");
//从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
// if(cacheCode==null||!cacheCode.toString().equals(code)){//如果值是String就不需要用toString()进行校验
// 3 不一致 报错
if(cacheCode==null||!cacheCode.equals(code)){
return Result.fail("验证码错误!");
}
// 4 一致,根据手机号查询用户 select *from tb_user where phone =?
// List<User> phone1 = query().eq("phone", phone).list();如果查出多个值可以返回为一个列表
User user = query().eq("phone", phone).one();
// 判断用户是否存在
if(user==null){
// 不存在,创建新用户并保存
user=createUserWithPhone(phone);
}
//1保存用户信息到redis中
String token = UUID.randomUUID().toString(true);
// 3将user对象作为hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 2随机生成字符串作为key,作为登录令牌
//4存储.将user转为map存储,将userDto中的数据类型转为String
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())
);
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);//设置有效期
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
//返回token
return Result.ok(token);//把token·返回给前端,前端通过sessionStorage将token存储到请求头中
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
String NickName = RandomUtil.randomString(8);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +NickName);
save(user);
log.info("新增用户{}",user);
return user;
}
}
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session
// HttpSession session = request.getSession();
// 获取session中的用户
// todo :new 获取请求头中的token
String token = request.getHeader("authorization");
if (StringUtil.isNullOrEmpty(token)) {
// token为空,拦截
response.setStatus(401);
return false;
}
// Object user = session.getAttribute("user");
// 判断用户是否存在
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
if(userMap.isEmpty()){
// 不存在。拦截,返回一个401状态码
response.setStatus(401);
return false;
}
// 将usermap转换为UserDto
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser((UserDTO)user);//把user的信息保存到ThreadLocal
//每次请求都刷新有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户信息,避免泄露
UserHolder.removeUser();
}
}
由于这样写的话这个拦截器只会拦截登录,
"/user/code",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**",
"/user/login",
"/blog/hot"
只会拦截这些路径,其他路径不拦截,这样就无法刷新token中的有效期了,解决办法就是再写一个拦截器,拦截所有路径,通过order方法把这个拦截器放在登录校验拦截器前面,如果没有请求头携带token就放行,因为就算放行在第二个拦截器也会拦截,第一个拦截器只需要刷新token(假设有token)情况下,并保存user信息到ThreadLocal
相关代码:
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())//默认拦截一切
.excludePathPatterns(
"/user/code",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**",
"/user/login",
"/blog/hot"
).order(2);
registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).order(0);//默认拦截所有请求
}
}
public class RefreshInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session
// HttpSession session = request.getSession();
// 获取session中的用户
// todo :new 获取请求头中的token
String token = request.getHeader("authorization");
if (StringUtil.isNullOrEmpty(token)) {
// token为空,拦截
return true;
}
// Object user = session.getAttribute("user");
// 判断用户是否存在,根据键去查找值
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
if(userMap.isEmpty()){
// 如果用户为空,不拦截直接放行
return true;
}
// 将usermap转换为UserDto
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser((UserDTO)user);//把user的信息保存到ThreadLocal
//每次请求都刷新有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户信息,避免泄露
UserHolder.removeUser();
}
}
package com.hmdp.utils;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//todo 判断是否需要拦截(ThreadLocal中是否有用户
if(UserHolder.getUser()==null){
//没有,拦截
response.setStatus(401);
return false;
}
//有用户 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户信息,避免泄露
UserHolder.removeUser();
}
}
什么是缓存:



对象转json:JSONUtil.toJsonStr(shop)
json转对象:JSONUtil.toBean(shopJson,Shop.class)
商铺缓存实现:
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
// 查询店铺类型,查到后进行缓存
String shopType = stringRedisTemplate.opsForValue().get(LOCK_SHOP_KEY);
if (StrUtil.isNotBlank(shopType)) {
JSONArray jsonArray = JSONUtil.parseArray(shopType);
List<ShopType> shopList = jsonArray.toList(ShopType.class);
return Result.ok(shopList);
}
//redis里面没有这个数据,去数据库查询
List<ShopType> typeList = typeService
.query().orderByAsc("sort").list();
String shopList = JSONUtil.toJsonStr(typeList);
stringRedisTemplate.opsForValue().set(LOCK_SHOP_KEY,shopList,60, TimeUnit.MINUTES);
return Result.ok(shopList);
}
}
缓存更新策略:



缓存穿透:布隆过滤或者缓存空对象

缓存空对象:如果数据库和缓存都没有这个值,就把空值缓存到redis里,可能有额外的内存消耗,可以设置过期时间进行解决,可能会造成短期不一致性(控制ttl时间即可),也可以在新增一条数据室主动去更新
布隆过滤器:在客户端和redis之间加布隆过滤器,先去布隆过滤器里查。。。其实布隆过滤器里放的并不是真实的数据,而是数据的hash值,再把hash值转化为二进制放到布隆过滤器里

缓存穿透问题:基于商铺查询实现
GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
//todo 添加缓存
// 1 请求过来,先从redis里去查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {//StrUtil.isNotBlank用来判断是否为null或者空字符串
// 2 如果redis有,直接返回
return Result.ok( JSONUtil.toBean(shopJson,Shop.class));//将json格式转化为类对象
}
if(shopJson!=null){
return Result.ok("店铺信息不存在");
}
// 3 如果redis没有,根据商户id去数据库查询
Shop shop = shopService.getById(id);
if(shop==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"null",3, TimeUnit.MINUTES);
return Result.fail("不存在该商户");
}
// 6,存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7,返回
return Result.ok(shop);//这个地方的getById是IService提供
}
在店铺查询的时候,如果这个根据id在缓存中没有查到,并且在数据库也没查到,就可以考虑缓存穿透的问题了,可以把值设置为null
3 如果redis没有,根据商户id去数据库查询
Shop shop = shopService.getById(id);
if(shop==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"null",3, TimeUnit.MINUTES);
return Result.fail("不存在该商户");
}
当前端传来存在的数剧时,可以避免重复去查询数据库的压力

缓存雪崩:

缓存击穿:



基于互斥锁的方式解决缓存击穿问题
思路:

利用redis的setnx命令:

它只能给key不存在的时候去执行
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
public IShopService shopService;
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
Shop shop = queryWithMutex(id);
if(shop==null){
return Result.fail("店铺不存在");
}
// 7,返回
return Result.ok(shop);//这个地方的getById是IService提供
}
//缓存击穿
public Shop queryWithMutex( Long id) {
//todo 添加缓存击穿
// 1 请求过来,先从redis里去查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {//StrUtil.isNotBlank用来判断是否为null或者空字符串
// 2 如果redis有,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);//将json格式转化为类对象
return shop;
}
// 判断命中的是否为空值
if(shopJson!=null){
return null;
}
// 开始实现缓存重建
// 获取互斥锁
Shop shop = null;
try {
if (!tryLock(LOCK_SHOP_KEY+id)) {//如果获取锁失败
System.out.println(Thread.currentThread()+"获取锁失败啦,该线程将进入休眠50毫秒");
Thread.sleep(50);
return queryWithMutex(id);//做个递归,如果没获取到锁就再重来一遍,如果获取到锁,就跳出if语句
}
System.out.println(Thread.currentThread()+"获取锁成功啦");
// 判断是否成功
// 失败,休眠并重试
// 成功,根据id查询数据库
// 3 如果redis没有,根据商户id去数据库查询
shop = shopService.getById(id);
Thread.sleep(200);//模拟延迟高并发
if(shop==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"null",3, TimeUnit.MINUTES);
return null;
}
// 6,存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放互斥锁
unLock(LOCK_SHOP_KEY+id);
}
// 7,返回
return shop;//这个地方的getById是IService提供
}
//缓存穿透
public Shop queryWithPassThrough( Long id) {
//todo 添加缓存
// 1 请求过来,先从redis里去查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isNotBlank(shopJson)) {//StrUtil.isNotBlank用来判断是否为null或者空字符串
// 2 如果redis有,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);//将json格式转化为类对象
return shop;
}
if(shopJson!=null){
return null;
}
// 3 如果redis没有,根据商户id去数据库查询
Shop shop = shopService.getById(id);
if(shop==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"null",3, TimeUnit.MINUTES);
return null;
}
// 6,存在,写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7,返回
return shop;//这个地方的getById是IService提供
}
//加锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//放锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}

利用Jemeter做压力测试
基于逻辑过期方式解决缓存击穿问题:

热点数据的缓存是需要提前导入的,即缓存预热
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id) {
//todo 用逻辑过期方式解决缓存击穿的问题
// 1 请求过来,先从redis里去查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isBlank(shopJson)) {//StrUtil.isNotBlank用来判断是否为null或者空字符串
// 2 如果redis没有,直接返回
return null;
}
//命中,将json反序列化对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期,返回数据即可
return shop;
}
//已过期,需要缓存重建
// 获取互斥锁
String lockKey=LOCK_SHOP_KEY + id;//锁的键
boolean isLock = tryLock(lockKey);
// 判断是否获取成功
if(isLock){
// 成功,开启一个线程,实现缓存重建,返回,这里用到的是线程池
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
saveShopRedis( id,30L);
System.out.println(Thread.currentThread()+"更新数据了");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
// 失败。返回过期信息
// 未过期,直接返回店铺信息
// 3 如果redis没有,根据商户id去数据库查询
return shop;//这里返回的还是过期数据
}
缓存工具封装:

代码:
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import com.hmdp.service.IShopService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
@Slf4j
@Component
public class CacheClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
public IShopService shopService;
//将任意java对象序列化为json并存储在String类型的key中,并且可以设置TTL过期时间
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
//将任意java对象序列化为json并存储在String类型的key中,设置逻辑过期,并可以设置逻辑过期时间,用于处理缓存击穿问题
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
RedisData redisData = new RedisData();
redisData.setData(value);
// unit.toSeconds(time)把时封装为秒
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
//方法三:根据指定的key查询缓存,并反序列化制定类型,利用缓存空值的方式解决缓存穿透的问题
//缓存穿透,返回空值的方式实现
public <T,ID> T queryWithPassThrough(String keyPrefix, ID id, Class<T> type,
Function<ID,T> dbFallback, Long time, TimeUnit unit) {
//todo 添加缓存
// 1 请求过来,先从redis里去查询商铺缓存
// Function代表有参有返回值的函数
String key=keyPrefix+id;
// 先从redis里查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (StrUtil.isNotBlank(json) && json.startsWith("{")) {
try {
return JSONUtil.toBean(json, type); // 解析JSON对象
} catch (Exception e) {
log.error("JSON解析失败,key:{},内容:{}", key, json, e);
return null; // 解析失败,视为缓存无效
}
}
// 2. 处理缓存穿透或空值
if (json != null) { // json为""(空值缓存)或非法格式
return null; // 直接返回null,不查DB
}
// 3. 从数据库查询
T apply = dbFallback.apply(id);
if (apply == null) {
// 缓存空值(用""表示,避免key不存在)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 4. 写入Redis并返回
this.set(key, apply, time, unit);
return apply;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
//方法四:缓存击穿 根据指定的key查询缓存,并反序列化制定类型,利用逻辑过期解决缓存击穿的问题,用到了函数式编程
public <T,ID> T queryWithLogicalExpire(String keyPrefix , ID id,Class<T> type,Function<ID,T> dbFallback, Long time, TimeUnit unit) {
//todo 用逻辑过期方式解决缓存击穿的问题
// 1 请求过来,先从redis里去查询商铺缓存
String key=keyPrefix+id;
String Json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (StrUtil.isBlank(Json)) {//StrUtil.isNotBlank用来判断是否为null或者空字符串
// 2 如果redis没有,直接返回
return null;
}
//// 转换为JSON字符串
// String jsonStr = JSONUtil.toJsonStr(user);
//命中,将json反序列化对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
T t = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
//未过期,返回数据即可
return t;
}
//已过期,需要缓存重建
// 获取互斥锁
String lockKey=key;//锁的键
boolean isLock = tryLock(lockKey);
// 判断是否获取成功
if(isLock){
// 成功,开启一个线程,实现缓存重建,返回,这里用到的是线程池
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
// 查询数据库
T apply = dbFallback.apply(id);
// 导入redis
this.setWithLogicalExpire(key,apply,time,unit);
System.out.println(Thread.currentThread()+"更新数据了");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
// 失败。返回过期信息
// 未过期,直接返回店铺信息
// 3 如果redis没有,根据商户id去数据库查询
return t;//这里返回的还是过期数据
}
//加锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//放锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
全局唯一ID:


@Component
public class RedisIdWorker {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final long BEGIN_TIMESTAMP=1640995200L;//开始时间戳
private static final int COUNT_BITS=32;//序列号位数
//keyPrefix 业务前缀
public long nextId(String keyPrefix){
// 1 生成时间戳
long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);//当前秒数
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//获取当前日期,精确到天
LocalDate date = LocalDate.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy:MM:dd");
String dateformat = date.format(dateTimeFormatter);
//2 生成序列号,使用redis自增长
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + dateformat);
// 拼接并返回
return timestamp<< COUNT_BITS| count;
}
// public static void main(String[] args) {
// LocalDate now = LocalDate.now();
//
// System.out.println(now);
// }
}
测试:
public void test2() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);//这里把初始计数设为 300,这意味着需要有 300 个 countDown() 操作,计数才会变为 0。
Runnable task=()->{
for (int i = 0; i <100 ; i++) {
long l = redisIdWorker.nextId("roder");
System.out.println(l);
}
latch.countDown();//当每个线程完成自身任务时,都会调用 countDown() 方法,使计数减 1。
};
long begin = System.currentTimeMillis();
for (int i = 0; i <300 ; i++) {
es.submit(task);
}
latch.await(); // 主线程会在此处阻塞,直到计数变为 0
long end = System.currentTimeMillis();
System.out.println("time="+(end-begin));
}

实现优惠券:

流程:

超卖问题:



一人一单:

package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
// 1 查询优惠卷
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 2 判断优惠卷是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 如果开始时间在当前时间之后说明还未开始
return Result.fail("抢卷时间尚未开始,开始时间为" + seckillVoucher.getBeginTime());
}
//3判断优惠卷是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
// 如果结束时间在当前时间之前说明已经结束
return Result.fail("抢卷时间已经结束开始,结束时间为" + seckillVoucher.getEndTime());
}
//4判断库存是否充足
if (seckillVoucher.getStock() <= 0) {
// 如果开始时间在当前时间之后说明还未开始
return Result.fail("优惠卷已被抢光!");
}
Long userId = UserHolder.getUser().getId();//用户id
synchronized(userId.toString().intern()) { //对id上锁
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId){
//一人一单
//拿到用户id,优惠卷id,做个查询,有的话返回已经抢过
Long userId = UserHolder.getUser().getId();//用户id
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经抢过啦!");
}
// 5扣减库存
boolean success = seckillVoucherService.update()//在更新数据的时候用到了悲观
.setSql("stock=stock-1")
.eq("voucher_id", voucherId)//这是where条件 where id =? and stock=?
.gt("stock", 0)
.update();
if (!success) {
return Result.fail("优惠卷已被抢光!");
}
//6创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7用户id
voucherOrder.setUserId(userId);
//8代金券id
voucherOrder.setVoucherId(voucherId);
//9返回订单id
save(voucherOrder);
return Result.ok(orderId);
}
}
重点解释一下这个代码:
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}解释一下这个代码
这段代码主要用于获取当前对象的 AOP 代理实例,并通过代理实例调用 createVoucherOrder 方法。其核心目的是确保在 同步块内部触发 Spring 的事务管理机制,让 @Transactional 注解生效。下面详细解释其原理和必要性:
1. 为什么需要 AOP 代理对象?
Spring 的事务管理(包括 @Transactional 注解)是基于 AOP(面向切面编程) 实现的。当一个带有 @Transactional 注解的方法被调用时,Spring 需要通过 代理对象 拦截该方法,并在方法前后添加事务管理逻辑(如开启事务、提交 / 回滚事务)。
关键点:
- 只有通过代理对象调用方法时,AOP 增强才会生效。
- 若在同一个类的方法内部直接调用另一个方法(如
this.createVoucherOrder()),AOP 代理会失效,导致事务注解不生效。
// 获取当前对象的 AOP 代理实例
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
// 通过代理对象调用带有 @Transactional 注解的方法
return proxy.createVoucherOrder(voucherId);
AopContext.currentProxy():- 返回当前正在执行的 AOP 代理对象(即被增强后的对象)。
- 需要在配置类中添加
@EnableAspectJAutoProxy(exposeProxy = true)才能使用,否则会抛出异常。
- 为什么要在同步块中获取代理对象?
- 原代码使用
synchronized(userId.toString().intern())对用户 ID 加锁,确保同一用户的请求串行执行。 - 通过代理对象调用
createVoucherOrder()方法,确保 锁的持有时间覆盖整个事务周期(从开启事务到提交 / 回滚),避免并发问题。
- 原代码使用
3. 若不使用代理对象会发生什么?
若直接调用 this.createVoucherOrder(voucherId),会导致:
- 事务失效:
@Transactional注解不会生效,数据库操作不再受事务管理。 - 锁与事务的顺序错乱:锁可能在事务提交前释放,导致其他线程在当前事务未提交时读取到脏数据。
获取代理对象的另一种更优雅的方式是 在类中注入自身:
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {
@Resource
private IVoucherOrderService self; // 注入自身代理对象
@Override
public Result seckillVoucher(Long voucherId) {
// ... 校验逻辑
synchronized(userId.toString().intern()) {
return self.createVoucherOrder(voucherId); // 通过代理对象调用
}
}
}
. 代码作用
java
synchronized(userId.toString().intern()) {
return self.createVoucherOrder(voucherId);
}
- 核心目的:
防止同一用户在极短时间内发起多次抢购请求,导致数据库插入多条订单记录(即 “一人多单”)。 - 同步机制:
通过synchronized关键字对 用户 ID 的字符串常量 加锁,确保同一用户的所有请求在锁的保护下串行执行。
2. 技术细节解析
2.1 userId.toString()
- 将用户 ID(通常是
Long类型)转换为字符串。
例如:userId = 123→"123"。
2.2 intern() 方法
- 字符串驻留(String Intern):
intern()会将字符串对象放入 JVM 的字符串常量池,并返回池中的引用。- 若池中已存在相同内容的字符串,则返回池中的实例。
- 若不存在,则将当前字符串放入池中并返回其引用。
示例:
String s1 = new String("123");
String s2 = new String("123");
System.out.println(s1 == s2); // false(不同对象)
System.out.println(s1.intern() == s2.intern()); // true(指向常量池同一实例)
3 锁的粒度
- 锁定对象:
锁的粒度是 用户 ID 对应的字符串常量。例如,用户ID=123的所有请求都会竞争同一个锁对象"123"。 - 效果:
- 同一用户的并发请求会被串行化,避免重复下单。
- 不同用户的请求互不影响,不会被阻塞(如
ID=123和ID=456的请求可并行执行)。
3. 为什么不直接用 userId.toString()?
若改为 synchronized(userId.toString()),可能导致锁失效:
- 问题:
userId.toString()每次返回的是新的字符串对象(即使内容相同),导致不同线程持有不同的锁实例。
示例:
String lock1 = userId.toString(); // 第一次调用,生成新对象
String lock2 = userId.toString(); // 第二次调用,生成新对象
System.out.println(lock1 == lock2); // false(不同对象)
5. 总结
userId.toString().intern() 的核心作用是:
- 将用户 ID 转换为字符串常量池中的唯一对象,确保同一用户的锁对象相同。
- 通过
synchronized实现 JVM 级别的用户级锁,防止同一用户并发下单。
执行流程:
1. 锁的识别逻辑:基于 userId 的唯一性
当用户发起抢购请求时,系统会执行以下步骤:
若已持有:说明同一用户的前一个请求正在处理中,当前线程会被阻塞,直到锁释放。
获取用户 ID:
通过 UserHolder.getUser().getId() 从当前请求中获取用户 ID(如 123)。
生成锁对象:
将用户 ID 转换为字符串并调用 intern(),得到唯一的锁对象(如字符串常量池中的 "123")。
加锁逻辑:synchronized(userId.toString().intern()) 会检查当前线程是否持有该锁对象的监视器(Monitor)。
若未持有:当前线程会竞争获取锁(即获取该对象的监视器所有权)。

因为每个jvm内部都有一个锁监视器,不同德jvm有不同的监视器,在并发请情况下,会出现锁不住的情况



问题:如果加锁之后宕机那么锁就不能释放了
way:加过期时间


如果在还没设置过期时间的时候就宕机了该怎么办呢:
用命令:set 键名 值 ex 过期时间 nx
用这条命令基本上就保证了原子性
流程:

package com.hmdp.utils;
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
*
* @return true 代表获取锁成功 false 代表获取锁失败
*
*/
boolean tryLock (Long timeoutSec);
void unlock();
}
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
// SimpleRedisLock 自身通过构造函数接收 StringRedisTemplate,
// 这确保了 SimpleRedisLock 内部可以使用 Redis 功能。但这只是解决了 SimpleRedisLock 类的依赖问题,
// 并没有解决外部如何创建 SimpleRedisLock 实例的问题
//当其他类(如 UserService)需要使用 SimpleRedisLock 时,必须先提供 StringRedisTemplate 实例才能创建 SimpleRedisLock 对象。
private String name;//锁的名字
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX="lock:";
public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
this.name=name;
}
// 获取锁
@Override
public boolean tryLock(Long timeoutSec) {
// 获取线程id作为锁的值
long id = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue().
setIfAbsent(KEY_PREFIX + name, id + "", timeoutSec, TimeUnit.SECONDS);
//这里用到了自动拆箱,如果success为null,一拆箱成空指针了,所以用下面的方式进行比较
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
解决分布式锁误删问题:

package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
// SimpleRedisLock 自身通过构造函数接收 StringRedisTemplate,
// 这确保了 SimpleRedisLock 内部可以使用 Redis 功能。但这只是解决了 SimpleRedisLock 类的依赖问题,
// 并没有解决外部如何创建 SimpleRedisLock 实例的问题
//当其他类(如 UserService)需要使用 SimpleRedisLock 时,必须先提供 StringRedisTemplate 实例才能创建 SimpleRedisLock 对象。
private String name;//锁的名字
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX="lock:";
private static final String LOCK_PREFIX= UUID.randomUUID().toString(true)+"-";
public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
this.name=name;
}
// 获取锁
@Override
public boolean tryLock(Long timeoutSec) {
// 获取线程id作为锁的值
String threadId= LOCK_PREFIX+Thread.currentThread().getId();//线程标识
Boolean success = stringRedisTemplate.opsForValue().
setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
//这里用到了自动拆箱,如果success为null,一拆箱成空指针了,所以用下面的方式进行比较
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String threadId= LOCK_PREFIX+Thread.currentThread().getId();//线程标识
String id = stringRedisTemplate.opsForValue().get(threadId);
if(threadId.equals(id)){
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
// 不一致的话就不管,说明锁不释放
}
}
问题:在第一个线程即将释放锁的时候(已经判断是自己的锁),由于jvm的垃圾回收机制,造成阻塞,线程2进来,造成误删问题
解决办法:




docker exec -it 容器名称或者id sh 进入到容器内









Resisson可重入锁原理:





开启看门狗就是不停的去更新有效期



