Redis学习总结
Redis学习总结
文章目录
- Redis学习总结
- Radis基本介绍
- docker的安装
- 基本数据结构
- 通用命令
- 字符型
- key的层次结构
- Hash类型
- List
- set
- sortedset集合
- redis的java客户端
- jedis的使用
- jedis连接池的配置
- SpringDataRedis
- 自定义redistemplate的序列化与反序列化方式
- stringtemplate的使用
- redis实战开发
- 短信登录
- 缓存
- 什么是缓存
- 缓存更新策略
- 缓存穿透的解决方案
- 缓存雪崩的问题
- 缓存击穿
- 优惠券秒杀
- 超卖问题
- 一人一单的问题
- 分布式锁
- Redisson操作
Radis基本介绍
- Radis是非关系型数据库,常被用作缓存使用。
docker的安装
docker安装redis
基本数据结构
通用命令
字符型
key的层次结构
Hash类型
List
set
- 案例练习
sortedset集合
- 常见命令
- zrank默认是升序,其他的也是如此,如果想要降序在z后面添加rev
- zrevrank是降序
- 案例
redis的java客户端
jedis的使用
- 引入依赖
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.6.1</version>
</dependency>
- 建立连接
// 创建jedis对象jedis = new Jedis("192.168.253.129",6379);
// 输入连接密码jedis.auth("123456");
// 选择数据库jedis.select(0);
- 使用jedis的api,提供的api函数与redis客户端命令一致
// 插入字符串jedis.set("name","wangwu");System.out.println("name="+jedis.get("name"));
- 关闭连接
@AfterEachpublic void after(){if (jedis!=null){jedis.close();}}
- 完整代码
package com.example.dockerfile;import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;@SpringBootTest
class DockerfileApplicationTests {private Jedis jedis;@Testvoid contextLoads() {
// 创建jedis对象jedis = new Jedis("192.168.253.129",6379);
// 输入连接密码jedis.auth("123456");
// 选择数据库jedis.select(0);
// 插入字符串jedis.set("name","wangwu");System.out.println("name="+jedis.get("name"));}@AfterEachpublic void after(){if (jedis!=null){jedis.close();}}}
jedis连接池的配置
public class JedisPoolFactory {private static final JedisPool jdedisppool ;static {JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 设置最大连接数jedisPoolConfig.setMaxTotal(8);
//设置最大空闲连接数jedisPoolConfig.setMaxIdle(8);
// 设置最小空闲连接数jedisPoolConfig.setMinIdle(0);
// 如果长时间空闲,连接池中的对象会被清理
// 设置等待时间
// Duration duration = new Duration(1000);jedisPoolConfig.setMaxWaitMillis(1000);jdedisppool=new JedisPool(jedisPoolConfig,"192.168.253.129",6379,1000,"123456");}// 获取资源public static Jedis getjedis(){return jdedisppool.getResource();}}
SpringDataRedis
- 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
<!-- <version>2.4.0</version>--></dependency><!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
<!-- <version>2.7.0</version>--></dependency>
- 配置文件
spring:redis:host: 192.168.253.129port: 6379password: 123456lettuce:pool:max-active: 8min-idle: 0max-idle: 8max-wait: 1000
- 基本使用
@SpringBootTest
public class SpringRedisTest {@Resourceprivate RedisTemplate redisTemplate;@Testpublic void test01(){redisTemplate.opsForValue().set("name","lmx");System.out.println(redisTemplate.opsForValue().get("name"));}
}
自定义redistemplate的序列化与反序列化方式
- 如果不设置序列化方式,使用原生的redistemplate添加的对象,无法在控制台上获取到,自动实现java对象的序列化与反序列化
@Configuration
public class RedisConfigure {@Beanpublic RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建redistemplate对象RedisTemplate<String, Object> stringObjectRedisTemplate = new RedisTemplate<>();// 设置连接工厂stringObjectRedisTemplate.setConnectionFactory(connectionFactory);
// 设置json序列化工具GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();// 设置key的序列化方式stringObjectRedisTemplate.setKeySerializer(RedisSerializer.string());stringObjectRedisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置值的序列化方式stringObjectRedisTemplate.setValueSerializer(jackson2JsonRedisSerializer);stringObjectRedisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);return stringObjectRedisTemplate;}
}
stringtemplate的使用
- stringredistemplete使用案例
@SpringBootTest
public class SpringRedisStringTest {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Testpublic void test01(){Person person = new Person("李满祥", 18);String s = JSONObject.toJSONString(person);stringRedisTemplate.opsForValue().set("person",s);String person1 = stringRedisTemplate.opsForValue().get("person");System.out.println(person1);Person person2 = JSONObject.parseObject(person1, Person.class);System.out.println(person2);}
}
redis实战开发
短信登录
- 基于session完成
- 流程图
- 向手机发送验证码功能
@Overridepublic Result SendPhone(String phone, HttpSession session) {// 校验手机验证码if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}//生成验证码String s = RandomUtil.randomNumbers(4);log.info("生成的验证码是:" + s);// 验证码保存在session中session.setAttribute(SavePattern.PHONECODE, s);return Result.ok();}
- 登录功能
@Overridepublic Result LoginService(LoginFormDTO loginForm, HttpSession session) {// 校验手机号if (RegexUtils.isPhoneInvalid(loginForm.getPhone())){return Result.fail("手机号格式错误");}
// 校验验证码Object sessioncode = session.getAttribute(SavePattern.PHONECODE);if (loginForm.getCode()==null || !loginForm.getCode().equals(sessioncode)){return Result.fail("验证码错误");}
// 数据库中查询用户User user = query().eq("phone", loginForm.getPhone()).one();// 如果没有该用户,插入数据库if (user==null){user=new User();user.setPhone(loginForm.getPhone());
// 随机生成昵称String s = RandomUtil.randomString(10);user.setNickName("user_"+s);save(user);}
// 用户信息保存到session中UserDTO userDTO = new UserDTO();userDTO.setId(user.getId());userDTO.setNickName(user.getNickName());session.setAttribute(SavePattern.LOGINUSER,userDTO);log.info("登录用户的信息已存入session中");return Result.ok();}
- 在拦截器中拦截获取登录状态请求
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// return HandlerInterceptor.super.preHandle(request, response, handler);HttpSession session = request.getSession();UserDTO attribute = (UserDTO) session.getAttribute(SavePattern.LOGINUSER);// 将用户的信息存入thradlocal中if (attribute==null){response.setStatus(401);return false;}
// 不放行;
// 身份不通过UserHolder.saveUser(attribute);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}
}
- 注册拦截器
package com.hmdp.config;import com.hmdp.controller.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.ArrayList;@Configuration
public class Webconfigure implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {ArrayList<String> patterns = new ArrayList<>();patterns.add("/user/code");patterns.add("/user/login");patterns.add("/user/logout");
// patterns.add("/user/me");patterns.add("/shop/**");patterns.add("/shop-type/**");patterns.add("/upload/**");registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(patterns);}
}
-
- 集群的session共享问题
- 集群的session共享问题
- 使用redis代替session,只要将项目中出现session的地方替换成redis即可,但又以下问题需要注意
- session的过期时间的30分钟,每次访问session都会重置它的过期时间,但是redis中无法自动更新,所以需要手动进行过期时间的更新,例如在前端请求登录状态的时候,后端进行redis的ttl更新操作
- 需要给前端返回在redis中存取值的key,再次选择使用phone存取验证码,后端生成的token存取用户的信息
缓存
什么是缓存
- 添加商户缓存
@Overridepublic Result queryByid(Long id) {String o = "shop:" + id;String s = stringRedisTemplate.opsForValue().get(o);if (s!=null){
// 缓存有商铺信息Shop shop = JSONObject.parseObject(s, Shop.class);return Result.ok(shop);}// 如果不存在,数据库中查询Shop shop = query().eq("id", id).one();if (shop==null){return Result.fail("无此商铺信息");}String shopstring = JSONObject.toJSONString(shop);stringRedisTemplate.opsForValue().set(o,shopstring);return Result.ok(shop);}
缓存更新策略
- 内存淘汰 超时剔除,主动更新
@Override@Transactionalpublic Result updateByid(Shop shop) {
// 先做校验if (shop == null && shop.getId() == 0L) {return Result.fail("商铺信息或商铺id不能为空");}
// 更新数据库操作updateByid(shop);// 删除缓存stringRedisTemplate.delete(pre+shop.getId());return Result.ok();}
缓存穿透的解决方案
@Overridepublic Result queryByid(Long id) {String o = pre + id;String s = stringRedisTemplate.opsForValue().get(o);// if (s!=null && ){
// return Result.fail("d店铺不存在");
// }if (s != null) {
// 解决缓存穿透的问题if ("".equals(s)){return Result.fail("店铺不存在");}
// 缓存有商铺信息Shop shop = JSONObject.parseObject(s, Shop.class);return Result.ok(shop);}// 如果不存在,数据库中查询Shop shop = query().eq("id", id).one();if (shop == null) {
// 将空对象写入缓存中,解决缓存穿透的问题stringRedisTemplate.opsForValue().set(o,"",2L,TimeUnit.MINUTES);// 有效时间是两分钟return Result.fail("无此商铺信息");}String shopstring = JSONObject.toJSONString(shop);stringRedisTemplate.opsForValue().set(o, shopstring);stringRedisTemplate.expire(o, 30L, TimeUnit.MINUTES);//设置过期时间为30 分钟return Result.ok(shop);}
缓存雪崩的问题
缓存击穿
- 基于互斥锁的方式
@Overridepublic Result queryByid(Long id) {// 使用缓存穿透的方法解决
// Shop shop=getshopWithCatchThrouw(id);
// 解决缓存击穿的问题,使用互斥锁String o = pre + id;String s = stringRedisTemplate.opsForValue().get(o);// if (s!=null && ){
// return Result.fail("d店铺不存在");
// }if (s != null) {
// 解决缓存穿透的问题if ("".equals(s)) {return Result.fail("店铺不存在");}
// 缓存有商铺信息Shop shop = JSONObject.parseObject(s, Shop.class);return Result.ok(shop);}Shop shop = null;try {boolean getlock = getlock();if (!getlock) {
// 进行等待Thread.sleep(50);
// 进行重试queryByid(id);}shop = query().eq("id", id).one();if (shop == null) {
// 将空对象写入缓存中,解决缓存穿透的问题stringRedisTemplate.opsForValue().set(o, "", 2L, TimeUnit.MINUTES);// 有效时间是两分钟return Result.fail("店铺不存在");}String shopstring = JSONObject.toJSONString(shop);stringRedisTemplate.opsForValue().set(o, shopstring);stringRedisTemplate.expire(o, 30L, TimeUnit.MINUTES);//设置过期时间为30 分钟} catch (Exception e) {throw new RuntimeException(e.getMessage());}finally {unlock();}return Result.ok(shop);}// 得到锁,使用redis的sentnx方法,如果redis中有key,则不会创建,返回falsepublic boolean getlock() {Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock:shop", "1", 10, TimeUnit.SECONDS);return aBoolean;}// 释放锁public void unlock() {stringRedisTemplate.delete("lock:shop");}
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
// 解决缓存击穿的问题,使用逻辑过期的方法private Shop getshopWithLogicLock(Long id) {
// 查询缓存中是否有数据
// 如果有数据,则判断是否过期,如果没有过期,直接返回
// 如果已过期,进行缓存重建,返回旧数据String o = pre + id;String s = stringRedisTemplate.opsForValue().get(o);
// 如果未命中,返回空if (s == null) {return null;}RedisData redisData = JSONObject.parseObject(s, RedisData.class);LocalDateTime ecpiretime = redisData.getEcpiretime();//过期时间JSONObject data = (JSONObject) redisData.getData();Shop shop = JSON.toJavaObject(data, Shop.class);
// 过期时间在现在时间之前,说明过期if (ecpiretime.isBefore(LocalDateTime.now())) {
// 如果获取到锁进行缓存重建,如果没有则,将其他进程在进行缓存重建,直接放回旧数据boolean getlock = getlock();if (getlock) {executorService.submit(() -> {try {saveshopwithlogic(id, 2L);// 设置过期时间是2秒} catch (InterruptedException e) {throw new RuntimeException(e.getMessage());} finally {unlock();}});}}return shop;}
注:java中的线程池方法:
private final ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.submit(() -> {try {saveshopwithlogic(id, 2L);// 设置过期时间是2秒} catch (InterruptedException e) {throw new RuntimeException(e.getMessage());} finally {unlock();}});
优惠券秒杀
全局唯一id格式
@Component
public class RedisOneID {// 生成全局唯一id,生成的id 符号位+时间戳+序列号private final static long starttime = 1286064000L;@Resourceprivate StringRedisTemplate stringRedisTemplate;public long getLongId(String pre) {
// 获取当前的时间戳long l = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
// long l1 = LocalDateTime.of(2010, 10, 3, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);
// System.out.println(l1);//long time = l - starttime;time=time<<32; //时间戳向左移32=位,空出位置String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));Long increment = stringRedisTemplate.opsForValue().increment("ice" + pre + format);// 拼接的字符串long l1 = time | increment;return l1;}}
超卖问题
- boolean voucher_id = update().setSql(“stock=stock-1”).eq(“voucher_id”, voucherId).eq(“stock”,seckillVoucher.getStock())
- 通过在更新时判断此时的库存是否与开始查询到的库存一致,如果一致说明未被修改,可以更新
package com.hmdp.service.impl;import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisOneID;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** <p>* 秒杀优惠券表,与优惠券是一对一关系 服务实现类* </p>** @author 虎哥* @since 2022-01-04*/
@Service
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {@Resourceprivate VoucherOrderMapper voucherOrderMapper;@Resourceprivate RedisOneID redisOneID;@Overridepublic Result getseckill(Long voucherId) {
// 查询优惠券SeckillVoucher seckillVoucher = query().eq("voucher_id", voucherId).one();
// 判断时间是否开始boolean after = seckillVoucher.getBeginTime().isAfter(LocalDateTime.now());if (after) {return Result.fail("抢购时间未开始");}
// 查询库存是否够if (seckillVoucher.getStock() < 1) {return Result.fail("库存不足");}// 更新库存数据 ,判断向前查到的库存与是否发生改变boolean voucher_id = update().setSql("stock=stock-1").eq("voucher_id", voucherId).eq("stock",seckillVoucher.getStock()).update();if (!voucher_id) {return Result.fail("库存扣减失败");}// 插入订单数据VoucherOrder voucherOrder = new VoucherOrder();long order = redisOneID.getLongId("order");voucherOrder.setId(order);voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(UserHolder.getUser().getId());// 插入订单voucherOrderMapper.insert(voucherOrder);return Result.ok(order);}
}
一人一单的问题
- 使用添加悲观锁的方法解决
@Transactional
// 给该方法添加事务,事务的提交时机是在方法结束之后才提交,
// 所以需要将synchronized控制块添加当方法的外围,确保在锁中的代码事务提交之后在释放锁 - 如果不使用intern方法,那么每次synchronized 中的值就是新的string对象,通过intern方法可以获取到常量池中的唯一备份,确保不同线程的同一用户,synchronized中的值相同
- 使用@transactional注解进行事务管理时,在类内部调用方法时需要通过当前类的代理对象来获取,不能使用this对象直接调用
- 需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)注解,暴露代理类
-
// intern可以获取string常量池中的对象,确保synchronized中锁定的同一个用户synchronized (UserHolder.getUser().getId().toString().intern()) {ISeckillVoucherService o = (ISeckillVoucherService)AopContext.currentProxy();// @Transactional 对象的原理其实是通过aop代理实现的,// 所以在调用方法时需要获取带当前对象的代理对象return o.createorder(voucherId,seckillVoucher);}
~~~java
@Service
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {@Resourceprivate VoucherOrderMapper voucherOrderMapper;@Resourceprivate RedisOneID redisOneID;@Override@Transactionalpublic Result getseckill(Long voucherId) {// 查询优惠券SeckillVoucher seckillVoucher = query().eq("voucher_id", voucherId).one();
// 判断时间是否开始boolean after = seckillVoucher.getBeginTime().isAfter(LocalDateTime.now());if (after) {return Result.fail("抢购时间未开始");}
// 查询库存是否够if (seckillVoucher.getStock() < 1) {return Result.fail("库存不足");}// intern可以获取string常量池中的对象,确保synchronized中锁定的同一个用户synchronized (UserHolder.getUser().getId().toString().intern()) {ISeckillVoucherService o = (ISeckillVoucherService)AopContext.currentProxy();// @Transactional 对象的原理其实是通过aop代理实现的,// 所以在调用方法时需要获取带当前对象的代理对象return o.createorder(voucherId,seckillVoucher);}}@Transactional// 给该方法添加事务,事务的提交时机是在方法结束之后才提交,// 所以需要将synchronized控制块添加当方法的外围,确保在锁中的代码事务提交之后在释放锁public Result createorder(Long voucherId,SeckillVoucher seckillVoucher){LambdaQueryWrapper<VoucherOrder> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(VoucherOrder::getUserId, UserHolder.getUser().getId()).eq(VoucherOrder::getVoucherId, voucherId);List<VoucherOrder> voucherOrders = voucherOrderMapper.selectList(queryWrapper);if (voucherOrders == null) {return Result.fail("请勿重复抢购");}// 更新库存数据 ,判断向前查到的库存与是否发生改变boolean voucher_id = update().setSql("stock=stock-1").eq("voucher_id", voucherId).eq("stock", seckillVoucher.getStock()).update();if (!voucher_id) {return Result.fail("库存扣减失败");}// 插入订单数据VoucherOrder voucherOrder = new VoucherOrder();long order = redisOneID.getLongId("order");voucherOrder.setId(order);voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(UserHolder.getUser().getId());
// voucherOrder.setUserId(1011L);// 插入订单voucherOrderMapper.insert(voucherOrder);return Result.ok(order);}
}
分布式锁
- 分布式锁方案
- 基于Redis的分布式锁
- 其他的线程将不属于自己的锁释放了,造成并发执行,此时的解决方案
- 在释放锁时通过判断是否是当前线程的标识
- 解决方案流程图
public class SimpleRedisLock implements Ilock {private String name; // 给哪个对象加锁,的名字private StringRedisTemplate redisTemplate;private final String KEY_PRE = "lock:";private final String ID_PRE= UUID.randomUUID().toString().replace("-","")+"-";public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {this.name = name;this.redisTemplate = redisTemplate;}/*** @Pgrm 尝试获取锁* */@Overridepublic boolean trylocak(Long timeoustSecond) {long id = Thread.currentThread().getId(); // 获取当前线程的id值,充当sentnx的vlaueString ids=ID_PRE+id;Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(KEY_PRE + name, ids + "", timeoustSecond, TimeUnit.SECONDS);// 在进行自动拆箱的过程中,可能会返回null对象,导致发生异常return Boolean.TRUE.equals(aBoolean);}@Overridepublic void unlock() {
// 判断当前的id值,是否与缓存内存取的id值相等long id = Thread.currentThread().getId();String ids= ID_PRE+id;String s = redisTemplate.opsForValue().get(KEY_PRE + name);if (ids.equals(s)){redisTemplate.delete(KEY_PRE + name);}}
}
- 存在一种极端情况,当判断锁标识是否相等后,这时,线程发生阻塞(由于垃圾回收的原因),这时在阻塞结束后,因为已经判断过,所以不再判断直接删除,安全隐患发生的时机在于判断之后,未删除之前,所以需要将该操作变为原子操作
if (ids.equals(s)){redisTemplate.delete(KEY_PRE + name);}
Redisson操作
- 引入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.8</version>
</dependency>