Redis学习

基于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);
    }
}

Cookie和session的关系:

在Web开发中,sessioncookie都是用于在客户端和服务器之间保存和传递信息的机制,二者紧密相关,同时又有明显区别,以下将从关联、区别、协作流程等方面详细介绍它们的关系:

  • 实现会话跟踪sessioncookie共同服务于Web应用的会话跟踪。session 是服务器端跟踪用户会话状态的机制,而cookie 是客户端存储信息的方式,二者相互配合,使得服务器能够识别不同的客户端,为用户提供连续的服务体验。例如,在一个电商网站中,用户登录后,服务器通过session 记录用户的登录状态,同时借助cookie 将会话标识传递给客户端,后续客户端每次请求时携带该标识,服务器就能识别出是哪个用户在操作。
  • 依赖关系:通常情况下,session 的实现依赖于cookie。服务器在创建session 后,会生成一个唯一的会话ID(如JSESSIONID),并通过Set - Cookie 头信息将这个会话ID以cookie 的形式发送给客户端。客户端在后续的请求中会自动携带这个cookie,服务器根据cookie 中的会话ID来查找对应的session,从而实现会话的跟踪和管理。

区别

  • 存储位置
    • sessionsession 数据存储在服务器端,服务器会为每个客户端创建一个独立的session 对象,用于存储与该客户端相关的会话信息。例如,在一个在线论坛中,服务器会为每个登录的用户创建一个session,存储用户的用户名、权限等信息。
    • cookiecookie 数据存储在客户端的浏览器中。浏览器会按照服务器的指示,将cookie 信息保存在本地文件或内存中。比如,当用户访问某个网站时,网站可能会在用户的浏览器中存储一个cookie,记录用户的偏好设置。
  • 安全性
    • session:由于session 数据存储在服务器端,相对来说更安全。服务器可以对session 数据进行加密、验证等操作,防止数据被篡改或窃取。例如,银行网站在处理用户的敏感信息时,通常会使用session 来存储用户的登录状态和交易信息。
    • cookiecookie 存储在客户端,容易被用户或恶意程序访问和修改,安全性较低。例如,一些恶意网站可能会通过脚本读取用户浏览器中的cookie 信息,从而获取用户的隐私数据。
  • 数据大小限制
    • sessionsession 存储在服务器端,服务器对session 数据的大小限制通常取决于服务器的配置和可用资源,一般来说可以存储相对较大的数据量。
    • cookie:每个cookie 的大小通常限制在4KB左右,并且浏览器对每个域名下的cookie 数量也有限制(一般为20个左右)。因此,cookie 不适合存储大量的数据。

协作流程

  1. 客户端首次请求:客户端向服务器发送请求,服务器接收到请求后,发现客户端没有携带有效的会话ID(cookie 中没有JSESSIONID),于是创建一个新的session 对象,并生成一个唯一的会话ID。
  2. 服务器发送cookie:服务器在响应中通过Set - Cookie 头信息将会话ID以cookie 的形式发送给客户端。例如,响应头中可能会包含 Set - Cookie: JSESSIONID = 1234567890abcdef; Path=/; HttpOnly
  3. 客户端后续请求:客户端在接收到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的数据结构

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

相关代码:

@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);
  1. AopContext.currentProxy()
    • 返回当前正在执行的 AOP 代理对象(即被增强后的对象)。
    • 需要在配置类中添加 @EnableAspectJAutoProxy(exposeProxy = true) 才能使用,否则会抛出异常。
  2. 为什么要在同步块中获取代理对象?
    • 原代码使用 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可重入锁原理:

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

博客内容均系原创,未经允许严禁转载!
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇