文章目录
开始
别人的笔记: 入门笔记
序列化
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 {
@Bean
public 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名称,目的是为了查询时实现自动反序列化。这会带来额外的内存开销。
@Test
void 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 等),自定义序列化过程以满足特定需求。
总结
对象 + ""
适合快速查看对象的状态,主要用于调试;而序列化对象则是将对象转换为标准格式以便于存储或传输,适合数据交互场景。选择使用哪个取决于具体的需求和上下文。
@Test
void 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) {
//从请求头获取token
String 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;
@Data
public 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 {
@Override
public 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中。
}
@Override
public 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;
@Override
public 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对应数据。
实际操作
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public 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
@Transactional
public void updateShop(Shop shop) {
boolean b = this.updateById(shop);
stringRedisTemplate.delete(RedisConstant.Shop_store + shop.getId());
}
- 要加上事务。
缓存穿透
- 先查找布隆过滤器
@Override
public 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 {
@Autowired
private 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
@Transactional
public 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);
// 用户id
Long 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 {
@Autowired
private VoucherMapper voucherMapper;
@Transactional
public 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
@Transactional
public 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);
// 用户id
Long 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
@Transactional
public 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);
// 用户id
voucherOrder.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);
// 用户id
voucherOrder.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();
}
@Transactional
public 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);
// 用户id
voucherOrder.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 {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorder redisIdWorder;
@Autowired
private IVoucherOrderService IVoucherOrderServicelmpl;
@Override
public 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();
}
}
@Transactional
public 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);
// 用户id
voucherOrder.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分布式锁,因为所有服务实例都可以访问到。只要建设和删除一个锁。
redisson分布式锁。
业务线程需要多次获取同一把锁的情况通常出现在一些复杂的业务逻辑中,尤其是在以下场景中:
1. 递归操作或深度嵌套调用
如果一个线程在执行递归操作或嵌套调用中,需要在不同的层次上多次获取同一把锁。例如,树形结构的数据操作或递归算法时,线程可能在进入和退出递归时需要多次获得同一把锁,以确保线程安全。
示例:
public void recursiveFunction() {
synchronized(lock) {
// 处理一些业务逻辑
if (条件满足) {
recursiveFunction(); // 递归调用
}
}
}
2. 重复进入同步块
业务逻辑可能在不同的代码段重复进入同一个同步块。例如,方法 A 和方法 B 都需要访问同一个共享资源,而 A 调用 B 时需要多次获得同一把锁。
示例:
public void methodA() {
synchronized(lock) {
// 执行一些业务操作
methodB(); // 调用另外一个需要相同锁的操作
}
}
public void methodB() {
synchronized(lock) {
// 执行另一个需要同一锁的操作
}
}
3. 同一线程在事务中需要获取锁
在一些长事务操作中,可能需要多次操作共享资源,每次操作时都需要加锁保证数据一致性。例如,在处理批量订单或分段处理长业务逻辑时,业务线程会多次获取同一把锁。
示例:
@Transactional
public void processOrder(Long orderId) {
synchronized(lock) {
// 检查库存
checkInventory();
}
synchronized(lock) {
// 更新库存
updateInventory();
}
}
4. 锁重入 (Reentrant Locking)
Java 的 ReentrantLock
或 synchronized
是可重入的,这意味着同一个线程可以多次获取同一把锁,而不会发生死锁。这对于需要在业务流程中多次进入同步代码块的场景非常重要。
示例:
public void outerMethod() {
synchronized(lock) {
innerMethod(); // 内部方法也会获得同一把锁
}
}
public void innerMethod() {
synchronized(lock) {
// 执行某些操作
}
}
5. 分布式锁中的重入操作
在分布式环境中,业务线程可能在多个步骤中需要获取同一分布式锁,以确保某些操作的全局唯一性或避免竞态条件。例如,在库存扣减的场景中,可能会多次尝试获取锁。
示例:
// 分布式锁的获取
if (redisLock.tryLock()) {
try {
// 操作1
processStep1();
// 操作2
processStep2();
} finally {
redisLock.unlock();
}
}
这种情况下,业务需要确保锁的可重入性和安全性,避免重复锁定或死锁。
总结
业务线程多次获取同一把锁的原因通常是为了确保数据在复杂逻辑(如递归、嵌套调用、事务等)中的一致性和线程安全性。在 Java 中,synchronized
和 ReentrantLock
机制能够支持这种多次加锁的场景。
快速入门
- 还是三步走:创建锁 获得锁 释放锁。
setnx与set
SETNX 是 Redis 的一个命令,用于设置键值对,但只有在键不存在时才设置。它是一个原子操作,意味着它会检查键是否存在,如果不存在,则设置键值对。如果键已经存在,操作将不会执行任何操作。
SETNX 命令的基本用法如下:
SETNX key value
key:要设置的键。
value:要设置的值。
命令的返回值是:
1:如果键被成功设置(即键之前不存在)。
0:如果键已经存在,因此没有设置任何内容。
在 Redis 中,如果你尝试设置一个已经存在的键(key)的值(value),旧的值会被新的值覆盖。
- setnx就是一个if(lock)
// 检查锁的是否存在。
if(lock){
// 没有获取到锁。
}else{
// 设置锁成功
// 拿到锁。
}
。。。
// 超过时间 解锁
// 任务完成解锁
- getlock是在创建一个对象, tryLock()查看一下有没有这个对象。
- 如果锁不存在,就创建锁之后获得锁。
- 如果锁存在,就获得 获取不到 和 在value+1;
主从一致
- 主从同步
- 锁的数据没有同步过去,缺少了新增加的数据。锁失效问题。
- 去除主从关系。 写的时候,全部写到所有Redis中。就算挂了一个,其他还有。
package com.hmdp.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean(name = "redissonClient")
public RedissonClient redissonClient() {
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// .setPassword("");
// 创建RedissonClient对象
return Redisson.create(config);
}
@Bean(name = "redissonClient1")
public RedissonClient redissonClient1() {
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6380").setDatabase(8);
;
// .setPassword("");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
package com.hmdp;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.hmdp.service.impl.ShopServiceImpl;
import java.util.concurrent.*;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisIdWorder;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
class HmDianPingApplicationTests {
@Resource
private ShopServiceImpl shopService;
@Resource
private RedisIdWorder redisIdWorder;
@Test
public void testSaveShop() throws JsonProcessingException {
shopService.saveShop2Redis(2L, 60 * 60 * 24 * 2);
}
private ExecutorService es = Executors.newFixedThreadPool(200);
@Test
public void test2() {
long id = redisIdWorder.netxId("order");
System.out.println(id);
}
@Resource
@Qualifier("redissonClient")
private RedissonClient redisClient1;
@Resource
@Qualifier("redissonClient1")
private RedissonClient redisClient;
RLock lock;
@BeforeEach
void setUp() {
RLock oreder = redisClient.getLock("oreder");
RLock oreder1 = redisClient1.getLock("oreder");
lock = redisClient.getMultiLock(oreder1, oreder);
}
@Test
public void test1() {
Runnable task = () -> {
try {
for (int i = 0; i < 100; i++) {
long id = redisIdWorder.netxId("order");
System.out.println(id);
}
} catch (Exception e) {
e.printStackTrace(); // 捕获异常并打印
}
};
for (int i = 0; i < 300; i++) {
es.submit(task);
}
try {
// 等待线程池中的任务全部执行完毕,设置超时时间为 60 秒
if (!es.awaitTermination(60, TimeUnit.SECONDS)) {
es.shutdownNow(); // 如果60秒后任务还未完成,强制关闭线程池
}
} catch (InterruptedException e) {
es.shutdownNow(); // 捕获中断异常并强制关闭
Thread.currentThread().interrupt(); // 重新设置线程的中断状态
}
}
@Test
void method1() throws InterruptedException {
boolean isLock = lock.tryLock(1L,60, TimeUnit.SECONDS);
if (!isLock) {
log.error("获取锁失败,1");
return;
}
try {
log.info("获取锁成功,1");
method2(); // 这里不需要再次获取锁,因为 lock 对象是可重入的
} finally {
log.info("释放锁,1");
lock.unlock();
}
}
void method2() {
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败,2");
return;
}
try {
log.info("获取锁成功,2");
// 执行业务逻辑
} finally {
log.info("释放锁,2");
lock.unlock();
}
}
}
-
在下载安装改配置一个redis。
-
获得锁需要拿到所有节点的锁,才算成功。一个失效了,发生了数据不一致,所以获得不到锁。(锁就是redis里面的key)
-
有主从架构或者是连锁架构,
秒杀优化
jmter变量
- 这是一个变量使用。
RLock lock = redissonClient.getLock("lock:order:" + useId);
boolean b = lock.tryLock(1, 10, TimeUnit.SECONDS);
。。。
lock.unlock();
- 这是redisson悲观锁 ,后面线程在排队。
性能分析
- 一个请求要2s.
资格放在redis上。
思路:使用lua判断有没有资格领取,有的话生成订单id。
- 数据库并发能力较弱。
- 我们的请求方法有越多的数据库请求时间越长。
- 用户请求来了就先查看他有没有资格(判断时间和库存还有一人一单校验);如果可以的话进行,减库存。
- 把资格判断放在redis。库存和一人一单。
set集合里面放同一个id多次是不行的,所以用来判断是否领取。
为什么放在redis会效率更高呢?我在tomcat代码中遇到直接return不可以吗?
我的想法
- 数据库的优惠券还有一人一单的数据库请求 (两个请求) 直接请求一次放在redis就可以了。在tomcat就需要每一次都要请求。
- 从检查库存开始用redisson包起来。
- 资格判断有库存(string)和一人一单(set)。
local voucherId = ARGV[1]
local userId = ARGV[2]
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId
-- 存放库存
-- 存放一人一单
if (tonumber(redis.call("get", stockKey)) <= 0) then
-- 库存不足
return 1
end
-- 判断用户是否已经购买过
if (redis.call("SISMEMBER", orderKey, userId) == 1) then
-- 已经领取过
return 2
end
-- 扣库存
redis.call("INCRBY", stockKey, -1)
-- 下单(保存用户)
redis.call("sadd", orderKey, userId)
return 0
- 可以看出使用redis的配置后,提高了速度。
阻塞队列是什么?
- 队列满的话就会,就会让生产者等到队列有空间才进来,而不是扔掉生产者的数据。队列空的话,消费者也会等待数据,而不是放弃。
- 防止数据丢失:通过阻塞生产者,可以确保即使在高负载情况下,数据也不会因为队列溢出而被丢弃。
- 内存限制
- jvm断电的话消息队列的东西就丢失了。
消息队列
- 消费者是一个处理消息,之后要返回确认消息,才会在队列里面去除消息。
基于stream的队列。
+
- 一条读取的命令只能读取一条,如果来了6个消息,最后一个消息的前面5个读不到。所以读最新消息是最后一个。
消费者组
基于mq的消息队列
package com.hmdp.config.RabbitMQ.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MQ消息转换器,
*/
@Configuration
public class MQConfig {
@Bean
public Jackson2JsonMessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
package com.hmdp.config.RabbitMQ.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class MQProducerAckConfig implements RabbitTemplate.ReturnCallback,RabbitTemplate.ConfirmCallback {
// 我们发送消息使用的是 private RabbitTemplate rabbitTemplate; 对象
// 如果不做设置的话 当前的rabbitTemplate 与当前的配置类没有任何关系!
@Autowired
private RabbitTemplate rabbitTemplate;
// 设置 表示修饰一个非静态的void方法,在服务器加载Servlet的时候运行。并且只执行一次!
@PostConstruct
public void init(){
rabbitTemplate.setReturnCallback(this);
rabbitTemplate.setConfirmCallback(this);
}
/**
* 表示消息是否正确发送到了交换机上
* @param correlationData 消息的载体
* @param ack 判断是否发送到交换机上
* @param cause 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息发送成功!");
}else {
System.out.println("消息发送失败!"+cause);
}
}
/**
* 消息如果没有正确发送到队列中,则会走这个方法!如果消息被正常处理,则这个方法不会走!
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息主体: " + new String(message.getBody()));
System.out.println("应答码: " + replyCode);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交换器 exchange : " + exchange);
System.out.println("消息使用的路由键 routing : " + routingKey);
}
}
package com.hmdp.config.RabbitMQ.service;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
//发送消息
@Service
public class RabbirService {
@Autowired
private RabbitTemplate rabbitTemlate;
/**
*
* @param exchange 交换机
* @param routingKey 队列名字key
* @param message 消息也就是消息实体。
* @return
*/
public boolean sendMessage(String exchange,String routingKey,Object message){
rabbitTemlate.convertAndSend(exchange,routingKey,message);
return true;
}
}
rabbitService.sendMessage(MqConst.EXCHANGE_ORDER_DIRECT, MqConst.ROUTING_ORDER_KEY, voucherOrder);
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MqConst.QUEUE_ORDER_KEY, durable = "true"),
exchange = @Exchange(value = MqConst.EXCHANGE_ORDER_DIRECT),
key = {MqConst.ROUTING_ORDER_KEY}
))
private void handleVoucherOrder(VoucherOrder voucherOrder, Message message, Channel channel) throws InterruptedException, IOException {
探店笔记
- 店铺和用户
- 点赞数量和评论数量。
- 上传图片组件是一个独立组件。上传多个图片就是放到数组里面,但是在数据库就是只放在一个字段里面使用,分割。
@PostMapping("blog")
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
- 上传图片到这个接口,参数是MultipartFile
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
图片转移到另一个文件夹。
StringRedisTeamplate
使用StringRedisTeamplate 可以解决String和字符串编码问题。但是对象转码还是需要做的
public 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;
}
-
利用redistemplate默认需要使用jdk编码器。
-
点赞之后新的请求数据会返回替换指定旧数据。
点赞排行榜
- 点赞是需要是唯一的。
- 点赞排行榜是一个需要唯一和排行榜的。
关注
- 用户与用户表之间是多对多关系。关注就是增加多对多表的记录。取关就删除。
共同关注
- 逻辑:就是自己关注了谁;对方关注了谁的交集。
- set是一个集合,可以进行并集和交集操作。
- 就是我:【a,b,c,d】 他:【b,c,d】 交集。
关注推送
- 推模式:把发件箱的东西推给收件箱。粉丝少。
- 拉模式:收件箱拉消息过来。大V。
- 推拉模式就是看发送者的粉丝数量。
推模式和拉模式。
- 保存文章之后进行推送大收件箱。
- 收件箱根据时间戳排列。redis。
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 1.获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2.保存探店博文
blogService.save(blog);
stringRedisTemplate.opsForZSet().add(RedisConstant.Blog_liked + blog.getId().toString(), "0", System.currentTimeMillis());
// 3.查询所有的粉丝 根据数据库查出。
List<Follow> followUserId = followservice.query().eq("follow_user_id", user.getId()).list();
for (Follow follow : followUserId) {
// 4.推送给文章id给所有粉丝。
long userId = follow.getUserId();
String key = "free:" + userId;
follow.getFollowUserId();
stringRedisTemplate.opsForZSet().add(key, blog.getUserId().toString(), System.currentTimeMillis());
}
// 5. 返回id
return Result.ok(blog.getId());
}
- 收件箱是follow:10。 里面是文章id。
- 查询所有粉丝。
- 新的消息是排在最前面的。
- 消息在变,使用zset的角标方式。
- 关注指针。
- 第一次的limit 是0 接下来是1.
- 这个是不断查找老的消息,如果有新的消息的话,可以从头开始在此寻找。
- 需要更改max和offset
- 找出对应数据。之后找到最小的分数;还有最小的分数个数。
博客列表
一个博客列表需要查询是否被点赞。和用户信息。
- 点赞 检查文章的点赞redis,存到数据库。
geo
- 每个geo集合存放不同类型的店铺。
- 一个geo项目 存放 value和坐标。
List<Shop> list = shopService.list();
Map<Long, List<Shop>> collect = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
- 例如:根据性别进行分开每个人到不同容器。collect()
- 这个方法可以用在好评评论或者是地理多中选择方案。所以参数加上required=false。
@Test
public void test4() {
List<Shop> list = shopService.list();
Map<Long, List<Shop>> collect = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
// 根据type_id建立集合,
Set<Map.Entry<Long, List<Shop>>> entries = collect.entrySet();
Iterator<Map.Entry<Long, List<Shop>>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<Long, List<Shop>> next = iterator.next();
// 获得key
Long key = next.getKey();
// 获得商品list。
List<Shop> value = next.getValue();
// geoadd type_id 经度 纬度 店铺id 经度 纬度 店铺id
List array = new ArrayList(value.size());
for (Shop shop : value) {
// stringRedisTemplate.opsForGeo().add("shop_type:" + key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
array.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY())));
}
stringRedisTemplate.opsForGeo().add("shop_geo_type:" + key, array);
}
}
- GEORADIUS places 116.397904 39.909005 10 km WITHCOORD WITHDIST。
- 参数有:places集合。 10公里 中心坐标。
@Override
public List<Shop> queryShopByType(Integer typeId, Integer current, Double x, Double y) {
// 1. 判断是否是根据坐标查询。
if (x != null && y != null) {
// 根据类型分页查询
// 定义中心点
Point center = new Point(x, y);
// 定义查询范围,单位是公里(Metrics.KILOMETERS)
Circle circle = new Circle(center, new Distance(25, Metrics.KILOMETERS));
long from = SystemConstants.DEFAULT_PAGE_SIZE * (current - 1);
long to = SystemConstants.DEFAULT_PAGE_SIZE;
// 加上距离显示。
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance();
// 执行 GEORADIUS 命令
// 找出这个圆圈范围内的 所有集合坐标。
GeoResults<RedisGeoCommands.GeoLocation<String>> radius = stringRedisTemplate.opsForGeo()
.radius(RedisConstant.Shop_geo_type + typeId, circle, args);
if (radius == null || radius.getContent().isEmpty()) {
return Collections.emptyList();
}
// 存储所有符合条件的商店id。
// 还有map。
List<String> ids = new ArrayList<>();
Map<String, Double> distanceMap = new HashMap<>();
radius.getContent().stream()
.skip(from)
.limit(to)
.forEach(item -> {
String shopIdStr = item.getContent().getName();
ids.add(shopIdStr);
distanceMap.put(shopIdStr, item.getDistance().getValue());
});
// 如果 ids 为空,直接返回
if (ids.isEmpty()) {
return Collections.emptyList();
}
// 构造 SQL 查询,处理 `FIELD` 中的字符串格式
// 如果 `id` 是字符串类型,需要在 `FIELD` 中加上引号
String fieldInClause = ids.stream()
.map(id -> "'" + id + "'") // 为每个字符串添加引号
.collect(Collectors.joining(","));
// 使用 MyBatis-Plus 的 in() 方法和 MySQL 的 Order By Field 进行排序
List<Shop> shops = query()
.in("id", ids) // 在数据库中根据 ID 查询
.last("ORDER BY FIELD(id, " + fieldInClause + ")") // 按顺序排序
.list();
// 将距离信息填充到结果中
shops.stream().forEach(shop -> {
shop.setDistance(distanceMap.get(shop.getId().toString()) * 1000);
});
return shops;
}
// 如果没有坐标,进行普通分页查询
Page<Shop> page = shopService.query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
return page.getRecords();
}
签到bitMap
- bitfield sign:1010:202410 get u18 0
bitfield key get u18(数量) 0(索引)
签到功能。
@Override
public void sign() {
Long userId = UserHolder.getUser().getId();
LocalDateTime now = LocalDateTime.now();
// setbit key offset value
// key sign_key+ userid+ yyyymm
String suffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = RedisConstant.SIGN_KEY_PRE + userId + suffix;
int dayOfMonth = now.getDayOfMonth();
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
}
/**
* 要从今天开始计算,统计连续签到天数。
*
* @return
*/
@Override
public int signCount() {
Long userId = UserHolder.getUser().getId();
LocalDateTime now = LocalDateTime.now();
int dayOfMonth = now.getDayOfMonth();
String suffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = RedisConstant.SIGN_KEY_PRE + userId + suffix;
// key get type offset.
List<Long> signRetult = stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
if (signRetult == null || signRetult.size() == 0) {
return 0;
}
//u18 请求出来的是十进制数据,但是我们用与运算或者是右移运算符。就变成二进制运算,
Long num = signRetult.get(0);
int count = 0;
while (true) {
// 获得二进制位数的位数值 使用与运算比较好。
if ((num & 1) == 0) {
break;
} else {
count++;
}
num >>>= 1;
}
return count;
}
@Test
public void test4() {
List<Shop> list = shopService.list();
Map<Long, List<Shop>> collect = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
// 根据type_id建立集合,
Set<Map.Entry<Long, List<Shop>>> entries = collect.entrySet();
Iterator<Map.Entry<Long, List<Shop>>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<Long, List<Shop>> next = iterator.next();
// 获得key
Long key = next.getKey();
// 获得商品list。
List<Shop> value = next.getValue();
// geoadd type_id 经度 纬度 店铺id 经度 纬度 店铺id
List array = new ArrayList(value.size());
for (Shop shop : value) {
// stringRedisTemplate.opsForGeo().add("shop_type:" + key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
array.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY())));
}
stringRedisTemplate.opsForGeo().add("shop_geo_type:" + key, array);
}
}
@Test
public void testHyperLogLog1() {
Long count = stringRedisTemplate.opsForHyperLogLog().size("visitor:userId:2");
System.out.println(count);
}