黑马头条(10-1开始学习)

news/2024/10/7 21:22:28/

文章目录

  • 开始
    • 序列化
    • 将对象与字符串相加(例如 `对象 + ""`)和序列化对象(如 JSON 序列化)之间有几个主要的区别:
      • 1. **用途**
      • 2. **输出格式**
      • 3. **适用场景**
      • 4. **性能**
      • 5. **灵活性**
      • 总结
  • 项目
    • 手机验证码
    • Threadlocal
    • session
      • 具体说明:
      • 小结:
        • 什么时候会生成新的 Session ID:
    • 登录拦截器
    • 处理敏感信息
    • redis+token
      • 登录状态的
      • 其他情况
  • 缓存
      • ShopTypeList == null与 CollectionUtils.isEmpty(ShopTypeList) 还有 if (StringUtils.isBlank(shop_type_string)) { 与 shop_type_string==null
    • 缓存策略
        • 实际操作
    • 缓存穿透
    • 缓存雪崩
    • 热点key问题。(缓存击穿)
      • 互斥锁操作。
    • 逻辑过期
            • 装饰器模式
    • 练习
            • 反序列化
    • 工具类
  • 实战篇2:优惠券
    • 全局唯一ID
      • Redis 实现自增计数器
        • 线程池没有打印出信息。
    • 添加优惠券。
    • 超卖问题,没有解决的话会给商家带来经济损失。
        • 悲观锁实现
      • 1. SQL 查询中添加行级锁
      • 2. 修改 Service 层逻辑
      • 3. Controller 层保持不变
      • 4. 重要注意事项
          • 还有synchronized 关键字。jdk方法。
        • 乐观锁
      • CAS方法(去除了version,更简约。)
          • 实操
    • 一人一单
          • 修改
    • 成功代码
      • 集群
    • 分布式锁
      • 改进思想


开始

别人的笔记: 入门笔记

序列化

springRedisData与redis-cli都是客户端。

  • 在redis-cli输入set name lqc 会直接存储进去redi内存中。
  • 但是使用springRedisData 存储name lqc value要先经过jdk序列化器,之后变成了存储字节了。

在这里插入图片描述

package com.example.redisdemo.Config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redistemplate(RedisConnectionFactory reactiveConnectionFactory) {
//        创建redistemplate对象RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(reactiveConnectionFactory);
//      创建json序列化工具GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//      key序列化工具template.setKeySerializer(RedisSerializer.string());template.setValueSerializer(RedisSerializer.string());
//        value序列化template.setHashKeySerializer(genericJackson2JsonRedisSerializer);template.setValueSerializer(genericJackson2JsonRedisSerializer);return template;}
}
  • RedisConnectionFactory 是用于建立和管理与 Redis 服务器连接的工厂类,它为 RedisTemplate 提供连接支持,确保应用程序能够顺畅地与 Redis 进行交互。通过这种设计,RedisTemplate 不需要自己处理连接管理的细节,而是交由连接工厂负责,使得连接管理更加灵活和高效。

  • 这些配置告诉 RedisTemplate 如何处理数据的序列化和反序列化,但 RedisConnectionFactory 只是提供连接。配置 RedisTemplate 后,它就可以通过 RedisConnectionFactory 建立的连接与 Redis 进行数据交互。

在这里插入图片描述

  • 整体可读性有了很大提升,并且能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。
  • 其中记录了序列化时对应的class名称,目的是为了查询时实现自动反序列化。这会带来额外的内存开销。
@Testvoid test1() {User user = new User("lqc","24");redisTemplate.opsForValue().set("user:100", user);
//        存入redis的是一个json,需要序列化
//        拿出来的时候是一个对象,需要反序列化User o = (User)redisTemplate.opsForValue().get("user:100");System.out.println(o);}

在这里插入图片描述
但是这里的@class会保存进去类的数据进去。占据内存。
在这里插入图片描述

将对象与字符串相加(例如 对象 + "")和序列化对象(如 JSON 序列化)之间有几个主要的区别:

1. 用途

  • 对象 + “”

    • 通过将对象与空字符串相加,可以隐式调用对象的 toString() 方法,得到对象的字符串表示。
    • 通常用于快速查看对象的状态,主要用于调试和日志输出。
  • 序列化对象

    • 将对象转换为一种标准格式(如 JSON、XML)以便于存储、传输或交互。
    • 用于持久化数据或通过网络发送对象数据。

2. 输出格式

  • 对象 + “”

    • 输出的字符串格式完全依赖于 toString() 方法的实现。
    • 如果没有重写 toString() 方法,输出的结果可能不够直观,通常是类名加上哈希码。
    • 示例:
      public class Person {private String name;private int age;
      }Person p = new Person();
      System.out.println(p + ""); // 可能输出:Person@1a2b3c4
      
  • 序列化对象

    • 输出为标准化的格式,例如 JSON,易于读取和解析。
    • 示例:
      ObjectMapper objectMapper = new ObjectMapper();
      String jsonString = objectMapper.writeValueAsString(p);
      System.out.println(jsonString); // 输出:{"name":"John","age":30}
      

3. 适用场景

  • 对象 + “”

    • 适合简单调试和快速查看对象状态,方便在控制台输出。
  • 序列化对象

    • 适合需要将对象数据存储到数据库、发送到客户端或与其他系统交互的场景。

4. 性能

  • 对象 + “”

    • 通常性能较好,因为只是调用 toString() 方法,生成字符串的开销较小。
  • 序列化对象

    • 性能相对较低,尤其是对于复杂对象,因为需要将对象的整个结构和状态转换为特定格式。

5. 灵活性

  • 对象 + “”

    • 输出内容灵活性较低,主要依赖 toString() 方法的实现。
  • 序列化对象

    • 提供更多的灵活性,可以使用不同的序列化器(如 Jackson、Gson 等),自定义序列化过程以满足特定需求。

总结

  • 对象 + "" 适合快速查看对象的状态,主要用于调试;而序列化对象则是将对象转换为标准格式以便于存储或传输,适合数据交互场景。选择使用哪个取决于具体的需求和上下文。
    @Testvoid test2() throws JsonProcessingException {
//        stringTemplate.opsForValue().set("user:name:45", "lqc");User user = new User("虎哥", "100");String jsonString = objectMapper.writeValueAsString(user);stringTemplate.opsForValue().set("user:name:45", jsonString);String s = stringTemplate.opsForValue().get("user:name:45");
//        json字符串不能强转类型。字符串变成user。User user1 = objectMapper.readValue(s, User.class);System.out.println(user1);}
  • 使用String的序列化是需要先把对象进行序列化的存到redis。

在这里插入图片描述

项目

在这里插入图片描述
在这里插入图片描述

  server {listen       8080;server_name  localhost;# 指定前端项目所在的位置location / {root   html/hmdp;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}location /api {  default_type  application/json;#internal;  keepalive_timeout   30s;  keepalive_requests  1000;  #支持keep-alive  proxy_http_version 1.1;  rewrite /api(/.*) $1 break;  proxy_pass_request_headers on;#more_clear_input_headers Accept-Encoding;  proxy_next_upstream error timeout;  # 后端地址proxy_pass http://127.0.0.1:8099;#proxy_pass http://backend;}}upstream backend {server 127.0.0.1:8089 max_fails=5 fail_timeout=10s weight=1;#server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;}  

在这里插入图片描述

  • 检验登录状态

    1. 前端传送过来token或者是session_id.
    2. 后端有一个地方保存信息,判断是否有为null。(是否登录)
    3. 有登录信息不为null后,把信息存入ThreadLocal.
  • jwt传送够来token

  • session的话从cookies拿到传送session_id。

private void initUserLoginVo(HttpServletRequest request) {//从请求头获取tokenString token = request.getHeader("token");System.out.println(token);if (!StringUtils.isEmpty(token)) {Long userId = JwtHelper.getUserId(token);
//            登录拿到token,解析出用户id.
//            在redis拿到用户相关的数据.UserLoginVo userLoginVo = (UserLoginVo) redisTemplate.opsForValue().get(RedisConst.USER_LOGIN_KEY_PREFIX + userId);
//            登录过的,设置在线程变量中.if (userLoginVo != null) {//将UserInfo放入上下文中AuthContextHolder.setUserId(userLoginVo.getUserId());AuthContextHolder.setWareId(userLoginVo.getWareId());log.info("当前用户:" + AuthContextHolder.getWareId());}}}

手机验证码

在这里插入图片描述

  • Cookies 是一种存储在客户端浏览器中的数据,可以随着每个请求自动发送给服务器。因此,服务器通常会将 Session ID 存储在 Cookies 中(一般是 JSESSIONID),然后浏览器在后续的请求中会自动将该 Cookie 发回服务器。在这里插入图片描述1. 拦截就是用来识别用户身份的。(通过token识别出来用户id.在redis拿到用户对象)
    2.之后把用户信息存储到threadlocal中。
    在这里插入图片描述

Threadlocal

  • 一种用于实现线程局部变量的机制,主要是指“线程的空间”。
  • 一个线程可以有多个Threadlocal空间。
  • 每个线程的threadlocal都是独立分开的。每个线程都有自己的空间。
  • 但是用完之后要记得删除空间的数据,这样才会线程回收。避免线程数据泄漏。
package com.atguigu.ssyx.common.auth;import com.atguigu.ssyx.vo.user.UserLoginVo;
import lombok.Data;@Datapublic class AuthContextHolder {private static ThreadLocal<Long> userId = new ThreadLocal<>();private static ThreadLocal<Long> wareId = new ThreadLocal<>();private static ThreadLocal<UserLoginVo> userLoginVo = new ThreadLocal<>();public static void setUserId(Long id) {userId.set(id);}public static Long getUserId() {return userId.get();}public static void setWareId(Long id) {wareId.set(id);}public static Long getWareId() {return wareId.get();}public static void setUserLoginVo(UserLoginVo userLoginVo) {AuthContextHolder.userLoginVo.set(userLoginVo);}public static UserLoginVo getUserLoginVo() {return userLoginVo.get();}
}
  • 一个空间有get() set()。

  • session key是手机号 登录就是code。

session

每个用户的 Session 对象是独立的,并且在服务端会为每个用户维护一个唯一的 Session 对象。每个 Session 对象相当于一个存储空间,可以存储多个 key-value 对,来保存该用户的相关信息。

具体说明:

  1. 每个用户都有独立的 Session

    • 当用户访问服务器时,服务器会为该用户创建一个唯一的 Session,并通过 Session ID 来标识用户的这个会话。这个 Session ID 通常会通过浏览器的 Cookie 进行存储,每次请求时自动发送给服务器。
  2. 每个 Session 可以存储多个 key-value

    • Session 就像是一个存储容器,每个用户都有一个独立的容器,你可以通过 session.setAttribute(key, value) 往里面放入多个数据。
    • 例如,存储用户登录信息时可以设置:
      session.setAttribute("user", userObject);  // 存储用户信息对象
      session.setAttribute("phone", phoneNumber);  // 存储用户手机号
      session.setAttribute("code", verificationCode);  // 存储验证码
      
  3. 访问 Session 中的数据

    • 你可以通过 session.getAttribute(key) 来获取存储在 Session 中的数据。例如:
      User user = (User) session.getAttribute("user");  // 获取用户信息
      String phone = (String) session.getAttribute("phone");  // 获取手机号
      
  4. 每个用户的 Session 数据互相隔离

    • 由于每个用户都有自己独立的 Session 对象,因此用户 A 的 Session 和用户 B 的 Session 是完全隔离的,互不影响。用户 A 的 Session 中的数据不会影响到用户 B。
  5. Session 的作用范围

    • Session 在用户与服务器的会话期间有效,如果会话结束(例如用户关闭浏览器、Session 超时等),服务器可能会销毁该 Session,此时存储在 Session 中的数据也会失效。

小结:

  • 每个用户都会有独立的 Session 对象。
  • 你可以在 Session 中存储多个 key-value 数据对。
  • 每个用户的 Session 数据是相互独立、隔离的,不会发生冲突或覆盖。

这样就可以在不混淆不同用户数据的情况下,轻松实现如登录状态、购物车、验证码等信息的存储和管理。

什么时候会生成新的 Session ID:
  1. 首次访问:用户第一次访问时,服务器会生成新的 Session ID。
  2. 会话过期:如果用户的会话(Session)过期了,服务器会删除旧的 Session 对象,用户再次访问时会生成一个新的 Session ID。
  3. 每次调用 session.setAttribute(phone, code) 都不会生成新的 Session ID。Session ID 是在用户第一次访问服务器并创建会话时生成的,之后同一个会话中,无论你调用多少次 setAttribute 或进行其他操作,Session ID 都保持不变。

登录拦截器

package com.hmdp.utils;import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//    1.获得session。HttpSession session = request.getSession();
//        2.从session获得user。Object user = session.getAttribute("user");
//        3.判断用户是否存在。if (user == null) {
//            还没有登录。response.setStatus(401);return false;}UserHolder.saveUser((User) user);return true;//        从session中取出user,放入ThreadLocal中。}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
  • session的user代表有没有登录。

  • Threadlocal代表线程的空间。

  • 登录的时候存储 , 在登录拦截器 查询有无user字段。

处理敏感信息

  • 使用dto处理去除掉一些关键信息字段。

在这里插入图片描述

  • 登录拦截器只要找不到user的话就返回401状态码。
  • Session 通常是基于服务器内存存储的。

在这里插入图片描述

redis+token

  • 验证手机号码key变成手机号码。

  • 登录注册 value不变,但是key是要改变,key是token。

   UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMaps = BeanUtil.beanToMap(userDTO);
  • 把对象的所有属性,拿到map中。

登录状态的

  • 在登录时候会存入token,这个是用来判断是否在登录状态的。后面都需要依靠这个来进行判断。

其他情况

在这里插入图片描述

  • 拦截器如果用户登录了,但是首页和文章页面 没有拦截,用户浏览了1小时,可是由于没有刷新redis的该用户数据,导致登录失效。这是不合理的。

  • 登录拦截器就是有些页面对用户信息有需求的。

  • 所有路径都刷新token存储时间,

  • 登录拦截器就是依靠ThreadLocal判断是否登录,就是功能还是不变,就是多了一个所有路径都刷新存活时间。

缓存

在这里插入图片描述

  • 比如浏览器找不到再去tomcat找这种行为叫未命中。
    在这里插入图片描述

在这里插入图片描述

  • 我自己写的时候使用的是opsHash()。但是使用的是opsValue()
  • 而且使用的是stringtemplate,那么java对象需要转换格式。

报错:

 问题的核心在于 Jackson 不知道如何将 JSON 中的日期时间字符串解析为 Java 的 LocalDateTime 对象。由于 LocalDateTime 没有默认构造函数,且日期时间的格式可能多种多样(例如 yyyy-MM-dd、yyyy-MM-dd'T'HH:mm:ss 等等),Jackson 无法直接把这些字符串转换为 LocalDateTime 类型的对象。+ json字符串不知道java类中的时间字段的格式所以失败了,我们应该在类上面写好时间的格式。
 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")/*** 更新时间*/private LocalDateTime updateTime;

在这里插入图片描述

 @Overridepublic List<ShopType> getCategory() throws JsonProcessingException {String shop_type_string = stringRedisTemplate.opsForValue().get(RedisConstant.Shop_type);if (StringUtils.isNotBlank(shop_type_string)) {List<ShopType> shopTypes = objectMapper.readValue(shop_type_string, new TypeReference<List<ShopType>>() {});return shopTypes;}List<ShopType> ShopTypeList = this.list();if (ShopTypeList == null) {throw new HmdpException(ResultCodeEnum.DATA_ERROR);}String json = objectMapper.writeValueAsString(ShopTypeList);stringRedisTemplate.opsForValue().set(RedisConstant.Shop_type,json);return ShopTypeList;}

ShopTypeList == null与 CollectionUtils.isEmpty(ShopTypeList) 还有 if (StringUtils.isBlank(shop_type_string)) { 与 shop_type_string==null

使用 ShopTypeList == null 时,只是判断是否为 null。表示还没有初始化。
使用 CollectionUtils.isEmpty(ShopTypeList) 可以同时判断 null 和空集合,这样在处理集合时更加安全和方便。

这个方法不仅检查 shop_type_string 是否为 null,还会检查它是否为空字符串 (“”)

缓存策略

在这里插入图片描述

在这里插入图片描述

  • 在更新数据库的同时更新redis。

在这里插入图片描述
在这里插入图片描述

  • 先操作数据库,在操作redis。在这里插入图片描述
  • 读数据库时候更新设定redis数据时间
  • 更新删除 数据库的时候,删除redis对应数据。
实际操作
    @Autowiredprivate StringRedisTemplate stringRedisTemplate;private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic Shop queryById(Long id) throws JsonProcessingException {String shopJSon = stringRedisTemplate.opsForValue().get(RedisConstant.Shop_store + id);
// 注册 JavaTimeModule 以支持 LocalDateTime 反序列化objectMapper.registerModule(new JavaTimeModule());if (StringUtils.isNotBlank(shopJSon)) {
//            redis有数据。命中Shop shop = objectMapper.readValue(shopJSon, Shop.class);return shop;}Shop shop = getById(id);if (shop == null) {throw new HmdpException(ResultCodeEnum.shop_Not);}String shopString = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, shopString);stringRedisTemplate.expire(RedisConstant.Shop_store + id, 20, java.util.concurrent.TimeUnit.MINUTES);return shop;}@Override@Transactionalpublic void updateShop(Shop shop) {boolean b = this.updateById(shop);stringRedisTemplate.delete(RedisConstant.Shop_store + shop.getId());}
  • 要加上事务。

缓存穿透

在这里插入图片描述

  • 先查找布隆过滤器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

 @Overridepublic Shop queryById(Long id) throws JsonProcessingException {String shopJSon = stringRedisTemplate.opsForValue().get(RedisConstant.Shop_store + id);
// 注册 JavaTimeModule 以支持 LocalDateTime 反序列化objectMapper.registerModule(new JavaTimeModule());if (shopJSon.equals("")) {
//            redis有数据。命中空字符。throw new HmdpException(ResultCodeEnum.shop_Not);}if (StringUtils.isNotBlank(shopJSon)) {
//            redis有数据。命中Shop shop = objectMapper.readValue(shopJSon, Shop.class);return shop;}Shop shop = getById(id);if (shop == null) {
//            穿透了.stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, "", 20, java.util.concurrent.TimeUnit.MINUTES);throw new HmdpException(ResultCodeEnum.shop_Not);}String shopString = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, shopString);stringRedisTemplate.expire(RedisConstant.Shop_store + id, 20, java.util.concurrent.TimeUnit.MINUTES);return shop;}
  • 数据库找不到的话缓存进入redis
  • redis判断是否是空字符串,是的话返回。

缓存雪崩

在这里插入图片描述

  • 多级缓存:浏览器保存的是静态资源,无法保存动态资源。
    public Shop queryWithPassThrough(Long id) throws JsonProcessingException {String shopJSon = stringRedisTemplate.opsForValue().get(RedisConstant.Shop_store + id);
// 注册 JavaTimeModule 以支持 LocalDateTime 反序列化objectMapper.registerModule(new JavaTimeModule());if ("".equals(shopJSon)) {
//            redis有数据。命中空字符。throw new HmdpException(ResultCodeEnum.shop_Not);}if (StringUtils.isNotBlank(shopJSon)) {
//            redis有数据。命中Shop shop = objectMapper.readValue(shopJSon, Shop.class);return shop;}Shop shop = getById(id);if (shop == null) {
//            穿透了.stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, "", 20, java.util.concurrent.TimeUnit.MINUTES);throw new HmdpException(ResultCodeEnum.shop_Not);}String shopString = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, shopString);// 生成一个介于0到100之间的随机整数Random random = new Random();// 生成一个0到99之间的随机整数,防止redis雪崩。int randomInt = random.nextInt(100);stringRedisTemplate.expire(RedisConstant.Shop_store + id, randomInt + 10, java.util.concurrent.TimeUnit.MINUTES);return shop;}

热点key问题。(缓存击穿)

在这里插入图片描述

  • 大量访问
  • 构建起来的时间很长。

如果key过期了,在构建key时候大量的请求访问到数据库去。数据库压力巨大。

在这里插入图片描述

  • 互斥锁:让更新redis的线程获得互斥锁,其他线程在睡眠和获得redis中不断循环。
  • 逻辑过期:时间过期了,让一个线程开启一个线程去更新数据(开启互斥锁)。然后拿旧数据。其他线程也拿旧数据。

在这里插入图片描述

互斥锁操作。

在这里插入图片描述

在这里插入图片描述

  • 先判断key和锁的情况。
  • 如果可以的话执行, 在数据库和添加redis操作之间添加加锁和删除锁的操作。
 //互斥锁。public Shop queryWithLock(Long id) throws JsonProcessingException, InterruptedException {String key = RedisConstant.Shop_store + id;String shopJSon = stringRedisTemplate.opsForValue().get(key);
// 注册 JavaTimeModule 以支持 LocalDateTime 反序列化objectMapper.registerModule(new JavaTimeModule());if ("".equals(shopJSon)) {
//            redis有数据。命中空字符。throw new HmdpException(ResultCodeEnum.shop_Not);}if (StringUtils.isNotBlank(shopJSon)) {
//            redis有数据。命中Shop shop = objectMapper.readValue(shopJSon, Shop.class);return shop;}
//            没有找到合理key数据。要去数据库找。String lockKey = "lock:shop:" + id;boolean isLock = tryLock(lockKey);Shop shop = null;try {if (!isLock) {//           加锁失败Thread.sleep(500);return queryWithPassThrough(id);}
//加锁成功继续执行shop = getById(id);if (shop == null) {//            穿透了.stringRedisTemplate.opsForValue().set(key, "", 20, TimeUnit.MINUTES);throw new HmdpException(ResultCodeEnum.shop_Not);}
//没有穿透继续执行。String shopString = objectMapper.writeValueAsString(shop);stringRedisTemplate.opsForValue().set(key, shopString);// 生成一个介于0到100之间的随机整数Random random = new Random();// 生成一个0到99之间的随机整数,防止redis雪崩。int randomInt = random.nextInt(100);stringRedisTemplate.expire(key, 100 + randomInt, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} catch (JsonProcessingException e) {throw new RuntimeException(e);} catch (HmdpException e) {throw new RuntimeException(e);} finally {unLock(lockKey);}return shop;}public boolean tryLock(String id) {
//如果有锁。boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(id, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}public void unLock(String id) {stringRedisTemplate.delete(id);}
  • 锁的key和店铺的key是不一样的。

在这里插入图片描述
在这里插入图片描述

逻辑过期

在这里插入图片描述

  • 先判断逻辑过期 在判断获得到锁
  • 获得到开启新的线程 开启锁 查询数据库和写入redis 释放锁。
  • 返回旧的数据。

这个没有命中的话,是需要先在redis把数据放上去的。

在互斥锁中是:

  • 先判断key和锁的情况。
  • 如果可以的话执行, 在数据库和添加redis操作之间添加加锁和删除锁的操作。

区别在于:开启新的线程;返回旧的数据。

//    逻辑过期public Shop queryWithLogicExpire(Long id) throws JsonProcessingException {String shopJSon = stringRedisTemplate.opsForValue().get(RedisConstant.Shop_store + id);
// 注册 JavaTimeModule 以支持 LocalDateTime 反序列化objectMapper.registerModule(new JavaTimeModule());if ("".equals(shopJSon)) {
//            redis有无效数据。命中空字符。throw new HmdpException(ResultCodeEnum.shop_Not);}if (StringUtils.isBlank(shopJSon)) {return null;}RedisData redisData = objectMapper.readValue(shopJSon, RedisData.class);
// 反序列化 redisData 对象的 data 部分为 Shop 类Shop redisData_shop = objectMapper.convertValue(redisData.getData(), Shop.class);//4.        redis命中后判断是否不为空。而且判断时间是否过期,LocalDateTime now = LocalDateTime.now();// 转换为时间戳(秒数)long timestampInSeconds = now.toEpochSecond(ZoneOffset.UTC);if (redisData.getExpireTime() > timestampInSeconds && StringUtils.isNotBlank(shopJSon)) {
//没有过期return redisData_shop;}
//        过期了String lockKey = "lock:shop:" + id;boolean b = tryLock(lockKey);if (!b) {return redisData_shop;}
//锁上了。CACHE_REBUILD_EXECUTOR.submit(() -> {try {saveShop2Redis(id, 60 * 60L);} catch (JsonProcessingException e) {throw new RuntimeException(e);} finally {this.unLock(lockKey);}});
//        未过期,返回数据//        过期,//        获得互斥锁。成功的话进行开启另一个线程//        失败的话,返回旧数据。return redisData_shop;}
装饰器模式

是一种结构型设计模式,它允许你通过将对象放入一个包含新行为的包装类(也称为装饰器)中,来动态地向原始对象添加新的功能,而无需修改原始类的代码。与继承不同,装饰器模式更灵活,因为它允许在运行时动态组合对象的功能。

package com.hmdp.utils;import lombok.Data;import java.time.LocalDateTime;@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

这个data可以指向其他的对象,时间是特别行为。

练习

在这里插入图片描述

使用这个解决对象存入redis格式问题。

   @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")@JsonDeserialize(using = LocalDateTimeDeserializer.class)@JsonSerialize(using = LocalDateTimeSerializer.class)private LocalDateTime expireTime;
反序列化
String key = "yourRedisKey"; // 您要获取的 Redis 键
String jsonData = stringRedisTemplate.opsForValue().get(key); // 从 Redis 中获取 JSON 字符串if (jsonData != null) {try {// 反序列化为 RedisData 对象RedisData redisData = objectMapper.readValue(jsonData, RedisData.class);// 获取数据并进行类型转换Object data = redisData.getData();if (data instanceof Shop) { // 检查类型String jsonShop = objectMapper.writeValueAsString(data);Shop shop = objectMapper.readValue(jsonShop, Shop.class);// 现在您可以使用 shop 对象} else {// 处理不匹配的情况}// 访问过期时间LocalDateTime expireTime = redisData.getExpireTime();} catch (JsonProcessingException e) {// 处理反序列化失败的情况e.printStackTrace();}
} else {// 处理未找到的情况
}
  • 对象里面还有对象,需要进行两次的反序列化。

缓存穿透:通过存储空值来避免反复查询数据库。当查询一个不存在的数据时,会直接命中空值,而不是频繁访问数据库。(一共两条,查询数据库为空时候写入redis;查询redis时候返回找不到错误信息。)
缓存雪崩:通过为缓存设置不同的过期时间,避免大量缓存同时失效,减轻数据库压力。(在读取到的数据,设置随机过期时间。)

工具类

package com.hmdp.utils;import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;@Component
@Slf4j
public class CacheClient {@Autowiredprivate StringRedisTemplate stringRedisTemplate;ObjectMapper objectMapper = new ObjectMapper();public void set(String key, Object object, Long time, TimeUnit unit) throws JsonProcessingException {stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(object), time, unit);}public void setWithLogicExpices(String key, Object object, Long time) throws JsonProcessingException {RedisData redisData = new RedisData();redisData.setData(object);redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(redisData));}//    雪崩public <R, ID> R queryWithPassThrough(String head, ID id, Class<R> type, Function<ID, R> dback, Long time, TimeUnit unit) throws JsonProcessingException {String key = head + id;String shopJSon = stringRedisTemplate.opsForValue().get(key);objectMapper.registerModule(new JavaTimeModule());if ("".equals(shopJSon)) {
//            redis有数据。命中空字符。throw new HmdpException(ResultCodeEnum.shop_Not);}if (StringUtils.isNotBlank(shopJSon)) {
//            redis有数据。命中R r = objectMapper.readValue(shopJSon, type);return r;}R r = dback.apply(id);if (r == null) {
//            穿透了.stringRedisTemplate.opsForValue().set(key, "", 20, TimeUnit.SECONDS);throw new HmdpException(ResultCodeEnum.shop_Not);}String shopString = objectMapper.writeValueAsString(r);stringRedisTemplate.opsForValue().set(key, shopString);stringRedisTemplate.expire(RedisConstant.Shop_store + id, time, unit);return r;}public boolean tryLock(String id) {
//如果有锁。boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(id, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}public void unLock(String id) {stringRedisTemplate.delete(id);}private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//    逻辑过期public <R, ID> R queryWithLogicExpire(String keyPredfix, ID id, Class<R> type, Function<ID, R> dback) throws JsonProcessingException {String shopJSon = stringRedisTemplate.opsForValue().get(keyPredfix + id);
// 注册 JavaTimeModule 以支持 LocalDateTime 反序列化objectMapper.registerModule(new JavaTimeModule());if ("".equals(shopJSon) || StringUtils.isBlank(shopJSon)) {
//            redis有无效数据。命中空字符。throw new HmdpException(ResultCodeEnum.shop_Not);}RedisData redisData = objectMapper.readValue(shopJSon, RedisData.class);
// 反序列化 redisData 对象的 data 部分为 Shop 类R redisData_shop = objectMapper.convertValue(redisData.getData(), type);//4.        redis命中后判断是否不为空。而且判断时间是否过期,// 转换为时间戳(秒数)if (redisData.getExpireTime().isAfter(LocalDateTime.now()) && StringUtils.isNotBlank(shopJSon)) {
//没有过期return redisData_shop;}
//        过期了String lockKey = RedisConstant.CACHE_Shop_Lock + id;boolean b = tryLock(lockKey);if (!b) {return redisData_shop;}
//锁上了。CACHE_REBUILD_EXECUTOR.submit(() -> {try {saveShop2Redis(id, 60 * 60L, dback);} catch (JsonProcessingException e) {throw new RuntimeException(e);} finally {this.unLock(lockKey);}});return redisData_shop;}public <R, ID> void saveShop2Redis(ID id, long expireSeconds, Function<ID, R> dback) throws JsonProcessingException {R shop = dback.apply(id);if (shop == null) {stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, "", 20, TimeUnit.MINUTES);}RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));String s = objectMapper.writeValueAsString(redisData);stringRedisTemplate.opsForValue().set(RedisConstant.Shop_store + id, s);}
}
 Shop shop = cacheClient.queryWithLogicExpire(RedisConstant.Shop_store, id, Shop.class, id2 -> getById(id2));
  • id2 -> getById(id2)这个id2不是太重要,重要的是实际参数传入。
  • R shop = dback.apply(id);

实战篇2:优惠券

在这里插入图片描述

全局唯一ID

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 返回key是时间戳和序列化拼接。如果使用字符串拼接又转化回来的时候有些麻烦。
  • 我们使用
return timecurrent << 32 | increment;

可以使用字符串拼接生成唯一id吗?

字符串拼接通常指的是在编程中通过连接字符串来生成新的字符串。如果您的意思是在每秒内通过字符串拼接来生成大量的唯一标识符(ID),那么这种方法在理论上是可行的,但实际应用中有几个问题需要考虑:

  • 性能问题:字符串拼接在某些编程语言中(如Python)是一个相对较慢的操作,特别是在高频率调用时。如果每秒需要生成数以百万计的ID,字符串拼接可能会导致性能瓶颈。

  • 唯一性:通过字符串拼接生成唯一ID,需要确保每次拼接的字符串都是唯一的。这通常需要依赖于外部因素(如时间戳、随机数生成器等)来保证。

并发问题:在多线程或分布式系统中,确保并发访问时ID的唯一性是一个挑战。如果多个进程或线程同时生成ID,可能会导致ID冲突。

可读性和可维护性:使用字符串拼接生成ID可能会使ID的格式变得复杂,这可能会影响ID的可读性和后续处理的可维护性。

Redis 实现自增计数器

这一行代码的目的是通过 Redis 来实现一个自增计数器,以便生成唯一 ID。下面是对这段代码的详细解释:


long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyprefix + ":" + yyyyMMdd);
线程池没有打印出信息。
  • 正是因为主线程提前结束了,导致线程池中的任务还没有来得及运行。所以运行了没有信息。
  if (!es.awaitTermination(60, TimeUnit.SECONDS)) {es.shutdownNow(); // 如果60秒后任务还未完成,强制关闭线程池}
  • 使用这个就是让主线程等待60s。

添加优惠券。

在这里插入图片描述

  • 平价券和特价券。
    在这里插入图片描述
    在这里插入图片描述
  • 库存 使用时间范围 创建和失效时间。

在这里插入图片描述

  • 判断抢购时间和库存。
  @Override@Transactionalpublic long seckillVoucher(Long voucherId) {
//        1.查询优惠券SeckillVoucher vouche = seckillVoucherService.getById(voucherId);LocalDateTime beginTime = vouche.getBeginTime();LocalDateTime endTime = vouche.getEndTime();
//        2.判断开始抢购时间了吗if (LocalDateTime.now().isBefore(beginTime) && LocalDateTime.now().isAfter(endTime)) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStart);}//        3.判断库存充足吗?Integer stock = vouche.getStock();if (stock <= 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}
//        创建订单。VoucherOrder voucherOrder = new VoucherOrder();//        获得订单id。long order = redisIdWorder.netxId("order");voucherOrder.setId(order);
//        用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);voucherOrder.setVoucherId(voucherId);save(voucherOrder);return voucherOrder.getId();}

超卖问题,没有解决的话会给商家带来经济损失。

在这里插入图片描述

  • 先查询之后扣减。
  • 此时库存为1,但是两个线程一起发生了查询有库存,接着继续扣减就变成了-1;

在这里插入图片描述

  • 之前是有多个线程来访问数据库的。加上锁之后只能有一个线程访问数据库。

  • 加上锁后,只有一个线程可以访问和修改数据库

  • 就是所有线程都要排队访问数据库,访问和修改数据库,这就是悲观锁。

  • 乐观锁可以多个线程访问。

悲观锁实现

要在你的 seckillVoucher 方法中加上悲观锁,可以按照以下步骤进行操作:

  • 悲观锁(如 FOR UPDATE):在数据库层面加锁,其他事务在这个事务未完成前无法读取被锁定的数据。适用于对数据的竞争较高的场景。

  • Java 中的 synchronized:在应用层面控制线程访问,保证同一时间只有一个线程能执行加锁的代码块。适用于代码中需要保护共享资源的场景。

1. SQL 查询中添加行级锁

在你的 MyBatis Mapper 中,使用 FOR UPDATE 来锁定记录。修改你的 SQL 查询如下:

<select id="getVoucherForUpdate" resultType="com.hmdp.entity.Voucher" parameterType="java.lang.Long">SELECT * FROM tb_voucher WHERE id = #{id} AND status = 1 FOR UPDATE
</select>

2. 修改 Service 层逻辑

在 Service 层中,调用这个带锁的查询方法,并在一个事务中执行秒杀逻辑。确保方法上加上 @Transactional 注解:

import org.springframework.transaction.annotation.Transactional;@Service
public class VoucherOrderService {@Autowiredprivate VoucherMapper voucherMapper;@Transactionalpublic long seckillVoucher(Long voucherId) {// 获取优惠券并加锁Voucher voucher = voucherMapper.getVoucherForUpdate(voucherId);// 检查优惠券是否可用if (voucher == null || voucher.getStock() <= 0) {throw new HmdpException(ResultCodeEnum.voucher_Not_Exist);}// 执行秒杀逻辑,例如减少库存// voucher.setStock(voucher.getStock() - 1);// voucherMapper.updateStock(voucher);// 返回结果,例如订单号等return orderId; // 返回订单 ID}
}

3. Controller 层保持不变

你的 Controller 层代码可以保持不变,只需确保正确调用 Service 层的方法。

4. 重要注意事项

  • 事务管理:确保在使用 @Transactional 注解的同时,保证整个秒杀过程是在同一事务中完成的,以确保数据的一致性。
  • 性能影响:使用悲观锁会影响系统的性能,特别是在高并发场景下,可能导致锁竞争。需要根据实际情况合理使用。
  • 数据库支持:确保你的数据库支持行级锁(如 MySQL、PostgreSQL 等)。
还有synchronized 关键字。jdk方法。
 //    领取订单@PostMapping("seckill/{id}")public synchronized Result seckillVoucher(@PathVariable("id") Long voucherId) {long l = voucherOrderService.seckillVoucher(voucherId);return Result.ok(l);}

这些都是悲观锁。

乐观锁

在这里插入图片描述

  • 根据之前版本进行查询,查询的到代表没有修改。就进行减库存和加版本操作。
    在这里插入图片描述
    根据版本号进行查询。
  • 之前拿到的版本号与再次查询版本号一样,第二次查询会出现没有修改和已经修改。

CAS方法(去除了version,更简约。)

在这里插入图片描述

实操
@Override@Transactionalpublic long seckillVoucher(Long voucherId) {
//        1.查询优惠券SeckillVoucher vouche = seckillVoucherService.getById(voucherId);Integer stock1 = vouche.getStock();LocalDateTime beginTime = vouche.getBeginTime();LocalDateTime endTime = vouche.getEndTime();
//        2.判断开始抢购时间了吗if (LocalDateTime.now().isBefore(beginTime) && LocalDateTime.now().isAfter(endTime)) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStart);}//        3.判断库存充足吗?Integer stock = vouche.getStock();if (stock <= 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}LambdaQueryWrapper<SeckillVoucher> ldbQueryWrapper = new LambdaQueryWrapper();ldbQueryWrapper.eq(SeckillVoucher::getVoucherId, voucherId).eq(SeckillVoucher::getStock, stock1);SeckillVoucher vouche_Check = seckillVoucherService.getOne(ldbQueryWrapper);if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}
//        创建订单。VoucherOrder voucherOrder = new VoucherOrder();//        获得订单id。long order = redisIdWorder.netxId("order");voucherOrder.setId(order);
//        用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);voucherOrder.setVoucherId(voucherId);save(voucherOrder);return voucherOrder.getId();}
LambdaQueryWrapper<SeckillVoucher> ldbQueryWrapper = new LambdaQueryWrapper();ldbQueryWrapper.eq(SeckillVoucher::getVoucherId, voucherId).eq(SeckillVoucher::getStock, stock1);SeckillVoucher vouche_Check = seckillVoucherService.getOne(ldbQueryWrapper);if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}
  • 先查询数据库对应优惠券行,(经过一些操作),再查询一次检查是否有变化,在进行修改。
    心得:
    在这里插入图片描述

一人一单

在这里插入图片描述

  • 扣减库存代表满足所有条件了。

在这里插入图片描述

  @Override@Transactionalpublic long seckillVoucher(Long voucherId) {
//        1.查询优惠券SeckillVoucher vouche = seckillVoucherService.getById(voucherId);Integer stock1 = vouche.getStock();LocalDateTime beginTime = vouche.getBeginTime();LocalDateTime endTime = vouche.getEndTime();
//        2.判断开始抢购时间了吗if (LocalDateTime.now().isBefore(beginTime) && LocalDateTime.now().isAfter(endTime)) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStart);}//        3.判断库存充足吗?Integer stock = vouche.getStock();if (stock <= 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}//乐观锁更新之前的检查LambdaQueryWrapper<SeckillVoucher> ldbQueryWrapper = new LambdaQueryWrapper();ldbQueryWrapper.eq(SeckillVoucher::getVoucherId, voucherId).eq(SeckillVoucher::getStock, stock1);SeckillVoucher vouche_Check = seckillVoucherService.getOne(ldbQueryWrapper);//        检查是否优惠券已经使用了。Long user_id = UserHolder.getUser().getId();Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();if (count != 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_use);}if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}
//        创建订单。VoucherOrder voucherOrder = new VoucherOrder();//        获得订单id。long order = redisIdWorder.netxId("order");voucherOrder.setId(order);
//        用户idvoucherOrder.setUserId(user_id);voucherOrder.setVoucherId(voucherId);save(voucherOrder);return voucherOrder.getId();}
  • 这里的一人一单完成了 但是在压力测试下,还是出现了添加多个单的情况。
    在这里插入图片描述

  • 很多线程都是在做检查数量。

  • 乐观锁就是在更新数据的时候使用的。

  • 悲观锁是在插入数据的时候时候的。

private synchronized VoucherOrder getVoucherOrder(Long voucherId, SeckillVoucher vouche_Check) {//      一人一单。  检查是否优惠券已经使用了。Long user_id = UserHolder.getUser().getId();Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();if (count != 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_use);}if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {
  • 使用synchronized来进行是把任何线程都是串行执行。如果给每个synchronized同一个用户的给一把锁。这样就可以多个用户访问了。同一个用户的请求只能一个一个访问了。
修改
private VoucherOrder getVoucherOrder(Long voucherId, SeckillVoucher vouche_Check) {Long id = UserHolder.getUser().getId();synchronized (id.toString()) {//      一人一单。  检查是否优惠券已经使用了。Long user_id = UserHolder.getUser().getId();Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();if (count != 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_use);}if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}
//        创建订单。VoucherOrder voucherOrder = new VoucherOrder();//        获得订单id。long order = redisIdWorder.netxId("order");voucherOrder.setId(order);
//        用户idvoucherOrder.setUserId(user_id);voucherOrder.setVoucherId(voucherId);save(voucherOrder);return voucherOrder;}}
  • 这个Long id = UserHolder.getUser().getId(); synchronized (id.toString()) {}
  • synchronized是看对象的地址来判断是不是同一个锁的。
  • synchronized (id.toString().intern()) {}这样来看的话他会查找字符串常量池方法。

在这里插入图片描述

  • 方法内的数据库操作都是@Tansitional注解帮助我们进行的,只有等到方法结束才会帮我们提交或者回滚。

我们的目的:数据修改完成之后才可以释放锁,所以

synchronized (id.toString().intern()) {VoucherOrder voucherOrder = getVoucherOrder(voucherId, vouche_Check);return voucherOrder.getId();}
    @Transactionalpublic VoucherOrder getVoucherOrder(Long voucherId, SeckillVoucher vouche_Check) {//      一人一单。  检查是否优惠券已经使用了。Long user_id = UserHolder.getUser().getId();Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();if (count != 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_use);}if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}
//        创建订单。VoucherOrder voucherOrder = new VoucherOrder();//        获得订单id。long order = redisIdWorder.netxId("order");voucherOrder.setId(order);
//        用户idvoucherOrder.setUserId(user_id);voucherOrder.setVoucherId(voucherId);save(voucherOrder);return voucherOrder;}
  • 在事务操作外面加上锁。

  • 事务的实现方式

  1. 事务管理 只有方法在代理对象才可以进行事务管理。
  2. 自己增加的普通java方法是没有在代理对象中。
    在这里插入图片描述
  • 解决方式:把那个普通java方法接口写到接口文件中。

  • 这一块没听懂的建议看下spring声明式事务的原理,是通过aop的动态代理实现的,这里是获取到这个动态代理,让动态代理去调用方法

成功代码

package com.hmdp.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
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.HmdpException;
import com.hmdp.utils.RedisIdWorder;
import com.hmdp.utils.ResultCodeEnum;
import com.hmdp.utils.UserHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;/*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorder redisIdWorder;@Autowiredprivate  IVoucherOrderService  IVoucherOrderServicelmpl;@Overridepublic long seckillVoucher(Long voucherId) {
//        1.查询优惠券SeckillVoucher vouche = seckillVoucherService.getById(voucherId);Integer stock1 = vouche.getStock();LocalDateTime beginTime = vouche.getBeginTime();LocalDateTime endTime = vouche.getEndTime();
//        2.判断开始抢购时间了吗if (LocalDateTime.now().isBefore(beginTime) && LocalDateTime.now().isAfter(endTime)) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStart);}//        3.判断库存充足吗?Integer stock = vouche.getStock();if (stock <= 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}//      乐观锁更新之前的检查LambdaQueryWrapper<SeckillVoucher> ldbQueryWrapper = new LambdaQueryWrapper();ldbQueryWrapper.eq(SeckillVoucher::getVoucherId, voucherId).eq(SeckillVoucher::getStock, stock1);SeckillVoucher vouche_Check = seckillVoucherService.getOne(ldbQueryWrapper);Long id = UserHolder.getUser().getId();synchronized (id.toString().intern()) {VoucherOrder voucherOrder;voucherOrder = IVoucherOrderServicelmpl.getVoucherOrder(voucherId, vouche_Check);return voucherOrder.getId();}}@Transactionalpublic VoucherOrder getVoucherOrder(Long voucherId, SeckillVoucher vouche_Check) {//      一人一单。  检查是否优惠券已经使用了。Long user_id = UserHolder.getUser().getId();Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();if (count != 0) {throw new HmdpException(ResultCodeEnum.shop_voucher_use);}if (vouche_Check == null) {throw new HmdpException(ResultCodeEnum.shop_voucher_versionChange);}//        充足的话扣减库存。boolean voucherId1 = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!voucherId1) {throw new HmdpException(ResultCodeEnum.shop_voucher_notStock);}
//        创建订单。VoucherOrder voucherOrder = new VoucherOrder();//        获得订单id。long order = redisIdWorder.netxId("order");voucherOrder.setId(order);
//        用户idvoucherOrder.setUserId(user_id);voucherOrder.setVoucherId(voucherId);save(voucherOrder);return voucherOrder;}}

心得:

  • 锁应该包括了整个事务的方法。
  • 事务操作多个表,只有事务方法完成之后才会进行提交。
  • 事务可以成功是事务方法在代理对象上。

集群

在这里插入图片描述

server {listen       8080;server_name  localhost;# 指定前端项目所在的位置location / {root   html/hmdp;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}location /api {  default_type  application/json;#internal;  keepalive_timeout   30s;  keepalive_requests  1000;  #支持keep-alive  proxy_http_version 1.1;  rewrite /api(/.*) $1 break;  proxy_pass_request_headers on;#more_clear_input_headers Accept-Encoding;  proxy_next_upstream error timeout;  # 后端地址# proxy_pass http://127.0.0.1:8099;proxy_pass http://backend;}}upstream backend {server 127.0.0.1:8099 max_fails=5 fail_timeout=10s weight=1;server 127.0.0.1:8100 max_fails=5 fail_timeout=10s weight=1;}  
  • nginx.exe -s reload

  • 默认采用轮训

  • 两个服务的字符串常量池是不共享的导致这里的用户id的字符串是俩个不同的对象所以锁不上.

在这里插入图片描述
在这里插入图片描述

  • 有多个进程进入了锁里面。因为多个实例有多个jvm,多个常量池。‘’
  • 多个jvm需要使用同一个锁才可以解决。

在这里插入图片描述

  • 集群通常指的是同一个服务的多个实例(例如多个服务器上运行相同的应用),它们共同工作以提供更高的可用性和负载均衡。

分布式锁

在这里插入图片描述
在这里插入图片描述

  • 需要一个公共的锁.
  • 在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • mysql也有分布式锁,for update。
  • redis有setnx key 需要手动删除。
  • 分布式锁就是多个线程可以互斥。

在这里插入图片描述
在这里插入图片描述

  • setnx key value 返回1或者0。
  • del key
  • expire key 5 定时5s. 如果在redis中进入锁的时候,redis崩溃,那么这个锁就无法删除。后面进程一直等待,

set key value ex 100 nx

在这里插入图片描述

  • 后面的进程有阻塞式和非阻塞方式。

改进思想

  1. 之前使用的是jvm的字符串常量池,锁是悲观锁的sylizattion关键字,锁放在常量池。
  2. 现在使用的是redis分布式锁,因为所有服务实例都可以访问到。只要建设和删除一个锁。

http://www.ppmy.cn/news/1535865.html

相关文章

编写高质量dbt模型实践指南

数据建模是越来越受关注的话题&#xff0c;尤其是在分析工程领域。数据建模和流行的数据转换工具dbt是相辅相成的。虽然数据建模概念已经存在很长时间&#xff0c;但dbt让它更具体、更易实现。 可以将数据模型理解为一系列转换&#xff0c;这些转换将数据从原始形式转化为最终可…

物联网 IOT 与工业物联网 IIOT 极简理解

物联网 IOT IOT&#xff08;全称 Internet of Things&#xff09;指物联网&#xff0c;它是指通过互联网连接&#xff0c;将各种物体&#xff08;例如&#xff0c;传感器、设备、车辆等&#xff09;和人进行互联互通的网络系统 物联网的核心是将各种物体连接到互联网&#xff…

Github 2024-10-03Go开源项目日报Top10

根据Github Trendings的统计,今日(2024-10-03统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Go项目10TypeScript项目1快速且可扩展的多平台Web服务器 创建周期:3551 天开发语言:Go协议类型:Apache License 2.0Star数量:57434 个Fork数…

仿生学习:智能系统设计的灵感与实现

引言 仿生学习&#xff08;Biomimetic Learning&#xff09;是一种模拟自然界生物行为和特性的机器学习方法。其核心理念源于对自然系统的观察&#xff0c;借鉴生物行为和神经网络特性&#xff0c;使机器学习模型具备类似于生物的适应性、灵活性和进化能力。近年来&#xff0c…

python自动化测试——unittest框架

前言 参考文献 自动化测试——unittest框架_加上unittest测试框架,数据驱动,数据断言等内容。-CSDN博客

更新C语言题目

1.以下程序输出结果是() int main() {int a 1, b 2, c 2, t;while (a < b < c) {t a;a b;b t;c--;}printf("%d %d %d", a, b, c); } 解析:a1 b2 c2 a<b 成立 ,等于一个真值1 1<2 执行循环体 t被赋值为1 a被赋值2 b赋值1 c-- c变成1 a<b 不成立…

Oracle 时间计算

Oracle中的日期差、时间差&#xff0c;话不多说直接上例子。 --时间差-年 select floor(to_number(sysdate - to_date(2023-09-29 15:55:03, yyyy-mm-dd hh24:mi:ss))/365) as spanYears from dual; --时间差-月 select ceil(MONTHS_BETWEEN(sysdate,to_date(2023-09-29 15:55…

尚硅谷rabbitmq 2024 第18-21节 消息可靠性答疑一

publisher-confirm-type:CORRELATED#交换机的确认publisher-returns:true #队列的确认 这两个是干嘛的&#xff0c;有什么区别 这两个参数都是用于RabbitMQ消息发布确认机制的&#xff0c;但它们的作用和使用场景有所不同。 1. **publisher-confirm-type: CORRELATED#交换机的…