目录
springboot对redis的操作封装了两个模板工具类, StringRedisTemplate和RedisTemplate类,StringRedisTemplate是RedisTemplate的子类,StringRedisTemplate它只能存储字符串类型,无法存储对象类型。要想用StringRedisTemplate存储对象必须把对象转为json字符串。
(1)引入相关依赖
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-data-redisartifactId>
- dependency>
(2)配置文件 前提记得开redis服务
- #redis的配置
- spring.redis.host=192.168.19.151
- spring.redis.port=7777
- spring.redis.jedis.pool.max-active=20
- spring.redis.jedis.pool.max-wait=20000
- spring.redis.jedis.pool.max-idle=10
- spring.redis.jedis.pool.min-idle=5
(3)注入StringRedisTemplate该类对象
- @Autowired
- private StringRedisTemplate redisTemplate;
(4)使用StringRedisTemplate
该类把对每种数据类型的操作,单独封了相应的内部类。
Spring内部存在一个类可以把java对象转化json字符串 ObjectMapper类2.2
- package com.wzh;
-
- import com.fasterxml.jackson.core.JsonProcessingException;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
- import com.wzh.entity.User;
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.data.redis.core.HashOperations;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import redis.clients.jedis.*;
-
- import javax.lang.model.element.VariableElement;
- import java.time.Duration;
- import java.util.*;
- import java.util.concurrent.TimeUnit;
-
- @SpringBootTest
- class RedisJedisSpringbootApplicationTests02 {
- //里面所有的key还是value field它的类型必须都是string类型
- //因为key和value获取field他们使用的都是String的序列化方式
- @Autowired
- private StringRedisTemplate redisTemplate;
-
- @Test
- public void test01(){
- //对hash类型的操作
- HashOperations
forHash = redisTemplate.opsForHash(); - forHash.put("k1","name","王振华");
- forHash.put("k1","age","15");
- Map
map = new HashMap<>(); - map.put("name","张三");
- map.put("age","15");
- forHash.putAll("k2",map);
-
- Object o = forHash.get("k1","name");
- System.out.println(o);
-
- Set
- System.out.println(keys);
- List
- System.out.println(values);
-
- //获取k1对应的所有的field和value
- Map
- System.out.println(k1);
-
- }
-
- @Test
- public void test02() throws JsonProcessingException {
- //删除指定的key
- Boolean k1 = redisTemplate.delete("k1");
- //查看所有的key
- Set
keys = redisTemplate.keys("*"); - System.out.println(keys);
- //是否存在指定的key
- Boolean k11 = redisTemplate.hasKey("k1");
- System.out.println(k11);
-
- //redisTemplate对每种数据类型都封装了一个新的类。而这些新的类有对应的操作方法。
- //操作字符串的类
- ValueOperations
forValue = redisTemplate.opsForValue(); - //存储字符串类型--key value long unit 相当于redis中的setex()
- forValue.set("k1","6666",30, TimeUnit.SECONDS);
-
- //等价于setnx 存入成功返回true 失败则返回false
- Boolean absent = forValue.setIfAbsent("k2", "7777", 30, TimeUnit.SECONDS);
- System.out.println(absent);
-
- //追加
- Integer append = forValue.append("k2", "9999");
- System.out.println(forValue.get("k2"));
-
- //存储对象类型
- //Spring内部存在一个类可以把java对象转化json字符串
- ObjectMapper objectMapper=new ObjectMapper();
- User user = new User(11, "李四", 15);
- String s = objectMapper.writeValueAsString(user);
- forValue.set("k3",s);
-
- String k3 = forValue.get("k3");
- //把json字符串转化为对应的类对象
- User user1 = objectMapper.readValue(k3, User.class);
- System.out.println(user1);
- }
- }
当你存储的value类型为对象类型使用redisTemplate
存储的value类型为字符串。使用StringRedisTemplate 比如验证码
- package com.wzh;
-
- import com.wzh.entity.User;
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.data.redis.core.HashOperations;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
- import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.TimeUnit;
-
- @SpringBootTest
- class RedisJedisSpringbootApplicationTests03 {
- //当你存储的value类型为对象类型使用redisTemplate
- //存储的value类型为字符串。StringRedisTemplate 比如验证码
- @Autowired
- public RedisTemplate redisTemplate;
-
- @Test
- public void test01(){
- //必须人为指定序列化方式
- redisTemplate.setKeySerializer(new StringRedisSerializer());
- //redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer
- //这两个序列化比较常用 Jackson2JsonRedisSerializer占用的内存小一点
- redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
-
- //对String类型操作类
- ValueOperations forValue = redisTemplate.opsForValue();
- //redis中key和value都变成乱码了
- //key和value都没有指定序列化方式,默认采用jdk的序列化方式
- forValue.set("k1","v1");
-
- //value默认采用jdk,类必须实现序列化接口
- forValue.set("k2",new User(15,"张三",22));
- }
- }
当RedisTemplate的value值是对象类型时,类需要实现Serializable接口,不然会乱码,因为默认会采用jdk序列化,上面存储String类型时没有乱码的原因是因为String类实现序列化接口了。
上面的RedisTemplate需要每次都指定key value以及field的序列化方式,能不能搞一个配置类,已经为RedisTemplate指定好序列化。以后再用就无需指定。
- package com.wzh.config;
-
- import com.fasterxml.jackson.annotation.JsonAutoDetect;
- import com.fasterxml.jackson.annotation.PropertyAccessor;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.cache.CacheManager;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.cache.RedisCacheConfiguration;
- import org.springframework.data.redis.cache.RedisCacheManager;
- 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.RedisSerializationContext;
- import org.springframework.data.redis.serializer.RedisSerializer;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- import java.time.Duration;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.config
- * @ClassName: RedisConfig
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:06
- * @Version: 1.0
- */
- @Configuration //标记该类为配置类
- public class RedisConfig {
- @Bean
- public RedisTemplate
redisTemplate(RedisConnectionFactory factory) { - RedisTemplate
template = new RedisTemplate<>(); - RedisSerializer
redisSerializer = new StringRedisSerializer(); - Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
- ObjectMapper om = new ObjectMapper();
- om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
- jackson2JsonRedisSerializer.setObjectMapper(om);
- template.setConnectionFactory(factory);
- //key序列化方式
- template.setKeySerializer(redisSerializer);
- //value序列化
- template.setValueSerializer(jackson2JsonRedisSerializer);
- //value hashmap序列化 filed value
- template.setHashValueSerializer(jackson2JsonRedisSerializer);
- template.setHashKeySerializer(redisSerializer);
- return template;
- }
- }
这样上面的就不需要设置序列化方式了
(1)数据存储在内存中,数据查询速度快。可以分摊数据库压力。
(2)什么样的数据适合放入缓存
查询频率比较高,修改频率比较低。
安全系数低的数据
(3)使用redis作为缓存
这里我们用到了mybatis-plus所以需要引入依赖
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- dependency>
- <dependency>
- <groupId>com.baomidougroupId>
- <artifactId>mybatis-plus-boot-starterartifactId>
- <version>3.5.1version>
- dependency>
controller层:
这里有个知识点,我们可以通过 @GetMapping(value = "getById/{id}") 传参,public CommonResult getById(@PathVariable("id") Integer id) 获取获取请求映射中{}中的值。
这样的好处是安全 ,这样他们就不知道参数 1 是什么 ,如果有id = 1 聪明的人一眼就知道了,所以这种方式现在比较流行
- package com.wzh.controller;
-
- import com.wzh.entity.User;
- import com.wzh.service.UserService;
- import com.wzh.utils.CommonResult;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.controller
- * @ClassName: UserController
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:36
- * @Version: 1.0
- */
- @RestController
- @RequestMapping("order")
- public class UserController {
- @Autowired
- private UserService userService;
-
- //order/getById/1
- //@PathVariable:获取请求映射中{}中的值
- @GetMapping(value = "getById/{id}")
- public CommonResult getById(@PathVariable("id") Integer id){
- return userService.findById(id);
- }
-
- @GetMapping("deleteById/{id}")
- public CommonResult deletebyId(@PathVariable("id") Integer id){
- return userService.deleteById(id);
- }
-
- @PostMapping("update")
- public CommonResult update(@RequestBody User user){
- return userService.update(user);
- }
-
- @PostMapping("insert")
- public CommonResult insert(@RequestBody User user){
- return userService.insert(user);
- }
- }
service:
- package com.wzh.service;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import com.wzh.entity.User;
- import com.wzh.utils.CommonResult;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.service
- * @ClassName: UserService
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:41
- * @Version: 1.0
- */
- public interface UserService{
- CommonResult findById(Integer id);
-
- CommonResult deleteById(Integer id);
-
- CommonResult insert(User user);
-
- CommonResult update(User user);
- }
serviceImpl:
- /*
- package com.wzh.service.impl;
- */
-
- import com.wzh.entity.User;
- import com.wzh.mapper.UserMapper;
- import com.wzh.service.UserService;
- import com.wzh.utils.CommonResult;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import org.springframework.stereotype.Service;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.service.impl
- * @ClassName: UserServiceImpl
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:41
- * @Version: 1.0
- *//*
- */
-
- @Service
- public class UserServiceImpl implements UserService {
- @Autowired
- private UserMapper userMapper;
-
- @Autowired
- private RedisTemplate redisTemplate;
-
- //业务代码
-
- @Override
- public CommonResult findById(Integer id){
- ValueOperations forValue = redisTemplate.opsForValue();
- //查询缓存
- Object o = forValue.get("user::" + id);
- //缓存命中
- if(o!=null){
- return (CommonResult) o;
- }
- //未命中 查询数据库
- User user = userMapper.selectById(id);
- if(user!=null){
- //存入缓存
- forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30,TimeUnit.MINUTES);
- }else{
- return new CommonResult(5000,"查询失败",null);
- }
- return new CommonResult(2000,"查询成功",user);
-
- }
-
- @Override
- public CommonResult deleteById(Integer id){
- redisTemplate.delete("user::"+id);
- int i = userMapper.deleteById(id);
- return new CommonResult(2000,"删除成功",i);
- }
- //@Transactional
- @Override
- public CommonResult update(User user){
- //ValueOperations forValue = redisTemplate.opsForValue();
- //forValue.set("user::"+user.getId(),user,2, TimeUnit.HOURS);
- redisTemplate.delete("user::"+user.getId());
- int i = userMapper.updateById(user);
- return new CommonResult(2000,"修改成功",i);
- }
-
- @Override
- public CommonResult insert(User user){
- int insert = userMapper.insert(user);
- return new CommonResult(2000,"添加成功",insert);
- }
-
- }
- /*
-
mapper:
- package com.wzh.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import com.wzh.entity.User;
- import org.apache.ibatis.annotations.Mapper;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.mapper
- * @ClassName: UserMapper
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:44
- * @Version: 1.0
- */
- @Mapper
- public interface UserMapper extends BaseMapper
{ - }
replication.properties:
- #redis的配置
- spring.redis.host=192.168.19.151
- spring.redis.port=7777
- spring.redis.jedis.pool.max-active=20
- spring.redis.jedis.pool.max-wait=20000
- spring.redis.jedis.pool.max-idle=10
- spring.redis.jedis.pool.min-idle=5
-
- server.port=8080
- #mysql配置
- spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
- spring.datasource.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai
- spring.datasource.username=root
- spring.datasource.password=123456
-
- #日志
- mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
查看的缓存: 前部分代码相同@before通知,后部分代码也相同后置通知。 我们可以AOP完成缓存代码和业务代码分离。
spring框架它应该也能想到。--使用注解即可完成。解析该注解。
(1)把缓存的配置类加入
- package com.wzh.config;
-
- import com.fasterxml.jackson.annotation.JsonAutoDetect;
- import com.fasterxml.jackson.annotation.PropertyAccessor;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.cache.CacheManager;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.cache.RedisCacheConfiguration;
- import org.springframework.data.redis.cache.RedisCacheManager;
- 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.RedisSerializationContext;
- import org.springframework.data.redis.serializer.RedisSerializer;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- import java.time.Duration;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.config
- * @ClassName: RedisConfig
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:06
- * @Version: 1.0
- */
- @Configuration //标记该类为配置类
- public class RedisConfig {
- @Bean
- public CacheManager cacheManager(RedisConnectionFactory factory) {
- RedisSerializer
redisSerializer = new StringRedisSerializer(); - Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
- //解决查询缓存转换异常的问题
- ObjectMapper om = new ObjectMapper();
- om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
- jackson2JsonRedisSerializer.setObjectMapper(om);
- // 配置序列化(解决乱码的问题),过期时间600秒
- RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
- .entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
- .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
- .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
- .disableCachingNullValues();
- RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
- .cacheDefaults(config)
- .build();
- return cacheManager;
- }
- }
上面不是已经配置过一次了吗,为什么还需要再配置,是因为上面的都是手动创建的RedisTeplate类对象redisTemplate.opsForValue(),可是这里我们使用注解的话,都是spring容器帮我们创建的,所以需要重新配置。他的缺点就是灵活性差,缓存过期时间都是统一的,不能根据客户的需要设置,可以使用AOP自己写也能实现。
(2)主启动类开启缓存注解
(3)使用注解
- package com.wzh.service.impl;
-
- import com.wzh.entity.User;
- import com.wzh.mapper.UserMapper;
- import com.wzh.service.UserService;
- import com.wzh.utils.CommonResult;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cache.annotation.CacheEvict;
- import org.springframework.cache.annotation.CachePut;
- import org.springframework.cache.annotation.Cacheable;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import java.util.concurrent.TimeUnit;
-
- /**
- * @ProjectName: redis-jedis-springboot
- * @Package: com.wzh.service.impl
- * @ClassName: UserServiceImpl
- * @Author: 王振华
- * @Description:
- * @Date: 2022/8/2 20:41
- * @Version: 1.0
- */
- @Service
- public class UserServiceImpl implements UserService {
- @Autowired
- private UserMapper userMapper;
-
- @Autowired
- private RedisTemplate redisTemplate;
-
- //业务代码
- //使用查询注解:cacheNames表示缓存的名称 key:唯一标志---user::key
- //先从缓存中查看key为(cacheNames::key)是否存在,如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
- @Override
- @Cacheable(cacheNames = {"user"},key = "#id")
- public CommonResult findById(Integer id){
- User user = userMapper.selectById(id);
- return new CommonResult(2000,"查询成功",user);
-
- }
-
- @Override
- //先删除缓存在执行方法体。
- @CacheEvict(cacheNames = {"user"},key = "#id")
- public CommonResult deleteById(Integer id){
- int i = userMapper.deleteById(id);
- return new CommonResult(2000,"删除成功",i);
- }
- //@Transactional
- @Override
- //这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
- @CachePut(cacheNames = {"user"},key = "#user.id")
- public CommonResult update(User user){
- int i = userMapper.updateById(user);
- if(i!=0) {
- return new CommonResult(2000, "成功", user);
- }else {
- return new CommonResult(5000, "失败", i);
- }
- }
-
- @Override
- public CommonResult insert(User user){
- int insert = userMapper.insert(user);
- return new CommonResult(2000,"添加成功",insert);
- }
-
- }
我们用这个项目来示范多线程并发带来的问题:
controller:
- package com.ykq.distrinctlock.controller;
-
- import com.ykq.distrinctlock.service.ProductStockService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.HttpRequest;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import javax.servlet.http.HttpServletRequest;
-
- @RestController
- @RequestMapping("productStock")
- public class ProductStockController {
- @Autowired
- private ProductStockService productStockService;
- //减库存
- @RequestMapping("decreaseStock/{productId}")
- public String decreaseStock(@PathVariable("productId") Integer productId){
- return productStockService.decreaseStock(productId);
- }
- }
service:
- package com.ykq.distrinctlock.service;
- public interface ProductStockService {
- //减少库存
- public String decreaseStock( Integer productId);
- }
serviceImpl:
- package com.ykq.distrinctlock.service.impl;
-
- import com.ykq.distrinctlock.dao.ProductStockDao;
- import com.ykq.distrinctlock.service.ProductStockService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- @Service
- public class ProductStockServiceImpl2 implements ProductStockService {
- @Autowired
- private ProductStockDao productStockDao;
-
- @Override
- public String decreaseStock(Integer productId) {
-
- //查看该商品的库存数量
- Integer stock = productStockDao.findStockByProductId(productId);
- if (stock > 0) {
- //修改库存每次-1
- productStockDao.updateStockByProductId(productId);
- System.out.println("扣减成功!剩余库存数:" + (stock - 1));
- return "success";
- } else {
- System.out.println("扣减失败!库存不足!");
- return "fail";
- }
-
-
- }
- }
dao:
- package com.ykq.distrinctlock.dao;
-
- import org.apache.ibatis.annotations.Mapper;
-
- @Mapper
- public interface ProductStockDao {
-
- public Integer findStockByProductId(Integer id);
-
- public void updateStockByProductId(Integer id);
- }
ProductStockMapper.xml:
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ykq.distrinctlock.dao.ProductStockDao">
-
- <select id="findStockByProductId" resultType="integer">
- select num from tbl_stock where productId=#{productId}
- select>
-
- <update id="updateStockByProductId">
- update tbl_stock set num=num-1 where productId=#{productId}
- update>
- mapper>
application.properties:
- server.port=8001
- spring.datasource.username=root
- spring.datasource.password=123456
- spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
- spring.datasource.url=jdbc:mysql://localhost:3306/distrinct_lock?serverTimezone=Asia/Shanghai
- mybatis.mapper-locations=classpath:/mapper/*.xml
-
- spring.redis.host=192.168.19.151
- spring.redis.port=7777
-
-
-
-
-
pom.xml:
- <dependencies>
- <dependency>
- <groupId>org.redissongroupId>
- <artifactId>redissonartifactId>
- <version>3.13.4version>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-data-redisartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
- <dependency>
- <groupId>org.mybatis.spring.bootgroupId>
- <artifactId>mybatis-spring-boot-starterartifactId>
- <version>2.1.3version>
- dependency>
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-devtoolsartifactId>
- <scope>runtimescope>
- <optional>trueoptional>
- dependency>
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- <scope>runtimescope>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-configuration-processorartifactId>
- <optional>trueoptional>
- dependency>
- <dependency>
- <groupId>org.projectlombokgroupId>
- <artifactId>lombokartifactId>
- <optional>trueoptional>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-testartifactId>
- <scope>testscope>
- <exclusions>
- <exclusion>
- <groupId>org.junit.vintagegroupId>
- <artifactId>junit-vintage-engineartifactId>
- exclusion>
- exclusions>
- dependency>
- dependencies>
使用压测工具jmeter测试高并发下带来线程安全问题
我们看到同一个库存被使用了n次。以及数据库中库存为负数。 线程安全问题导致。
解决方案: 使用 synchronized 或者lock锁
- package com.ykq.distrinctlock.service.impl;
-
- import com.ykq.distrinctlock.dao.ProductStockDao;
- import com.ykq.distrinctlock.service.ProductStockService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- @Service
- public class ProductStockServiceImpl2 implements ProductStockService {
- @Autowired
- private ProductStockDao productStockDao;
-
- @Override
- public String decreaseStock(Integer productId) {
- synchronized (this) {
- //查看该商品的库存数量
- Integer stock = productStockDao.findStockByProductId(productId);
- if (stock > 0) {
- //修改库存每次-1
- productStockDao.updateStockByProductId(productId);
- System.out.println("扣减成功!剩余库存数:" + (stock - 1));
- return "success";
- } else {
- System.out.println("扣减失败!库存不足!");
- return "fail";
- }
- }
-
- }
- }
使用synchronized 或者lock锁 如果我们搭建了项目集群,那么该锁无效。
这里我们用idea开集群项目
(1)创建另外一个tomcat
(2)配置nginx.conf并开启nginx(这里我们下载了window版的nginx) 建议不要下载到中文路径下
记得修改测压的端口号跟上边保持一致
(3)开启两个项目
再次压测,发现又出现: 重复数字以及库存为负数。
我们可以使用 redis作为锁 ,来获取锁和释放锁
- package com.ykq.distrinctlock.service.impl;
-
- import com.ykq.distrinctlock.dao.ProductStockDao;
- import com.ykq.distrinctlock.service.ProductStockService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import org.springframework.stereotype.Service;
-
- @Service
- public class ProductStockServiceImpl_redis implements ProductStockService {
- @Autowired
- private ProductStockDao productStockDao;
-
- @Autowired
- private StringRedisTemplate redisTemplate;
-
- @Override
- public String decreaseStock(Integer productId) {
- ValueOperations
forValue = redisTemplate.opsForValue(); - //必须保证一开始没有该key 也就是说flag必须为true
- Boolean flag = forValue.setIfAbsent("aaa::" + productId, "~~~~~~~~~~~~~~~~~~~~~~");
- if(flag) {
- try {
- //查看该商品的库存数量
- Integer stock = productStockDao.findStockByProductId(productId);
- if (stock > 0) {
-
- //修改库存每次-1
- productStockDao.updateStockByProductId(productId);
- System.out.println("扣减成功!剩余库存数:" + (stock - 1));
- return "success";
-
- } else {
- System.out.println("扣减失败!库存不足!");
- return "fail";
- }
- }finally {
- redisTemplate.delete("aaa::" + productId);
- }
-
- }
-
-
- return "服务器正忙,请稍后在试......";
- }
- }
记得开启redis服务,不然无法存储锁
这里我们发现没有重复和负数,我们的问题就解决了!!!!
Redis分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
可以使用:redission依赖,redission解决redis超时问题的原理。
为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。
使用:
(1)引入redisson依赖
- <dependency>
- <groupId>org.redissongroupId>
- <artifactId>redissonartifactId>
- <version>3.13.4version>
- dependency>
(2)配置redission对象并交于spring容器管理
- @Bean
- public Redisson redisson(){
- Config config =new Config();
- config.useSingleServer().
- setAddress("redis://localhost:6379").
- //redis默认有16个数据库
- setDatabase(0);
- return (Redisson) Redisson.create(config);
- }
这里我们因为方便,就不用linux的redis了,我们在window上下载了个redis,跟linux上的使用是一样的
测试:
- package com.ykq.distrinctlock.service.impl;
-
- import com.ykq.distrinctlock.dao.ProductStockDao;
- import com.ykq.distrinctlock.service.ProductStockService;
- import org.redisson.Redisson;
- import org.redisson.api.RLock;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import org.springframework.stereotype.Service;
-
- import java.util.concurrent.TimeUnit;
-
- @Service
- public class ProductStockServiceImpl_redisson implements ProductStockService {
- @Autowired
- private ProductStockDao productStockDao;
-
- @Autowired
- private Redisson redisson;
-
- @Override
- public String decreaseStock(Integer productId) {
- //获取锁对象
- RLock lock = redisson.getLock("aaa::" + productId);
- try {
- //加锁
- lock.lock(30,TimeUnit.SECONDS);
- //查看该商品的库存数量
- Integer stock = productStockDao.findStockByProductId(productId);
- if (stock > 0) {
-
- //修改库存每次-1
- productStockDao.updateStockByProductId(productId);
- System.out.println("扣减成功!剩余库存数:" + (stock - 1));
- return "success";
-
- } else {
- System.out.println("扣减失败!库存不足!");
- return "fail";
- }
- }finally{
- //释放锁
- lock.unlock();
- }
- }
- }
1. 数据库中没有该记录,缓存中也没有该记录,这时由人恶意大量访问这样的数据。这样就会导致该请求绕过缓存,直接访问数据,从而造成数据库压力过大。
2.解决办法:
[1]在controller加数据校验。
[2]我们可以在redis中存入一个空对象,而且要设置过期时间不能太长。超过5分钟
[3]我们使用布隆过滤器。底层:有一个bitmap数组,里面存储了该表的所有id.
建议三种都使用:
Controller层 判断数据是否有意义
这里只对查找方法进行了优化
- @Override
- public CommonResult findById(Integer id){
- //创建布隆过滤器对象
- BloomFilter
bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 50,0.05); - //存放1-50的id号
- for (int i = 1; i <= 50; i++) {
- bloomFilter.put(i);
- }
- ValueOperations forValue = redisTemplate.opsForValue();
- //查询缓存
- Object o = forValue.get("user::" + id);
-
- //缓存命中
- if(o!=null){
- // 判断对象是否属于该类型
- if(!(o instanceof NullObject)) {
- return (CommonResult) o;
- }
- return null;
- }
- //未命中
- if(bloomFilter.mightContain(id)){ //查看布隆过滤器是否存在
- User user = userMapper.selectById(id);
- if(user!=null){
- //存入缓存
- forValue.set("user::"+id,new CommonResult(2000,"查询成功",user),30, TimeUnit.MINUTES);
- return new CommonResult(2000,"查询成功",user);
- }else{
- //在redis中存入一个空对象,小于5分钟 避免缓存穿透
- forValue.set("user::"+id,new NullObject(),5, TimeUnit.MINUTES);
- return new CommonResult(5000,"查询失败",null);
- }
- }
- return null;
-
- }
//伪代码
String get(String key) { //布隆过滤器钟存储的是数据库表钟对应的id
String value = redis.get(key); //先从缓存获取。
if (value == null) { //缓存没有命中
if(!bloomfilter.mightContain(key)){//查看布隆过滤器中是否存在
return null;q
}else{
value = db.get(key); //查询数据库
redis.set(key, value);
}
}
return value;
}
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存击穿解决方案:
1.设置永久不过期。【这种只适合内存】
2.使用互斥锁(mutex key)业界比较常用的做法。
- //伪代码
- public String get(key) {
- String value = redis.get(key);
- if (value == null) { //代表缓存值过期
- //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
- if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
- value = db.get(key);
- redis.set(key, value, expire_secs);
- redis.del(key_mutex);
- } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
- sleep(50);
- get(key); //重试
- }
- } else {
- return value;
- }
- }
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
1.什么下会发生缓存雪崩:
[1]项目刚上线,缓存中没有任何数据
[2]缓存出现大量过期。
[3]redis宕机
2.解决办法:
1.上线前预先把一些热点数据放入缓存。
2.设置过期时间为散列值
3.搭建redis集群
默认是从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
在redis.conf中配置