一、springboot整合redis

注:如果连接不上虚拟机中的redis,参考 https://blog.csdn.net/qq_38763540/article/details/108453154

1、创建springboot项目,导入web、lombok、redis、Junit依赖

2、在application.yml文件中配置redis参数

  1. spring:
  2. redis:
  3. host: 127.0.0.1
  4. port: 6379
  5. #password:
  6. jedis:
  7. pool:
  8. max-active: 8
  9. max-idle: 8
  10. max-wait: -1 #最大阻塞等待时间 -1代表一直等待
  11. min-idle: 1

3、创建redis配置类,添加以下代码

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
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.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Method;
@EnableCaching  //开启redis缓存,让通过SQL查询出来的数据可以存放到redis中
@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {
    /*
     * key的生成策略
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        // TODO Auto-generated method stub
        return new KeyGenerator() {
            /*
             * 参数1:要操作的目标对象
             * 参数2:要操作的方法
             * 参数3:执行方法时的参数
             */
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sBuilder = new StringBuilder();
                sBuilder.append(target.getClass().getName());
                sBuilder.append(".");
                sBuilder.append(method.getName());    //生成key
                for(Object object:params){
                    sBuilder.append(".");
                    sBuilder.append(object.toString());
                }
                return sBuilder.toString();
            }
        };
    }
    /**
     * redisTemplate相关配置
     * @param factory
     * @return
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template  = new RedisTemplate<>();
        //设置工厂
        template.setConnectionFactory(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        //(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        //创建对象映射
        ObjectMapper mapper = new ObjectMapper();
        //指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer
        //等会抛出异常
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        //字符串序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value采用jackson的方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value也采用Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

3、测试类中编写测试代码

@RunWith(SpringRunner.class)
@SpringBootTest
class RedisApplicationTests {
    //操作redis的对象  作用类似于statement
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    /**
     * 字符串操作
     */
    @Test
    void set(){
        //设置
        //opsForValue  操作字符串
        redisTemplate.opsForValue().set("age",10);
    }
    @Test
    void get(){
        //获取
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
    @Test
    void del(){
        //删除
        redisTemplate.delete("age");
    }
    @Test
    void expire(){
        //过期
        redisTemplate.expire("name",10l, TimeUnit.SECONDS);
    }
    @Test
    void saveObject(){
        //向redis中存储一个对象,会将对象转化成JSO数据
        Student student = new Student().setSid(1001).setName("zhangsan").setGender("男");
        redisTemplate.opsForValue().set("student",student);
    }
    //
    @Test
    void getObject(){
        Object student = redisTemplate.opsForValue().get("student");
        System.out.println(student.getClass());//String  Studend?
    }
    /**
     * list
     */
    @Test
    void listSet(){
        List<String> list = new ArrayList<>();
        list.add("zhangsan6");
        list.add("lisi6");
        list.add("wangwu6");
//        redisTemplate.opsForList().leftPush("students","wangwu1");
//        redisTemplate.opsForList().leftPush("students","wangwu2");
//        redisTemplate.opsForList().leftPush("students","wangwu3");
        //将整个list作为一个元素放入
        redisTemplate.opsForList().leftPushAll("students",list);
    }
    @Test
    void listGet(){
        System.out.println(redisTemplate.opsForList().index("students",0));
    }
    @Test
    void listDel(){
        redisTemplate.opsForList().leftPop("students");
    }
    /**
     * hash   一般用来存储一个对象/map
     */
    @Test
    void hashSet(){
        //set
        Student student = new Student().setSid(1001).setName("zhangsan").setGender("男");
        //
        //redisTemplate.opsForHash().put("class","name","一年二班");
        //
        redisTemplate.opsForHash().put("studend","sid",1001);
    }
}

4、导入mybatis、MySQL依赖

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

5、在application.yml中配置连接数据库参数

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/k15?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

6、创建entity

@Data
@Accessors(chain = true)
public class Teacher implements Serializable {
    private int id;
    private String pwd;
    private String name;
    private String serial;
    private String gender;
    private Date birthday;
    private Date workday;
    private int jid;
    private int pid;
    private String nat;
    private boolean status;
}

7、编写mapper

@Mapper
@Repository
public interface TeacherMapper {

    @Select("select * from teacher")
    public List<Teacher> all();

    @Select("select * from teacher where id = #{id}")
    public Teacher findById(int id);
}

8、编写service及实现类

public interface TeacherService {
    public List<Teacher> all();
    public Teacher findById(int id);
    public boolean delById(int id);
}
@Service
public class TeacherServiceImpl implements TeacherService {
    @Autowired
    private TeacherMapper teacherMapper;
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Cacheable("allTeacher")  //将当前方法得到的数据写入到redis中
    @Override
    public List<Teacher> all() {
        System.out.println("执行service方法....");
        return teacherMapper.all();
    }
    @Cacheable("findTeacherById")
    @Override
    public Teacher findById(int id) {
        return teacherMapper.findById(id);
    }

    //删除数据  删除findTeacherById中指定id的数据
    //allEntries 如果为true,则删除findTeacherById中所有数据
    //allEntries 如果为false,则删除不掉,所以有问题,需要提高性能时则自己手动处理redis较好
//    @CacheEvict(value = "findTeacherById",allEntries = true)
    @CacheEvict(value = "findTeacherById",allEntries = true)
    @Override
    public Boolean delById(int id) {
        return true;
    }
}

9、编写controller

@RestController
@RequestMapping("/teacher")
public class TeacherController {
    @Autowired
    TeacherService teacherService;

    @RequestMapping("/all")
    public List<Teacher> all(){
        System.out.println("执行controller方法......");
        return teacherService.all();
    }

    @RequestMapping("/find")
    public Teacher findById(int id){
        System.out.println("执行controller方法......");
        return teacherService.findById(id);
    }

    @RequestMapping("/del")
    public boolean delById(int id) {
        return teacherService.delById(id);
    }
}

10、运行程序进行测试,每一个URL在第一次请求时会去执行SQL查询数据,之后的请求就不会去查数据了,原因是第一次redis中没有数据,所以会到数据库中查询,然后将查询到的数据存放到了redis中,之后的请求数据都来自redis数据库

但是在实际开发过程中不建议使用注解操作,因为其效率不太好,建议通过redisTemplate手动进行管理,例如:

//@Cacheable("allTeacher")  //将当前方法得到的数据写入到redis中
@Override
public List<Teacher> all() {
   System.out.println("执行service方法....");
   //1.去redis中查询是否有数据
   String key = RedisUtil.generate(this,"all",null);
   if (redisTemplate.hasKey(key)){
        //2.有直接返回
        System.out.println("redis中有数据   直接返回");
        return (List<Teacher>)redisTemplate.opsForValue().get(key);
   }
   //3.调用mapper查询数据
   List<Teacher> teachers = teacherMapper.all();
   //4.将数据存放到redis中
   redisTemplate.opsForValue().set(key,teachers);
   return teachers;
}

RedisUtil代码:

public class RedisUtil {
    public static String generate(Object target, String methodName, Object... params) {
        StringBuilder sBuilder = new StringBuilder();
        sBuilder.append(target.getClass().getName());
        sBuilder.append(".");
        sBuilder.append(methodName);    //生成key
        if(params != null){
            for(Object object:params){
                sBuilder.append(".");
                sBuilder.append(object.toString());
            }
        }
        return sBuilder.toString();
    }
}

但是如果在每一个方法中都先去判断key是否存在,然后再进行其他操作,显得非常繁琐,利用AOP可以解决这个问题
导入aspectj的依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

编写通知类

@Component
@Aspect
public class RedisAdvice {
    //1.配置切点
    @Pointcut("execution(* com.woniuxy.redis.service.impl.TeacherServiceImpl(..))")
    public void pc(){}
    //2.环绕通知
    @Around("pc()")
    public void around(ProceedingJoinPoint joinPoint){
        //1.先获取key   对象、方法名、参数
        //2.判断key是否存在   存在获取返回
        //3.放行调用service方法
        //4.得到数据之后存到redis中
        //5.返回数据
    }
}

二、分布式锁

2.1 jmeter

JMeter下载安装:jmeter.docx

介绍**
Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。

JMeter的作用
1.能够对HTTP和FTP服务器进行压力和性能测试, 也可以对任何数据库进行同样的测试(通过JDBC)。
2.完全的可移植性和100% 纯java。
3.完全 Swing 和轻量组件支持(预编译的JAR使用 javax.swing.*)包。
4.完全多线程 框架允许通过多个线程并发取样和 通过单独的线程组对不同的功能同时取样。
5.精心的GUI设计允许快速操作和更精确的计时。
6.缓存和离线分析/回放测试结果。

JMeter的高可扩展性
1.可链接的取样器允许无限制的测试能力。
2.各种负载统计表和可链接的计时器可供选择。
3.数据分析和可视化插件提供了很好的可扩展性以及个性化。
4.具有提供动态输入到测试的功能(包括Javascript)。
5.支持脚本编程的取样器(在1.9.2及以上版本支持BeanShell)。
在设计阶段,JMeter能够充当HTTP PROXY(代理)来记录IE/NETSCAPE的HTTP请求,也可以记录apache等WebServer的log文件来重现HTTP流量。当这些HTTP客户端请求被记录以后,测试运行时可以方便的设置重复次数和并发度(线程数)来产生巨大的流量。JMeter还提供可视化组件以及报表工具把量服务器在不同压力下的性能展现出来。
相比其他HTTP测试工具,JMeter最主要的特点在于扩展性强。JMeter能够自动扫描其lib/ext子目录下.jar文件中的插件,并且将其装载到内存,让用户通过不同的菜单调用。

2.2 秒杀(部分功能)

案例:

压力测试:压测

环境搭建

1、创建springboot项目,导入web、lombok、redis依赖

2、在application.yml文件中配置连接redis参数

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    #password:
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1  #最大阻塞等待时间  -1代表一直等待
        min-idle: 1
server:
  port: 8081

3、添加redis配置类

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;

@EnableCaching  //开启redis缓存,让通过SQL查询出来的数据可以存放到redis中
@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {
    /*
     * key的生成策略
     */
    @Override
    public KeyGenerator keyGenerator() {
        // TODO Auto-generated method stub
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sBuilder = new StringBuilder();
                sBuilder.append(target.getClass().getName());
                sBuilder.append(".");
                sBuilder.append(method.getName());    //生成key
                for(Object object:params){
                    sBuilder.append(".");
                    sBuilder.append(object.toString());
                }
                return sBuilder.toString();
            }

            /*
             * 参数1:要操作的目标对象
             * 参数2:要操作的方法
             * 参数3:执行方法时的参数
             */
        };
    }
    /**
     * redisTemplate相关配置
     * @param factory
     * @return
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template  = new RedisTemplate<>();
        //设置工厂
        template.setConnectionFactory(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
//(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        //创建对象映射
        ObjectMapper mapper = new ObjectMapper();
        //指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        mapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
        //指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer
//等会抛出异常
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        //字符串序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value采用jackson的方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value也采用Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

4、编写controller

@RestController
public class KillController {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @RequestMapping("/kill")
    public boolean kill(){
        synchronized (KillController.class){
            //判断库存是否足够
            int store = (Integer) redisTemplate.opsForValue().get("store");
            //
            if (store>0){
                store--;
                System.out.println("秒删成功,还剩:"+store);
                //修改redis中库存
                redisTemplate.opsForValue().set("store",store);
                //
                return true;
            }
        }

        return false;
    }
}

5、运行项目 单独测试

6、将该项目复制一份,用idea打开,并修改application.yml中的端口号,运行程序

7、配置nginx.conf,将两个服务器的地址配置到该文件中

upstream blance{
     server 192.168.11.98:8081;
     server 192.168.11.98:8082;
}

此处的ip为Windows的ip

8、运行nginx

9、在windows浏览器上请求nginx,测试是否能够正常请求并得到处理

10、修改jmeter请求参数,将ip改为虚拟机的ip、端口改为80

通过redis的setnx命令实现简易分布式锁

@RestController
public class KillController {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @RequestMapping("/kill")
    public boolean kill(){
        String key = "lock";
        //通过setnx设置值,如果能设置上返回true否则返回false
        boolean flag = redisTemplate.opsForValue().setIfAbsent(key,"woniuxy");

        //如果能设置上
        if (flag){
            //判断库存是否足够
            int store = (Integer) redisTemplate.opsForValue().get("store");
            //
            if (store>0){
                store--;
                System.out.println("秒删成功,还剩:"+store);
                //修改redis中库存
                redisTemplate.opsForValue().set("store",store);
                //移除锁
                redisTemplate.delete(key);
                return true;
            }
        }
        return false;
    }
}

存在问题:当业务代码出现异常时,释放锁的操作无法进行

解决方案:给业务代码加上try,在finally中释放锁

@RestController
public class KillController {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @RequestMapping("/kill")
    public boolean kill(){
        String key = "lock";
        //通过setnx设置值,如果能设置上返回true否则返回false
        boolean flag = redisTemplate.opsForValue().setIfAbsent(key,"woniuxy");

        //如果能设置上
        if (flag){
            try {
                //判断库存是否足够
                int store = (Integer) redisTemplate.opsForValue().get("store");
                //
                if (store>0){
                    store--;
                    System.out.println("秒删成功,还剩:"+store);
                    //修改redis中库存
                    redisTemplate.opsForValue().set("store",store);
                    //返回
                    return true;
                }
            } finally {
                //移除锁
                redisTemplate.delete(key);
            }

        }
        return false;
    }
}

存在问题:当在释放锁代码执行之前,服务器因为某种原因宕机,导致锁无法释放

解决方案:给锁设置过期时间

@RestController
public class KillController {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @RequestMapping("/kill")
    public boolean kill(){
        String key = "lock";
        //通过setnx设置值,如果能设置上返回true否则返回false
        boolean flag = redisTemplate.opsForValue().setIfAbsent(key,"woniuxy",10, TimeUnit.SECONDS);

        //如果能设置上
        if (flag){
            try {
                //判断库存是否足够
                int store = (Integer) redisTemplate.opsForValue().get("store");
                //
                if (store>0){
                    store--;
                    System.out.println("秒删成功,还剩:"+store);
                    //修改redis中库存
                    redisTemplate.opsForValue().set("store",store);
                    //返回
                    return true;
                }
            } finally {
                //移除锁
                redisTemplate.delete(key);
            }

        }
        return false;
    }
}

存在问题:前面的线程可能会删除后面线程设置key

解决方案:判断是不是自己设置的key,是则删除,否则不删除

@RequestMapping("/kill")
    public boolean kill(){
        String key = "lock";
        String value = UUID.randomUUID().toString();
        //通过setnx设置值,如果能设置上返回true否则返回false
        boolean flag = redisTemplate.opsForValue().setIfAbsent(key,value,10, TimeUnit.SECONDS);

        //如果能设置上
        if (flag){
            try {
                //判断库存是否足够
                int store = (Integer) redisTemplate.opsForValue().get("store");
                //
                if (store>0){
                    store--;
                    System.out.println("秒删成功,还剩:"+store);
                    //修改redis中库存
                    redisTemplate.opsForValue().set("store",store);
                    //返回
                    return true;
                }
            } finally {
                //获取value
                String uuid = (String) redisTemplate.opsForValue().get(key);
                if (uuid.equals(value)){
                    //移除锁
                    redisTemplate.delete(key);
                }
            }
        }
        return false;
    }

问题:业务时间如果大于过期时间,会导致业务还没执行完key就过期了

解决方案:通过redisson的分布式锁实现续命

2.3 Redisson实现秒杀

1、导入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.5</version>
</dependency>

2、编写redisson配置类

import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfiguration {
    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson)Redisson.create(config);
    }
}

3、修改controller代码

@RestController
public class KillController {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private Redisson redisson;

    @RequestMapping("/kill")
    public boolean kill(){
        String key = "key";
        //1.获取锁
        RLock lock = redisson.getLock(key);
        try {
            //2.上锁
            lock.lock();
            int store = (Integer)redisTemplate.opsForValue().get("store");
            if (store>0){
                store--;
                System.out.println("库存剩余:"+store);
                redisTemplate.opsForValue().set("store",store);
                return true;
            }
        }finally {
            //3.解锁
            lock.unlock();
        }
        return false;
    }
}

注意:key的值最好不要用敏感词汇,例如lock,会报错