黑马头条(10-1开始学习)
文章目录
- 开始
- 序列化
- 将对象与字符串相加(例如 `对象 + ""`)和序列化对象(如 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;}
-
检验登录状态
- 前端传送过来token或者是session_id.
- 后端有一个地方保存信息,判断是否有为null。(是否登录)
- 有登录信息不为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
对,来保存该用户的相关信息。
具体说明:
-
每个用户都有独立的
Session
:- 当用户访问服务器时,服务器会为该用户创建一个唯一的
Session
,并通过Session ID
来标识用户的这个会话。这个Session ID
通常会通过浏览器的Cookie
进行存储,每次请求时自动发送给服务器。
- 当用户访问服务器时,服务器会为该用户创建一个唯一的
-
每个
Session
可以存储多个key-value
对:Session
就像是一个存储容器,每个用户都有一个独立的容器,你可以通过session.setAttribute(key, value)
往里面放入多个数据。- 例如,存储用户登录信息时可以设置:
session.setAttribute("user", userObject); // 存储用户信息对象 session.setAttribute("phone", phoneNumber); // 存储用户手机号 session.setAttribute("code", verificationCode); // 存储验证码
-
访问
Session
中的数据:- 你可以通过
session.getAttribute(key)
来获取存储在Session
中的数据。例如:User user = (User) session.getAttribute("user"); // 获取用户信息 String phone = (String) session.getAttribute("phone"); // 获取手机号
- 你可以通过
-
每个用户的
Session
数据互相隔离:- 由于每个用户都有自己独立的
Session
对象,因此用户 A 的Session
和用户 B 的Session
是完全隔离的,互不影响。用户 A 的Session
中的数据不会影响到用户 B。
- 由于每个用户都有自己独立的
-
Session
的作用范围:Session
在用户与服务器的会话期间有效,如果会话结束(例如用户关闭浏览器、Session
超时等),服务器可能会销毁该Session
,此时存储在Session
中的数据也会失效。
小结:
- 每个用户都会有独立的
Session
对象。 - 你可以在
Session
中存储多个key-value
数据对。 - 每个用户的
Session
数据是相互独立、隔离的,不会发生冲突或覆盖。
这样就可以在不混淆不同用户数据的情况下,轻松实现如登录状态、购物车、验证码等信息的存储和管理。
什么时候会生成新的 Session ID:
- 首次访问:用户第一次访问时,服务器会生成新的 Session ID。
- 会话过期:如果用户的会话(Session)过期了,服务器会删除旧的 Session 对象,用户再次访问时会生成一个新的 Session ID。
- 每次调用 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;}
-
在事务操作外面加上锁。
-
事务的实现方式
- 事务管理 只有方法在代理对象才可以进行事务管理。
- 自己增加的普通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
- 后面的进程有阻塞式和非阻塞方式。
改进思想
- 之前使用的是jvm的字符串常量池,锁是悲观锁的sylizattion关键字,锁放在常量池。
- 现在使用的是redis分布式锁,因为所有服务实例都可以访问到。只要建设和删除一个锁。