1.环境配置IDEA连接远程redis:
ubantu22下载docker,下载redis:
一、更新系统软件包索引
二、安装docker
1 sudo apt install docker.io
三:拉取redis:
IDEA连接远程国外服务器的docker:
1.直接连接是无法连成功的!因为我购买的vps是国外的!被墙了的!我们需要先配置IDEA的http代理!这里我使用的是clash下载的代理: 2.打开设置查看windown的系统代理:记住地址和端口: 3.IDEA配置http代理: 4.设置IDEAdocker连接: 同理也可以根据上面ssh连接海外服务器!如果上述方法无法连接成功就是节点的问题!
IDEA中docker连接远程redis容器:
redis默认只能本地访问远程访问需要额外的配置: 1.新建远程的ssh连接: 2.创建docker中redis容器的数据卷!分别映射redis容器中的data和配置信息:
1 2 3 mkdir -p /mydata/redis/data mkdir -p /mydata/redis/conf touch /mydata/redis/conf/redis.conf
3.在外部的redis.conf中的配置文件中书写如下内容:
1 2 3 4 5 6 7 # 下面的意思是允许所有的ip访问!也可以如下书写: # bind bind 0.0.0.0 # protected-mode no # 是否 appendonly yes
4.启动容器:
1 2 docker run --name redis -p 6379:6379 -v /mydata/redis/data:/data -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf -d redis redis-server /etc/redis/redis.conf
5.在IDEA中Docker中查看redis连接情况;
连接过程中遇到的异常:
发现上述无法启动容器:
错误描述:
1 2 3 4 *** FATAL CONFIG FILE ERROR (Redis 7.2.4) *** 2024-01-29T00:56:19.699109788Z Reading the configuration file, at line 3 2024-01-29T00:56:19.699113769Z >>> 'protected-mode no #1' 2024-01-29T00:56:19.699117853Z wrong number of arguments
1.删除无法启动的容器:
1 2 # docker rm <container_name_or_id> docker rm /redis
2.原因分析: 是protected-mode no #1后面的注释也被单当成配置信息参数的一部分了! 3.解决方法: 删除后面的注释,或者后面的注释换行写!
在docker中的redis容器中使用redis:
1.查看redis的密码:
1 2 3 4 # 连接远程服务器中的redis服务器 redis-cli # 查看密码的配置信息 CONFIG GET requirepass
入门:
Redis数据结构介绍:
String:
Key结构:
Hash:
List:
Set:
SortedSet:
Jedis连接:
https://www.cnblogs.com/dxflqm/p/17012394.html
1 2 3 4 5 6 <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.9.0</version> </dependency>
测试连接:
1 2 3 4 5 6 7 8 9 10 void test1() { // 1.构造一个 Jedis 对象,因为这里使用的默认端口 6379,所以不用配置端口 jedis = new Jedis("8.130.50.249", 6379); // 2.密码认证,如果没有设置密码就不要下面这行代码 // jedis.auth(""); // 3.测试是否连接成功 String ping = jedis.ping(); // 4.返回 pong 表示连接成功 System.out.println(ping); }
简单使用:
1 2 3 4 5 6 7 @Test void testString() { String result = jedis.set("name", "zhangsan"); System.out.println(result); String value = jedis.get("name"); System.out.println(value); }
连接池:
虽然 redis 服务端是单线程操作,但是在实际项目中,使用 Jedis 对象来操作 redis 时,每次操作都需要新建/关闭 TCP 连接,连接资源开销很高,同时 Jedis 对象的个数不受限制,在极端情况下可能会造成连接泄漏,同时 Jedis 存在多线程不安全的问题。 为什么说 Jedis 线程不安全,更加详细的原因可以访问这个地址https://www.cnblogs.com/gxyandwmm/p/13485226.html 所以我们需要将 Jedis 交给线程池来管理,使用 Jedis 对象时,从连接池获取 Jedis,使用完成之后,再还给连接池。 创建一个简单的连接池测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test void testPool () { JedisPool pool = new JedisPool ("8.130.50.249" , 6379 ); Jedis jedis = pool.getResource(); String ping = jedis.ping(); System.out.println(ping); jedis.close(); }
SpringDataRedis:
例子:
1 2 3 4 5 6 7 8 9 10 <!--common-pool--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--Jackson依赖--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
1 2 3 4 5 6 7 8 9 10 11 spring: redis: host: 8.130 .50 .249 port: 6379 password: "" lettuce: pool: max-active: 8 #最大连接 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100ms #连接等待时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SpringBootTest class RedisDemoApplicationTests { @Autowired private RedisTemplate<Object,Object> redisTemplate; @Test void testString () { redisTemplate.opsForValue().set("name" , "虎哥" ); Object name = redisTemplate.opsForValue().get("name" ); System.out.println("name = " + name); } }
1.上面的操作是往redis储存的数据是JDK序列化的!看起来很麻烦!(K和V都是序列化的) 2.private RedisTemplate<String,Object> redisTemplate;注意这里如果直接使用@Autowired 注入的话就是会报错的: 原因: @Autowired默认是按照类型注入的!我们可以去查看注入上面bean的源代码: RedisTemplate<String,Object>按照@Autowired 注入RedisTemplate<String,Object>它会按照类型去容器中查找该类型的bean!容器中只有RedisTemplate<Object,Object>这种类型的bean! 解决方法:
1 2 @Autowired private RedisTemplate<Object,Object> redisTemplate;
或者使用Resource注解:
1 2 3 @Resource private RedisTemplate<String,Object> redisTemplate;
为什么可以使用@Resource 注解呢?简单来说是该注解是按照名称来进行依赖注入的!具体可见:@Autowired 和@Resource注解的区别:
数据序列化器:
RedisTemplate可以接收任意Object作为值写入Redis: 只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的: 缺点:
我们可以自定义RedisTemplate的序列化方式,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate <>(); template.setConnectionFactory(connectionFactory); GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer (); template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); template.setValueSerializer(jsonRedisSerializer); template.setHashValueSerializer(jsonRedisSerializer); return template; } }
这里采用了JSON序列化来代替默认的JDK序列化方式。最终结果如图:
StringRedisTemplate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @SpringBootTest class RedisStringTests { @Autowired private StringRedisTemplate stringRedisTemplate; @Test void testString () { stringRedisTemplate.opsForValue().set("verify:phone:13600527634" , "124143" ); Object name = stringRedisTemplate.opsForValue().get("name" ); System.out.println("name = " + name); } private static final ObjectMapper mapper = new ObjectMapper (); @Test void testSaveUser () throws JsonProcessingException { User user = new User ("虎哥" , 21 ); String json = mapper.writeValueAsString(user); stringRedisTemplate.opsForValue().set("user:200" , json); String jsonUser = stringRedisTemplate.opsForValue().get("user:200" ); User user1 = mapper.readValue(jsonUser, User.class); System.out.println("user1 = " + user1); } @Test void testHash () { stringRedisTemplate.opsForHash().put("user:400" , "name" , "虎哥" ); stringRedisTemplate.opsForHash().put("user:400" , "age" , "21" ); Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400" ); System.out.println("entries = " + entries); } }
总结: RedisTemplate的两种序列化实践方案:
方案一:
自定义RedisTemplate
修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
方案二:
使用StringRedisTemplate
写入Redis时,手动把对象序列化为JSON
读取Redis时,手动把读取到的JSON反序列化为对象
实战:
短信登录:
主要是实现发送验证码和登录个功能!
当我们在前端点击发送验证码的时候!我们就在后端生成验证码并储存在redis中!传统方式是把验证码储存在session中!但是这里有个问题就是:一个用户的同一个请求会被分发到不同的tomcat服务器中!这样用户在A服务器中的session,当用户再次访问该请求的时候可能会访问B服务器!这样就无法获取用户在A服务器的session了!我们虽然可以通过复制session的方法解决这个问题,但是又增加了额外的操作!但是如果我们采取的是redis储存的方式的话,就不要复制session了
当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。
Redis代替session的业务流程:
设计key的结构
首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。
设计key的具体细节
所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code了 在设计这个key的时候,我们之前讲过需要满足两点 1、key要具有唯一性 2、key要方便携带 如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Override public Result login (LoginFormDTO loginForm, HttpSession session) { String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { return Result.fail("手机号格式错误!" ); } String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.equals(code)) { return Result.fail("验证码错误" ); } User user = query().eq("phone" , phone).one(); if (user == null ) { user = createUserWithPhone(phone); } String token = UUID.randomUUID().toString(true ); UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap <>(), CopyOptions.create() .setIgnoreNullValue(true ) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); String tokenKey = LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); return Result.ok(token); }
小知识点:
1 2 3 4 5 6 7 8 9 10 User user = query().eq("phone" , phone).one(); 这个方法是Iservice接口中的默认方法!只不过这个类实现Iservice接口,可以在该类中直接使用query这个 默认方法! java中一个类A实现了接口B!在A中该如何调用接口B中default 修饰的方法? public class A implements B { public void someMethod () { defaultMethod(); } }
导包后我们可以直接使用常量的名称而不必在常量名称前加上特定的包! 配置拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor ()) .excludePathPatterns( "/shop/**" , "/voucher/**" , "/shop-type/**" , "/upload/**" , "/blog/hot" , "/user/code" , "/user/login" ).order(1 ); registry.addInterceptor(new RefreshTokenInterceptor (stringRedisTemplate)).addPathPatterns("/**" ).order(0 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor (StringRedisTemplate stringRedisTemplate) { this .stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("authorization" ); if (StrUtil.isBlank(token)) { return true ; } String key = LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); if (userMap.isEmpty()) { return true ; } UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO (), false ); UserHolder.saveUser(userDTO); stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES); return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
1.实现了HandlerInterceptor就是一个拦截器! 2.一个请求过来tomcat服务器就会生成一个线程来处理这个请求!每个线程有个ThreadLocal用来储存和隔离不同线程中的一些数据! 3、afterCompletion是HandlerInterceptor接口中的一个方法。它在请求处理完成之后,即在响应已发送给客户端后执行。这个方法会在每次请求处理完成后都会执行。也就是每个请求处理完后准备返回response的时候就会擦除储存在ThreandLocal中的信息!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (UserHolder.getUser() == null ) { response.setStatus(401 ); return false ; } return true ; } }
上面的拦截器是对于要用户登录后才能处理的请求的拦截处理!
商户查询缓存:
添加redis缓存:
缓存更新策略:
由缓存调用者,在更新数据库的同时更新缓存:有两种方式:
1.先删除缓存,然后操作数据库
2.先操作数据库然后更新缓存
上面2种情况都会出现缓存和数据库不一样的情况: 只不过后面异常情况发生的可能性不是很高!因为操作数据库的操作时间比较长,且存在等待I/O的时间!一般情况下线程2中更新数据库的指令会在线程1写入缓存的后面!
缓存穿透:
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。 缓存穿透带来的问题:不怀好意的人会一直利用缓存穿透来搞坏数据库服务器! 常见的解决方案有两种:
缓存空对象: 布隆克隆: 解决方法1:
缓存雪崩:
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。 解决方案:
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿;
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了(key的TTL过期了),无数的请求访问会在瞬间给数据库带来巨大的冲击。 常见的解决方案有两种:
逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
案例:
sexnx命令: 释放锁获取锁代码:
知识点1:
泛型方法、Function类的函数化编程与调用:
泛型方法:java泛型 函数式编程:万字详解 | Java 函数式编程_Lambda_Phoenix_InfoQ写作社区
装箱空指针异常:
Java细节,自动封箱拆箱导致的NullPointerException问题_拆箱的 ‘group_meals’ 可能产生 ’java.lang.nullpointerexcep-CSDN博客 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController @RequestMapping("/shop") public class ShopController { @Resource public IShopService shopService; @GetMapping("/{id}") public Result queryShopById (@PathVariable("id") Long id) { return shopService.queryById(id); } @PutMapping public Result updateShop (@RequestBody Shop shop) { return shopService.update(shop); } }
1 2 3 4 5 6 public interface IShopService extends IService <Shop> { Result queryById (Long id) ; Result update (Shop shop) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Service public class ShopServiceImpl extends ServiceImpl <ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Resource private CacheClient cacheClient; @Override public Result queryById (Long id) { Shop shop = cacheClient .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this ::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES); if (shop == null ) { return Result.fail("店铺不存在!" ); } return Result.ok(shop); } @Override @Transactional public Result update (Shop shop) { Long id = shop.getId(); if (id == null ) { return Result.fail("店铺id不能为空" ); } updateById(shop); stringRedisTemplate.delete(CACHE_SHOP_KEY + id); return Result.ok(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 @Slf4j @Component public class CacheClient { private final StringRedisTemplate stringRedisTemplate; private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10 ); public CacheClient (StringRedisTemplate stringRedisTemplate) { this .stringRedisTemplate = stringRedisTemplate; } public void set (String key, Object value, Long time, TimeUnit unit) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit); } public void setWithLogicalExpire (String key, Object value, Long time, TimeUnit unit) { RedisData redisData = new RedisData (); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } public <R,ID> R queryWithPassThrough ( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } if (json != null ) { return null ; } R r = dbFallback.apply(id); if (r == null ) { stringRedisTemplate.opsForValue().set(key, "" , CACHE_NULL_TTL, TimeUnit.MINUTES); return null ; } this .set(key, r, time, unit); return r; } public <R, ID> R queryWithLogicalExpire ( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isBlank(json)) { return null ; } RedisData redisData = JSONUtil.toBean(json, RedisData.class); R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); LocalDateTime expireTime = redisData.getExpireTime(); if (expireTime.isAfter(LocalDateTime.now())) { return r; } String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); if (isLock){ CACHE_REBUILD_EXECUTOR.submit(() -> { try { R newR = dbFallback.apply(id); this .setWithLogicalExpire(key, newR, time, unit); } catch (Exception e) { throw new RuntimeException (e); }finally { unlock(lockKey); } }); } return r; } public <R, ID> R queryWithMutex ( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { String key = keyPrefix + id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)) { return JSONUtil.toBean(shopJson, type); } if (shopJson != null ) { return null ; } String lockKey = LOCK_SHOP_KEY + id; R r = null ; try { boolean isLock = tryLock(lockKey); if (!isLock) { Thread.sleep(50 ); return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit); } r = dbFallback.apply(id); if (r == null ) { stringRedisTemplate.opsForValue().set(key, "" , CACHE_NULL_TTL, TimeUnit.MINUTES); return null ; } this .set(key, r, time, unit); } catch (InterruptedException e) { throw new RuntimeException (e); }finally { unlock(lockKey); } return r; } private boolean tryLock (String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1" , 10 , TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unlock (String key) { stringRedisTemplate.delete(key); } }
秒杀:
如果用redis生成全局唯一ID安全性可能难以满足!因为redis中的默认自增是按照一定的长度自增的!