Redis实例应用(应用场景+分布式锁)

发布时间 2023-09-01 11:28:50作者: 九极致之术

1.redis的实际使用实例

1.1 热点数据的实际缓存

缓存缓存,在实际应用中,我们通常会把查询数据次数高的数据放入到Redis中,以便减轻后方数据库的压力。

当缓存数据存入到Redis中,下次在访问相同的数据时,就不在直接操作数据库,直接从Redis中取缓存数据

【注:我们通常会把查询频率高的数据修改频率低的数据数据安全性要求不高的数据放入Redis中】

【注:缓存可以提高我们的查询效率,可以降低数据库的访问频率,减少数据库的压力】

示意图:

1.2使用Redis作为缓存

1.2.1 自己手写Redis缓存非业务代码

准备:

新建springboot工程,记得勾选以下的依赖包

创建好后我这里将版本改成2.3.2.RELEASE

如果你们要跟我一样的版本,那么刚才所勾选的mysql是不兼容的,所以需要改动依赖

而且我们也需要MybatisPlass,所以也需要加MybatisPlass依赖

      <!-- 2.3.2版本下的mysql依赖 -->
    <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
     </dependency>
    <!-- MybatisPlass依赖 -->
     <dependency>
         <groupId>com.baomidou</groupId>
         <artifactId>mybatis-plus-boot-starter</artifactId>
         <version>3.5.1</version>
     </dependency>

application文件中添加配置:

#mysql连接数据库信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/aaasql?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

#redis
spring.redis.host=192.168.235.135
spring.redis.port=6379

#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

实体类:

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "tbl_student")
public class student {
    @TableId(type = IdType.AUTO)
    private int id;
    @TableField(value = "s_name")
    private String sname;
    private String sex;
    private int cid;
}

vo-Result实体类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Retust {
    private int code;
    private String msg;
    private Object data;
}

dao层:

public interface StudentDao extends BaseMapper<student> {
}

service层与service实现类:

public interface StudentService {
    Retust seleid(Integer id);
}
@Service
public class StudentServiceim implements StudentService {
    @Autowired
    private StudentDao studentDao;

    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public Retust seleid(Integer id) {
        //首先从Redis中查询数据
        Object o = redisTemplate.opsForValue().get("stu::" + id);
        //判断缓存中是否存在,存在就直接将数据返回回去
        if (o != null && o instanceof student){
            return new Retust(200,"查询成功",o);
        }
        //判断数据库中是否存在所要查询的结果,如果有就将结果返回且将结果存入到缓存中
        student student = studentDao.selectById(id);
        if (student != null){
            redisTemplate.opsForValue().set("stu::"+id,student);
            return new Retust(200,"查询成功",student);
        }
        return new Retust(500,"查询失败",null);
    }
}

controller层:

@RestController
public class StudentController {

    @Autowired
    private StudentService service;

    @GetMapping("/sele")
    private Retust sele(Integer id){
        return service.seleid(id);
    }
}

运行测试:

 

【注:第一次运行,缓存中并没有要查找的数据,所以需要从数据库中查找数据,查找成功后,其将数据返回且存入了缓存中】

【注:那么第二次查询,其将会直接从缓存中取出数据返回】

1.2.1 springboot提供注解来实现Redis缓存

@Cacheable(cacheNames = "key",key = "#id")】--查询缓存注解

@CachePut(cacheNames = "key",key="#student.id")】--修改缓存注解

@CacheEvict(cacheNames = "key",key = "#id")】--删除缓存注解

性能更好,更加人性化

【注:首先必须让redisTemplate支持缓存】

在已有的config包中的RedisConfig文件中【没有创建一个】

@Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> 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;
    }

 在主启动类上添加开启缓存注解

@SpringBootApplication
@MapperScan(basePackages = "com.aaa.dao")
@EnableCaching //开启缓存注解驱动
public class RedisSpringBoot02Application {
    public static void main(String[] args) {
        SpringApplication.run(RedisSpringBoot02Application.class, args);
    }
}

 在业务层使用该注解: @Cacheable(cacheNames = "key 的名字",key = "#id")

@Service
public class StudentServiceim implements StudentService {
    @Autowired
    private StudentDao studentDao;

    //该注解用于查询功能方法上。 先查询缓存中是否存在cacheNames+"::"+key是否存在,
    // 如果存在则不执行该方法,如果不存在则执行该方法并把该方法的返回结果存入缓存中。以cacheName+"::"+key作为缓存的key
    @Cacheable(cacheNames = "stu",key = "#id")
    public Retust seleid(Integer id) {
        student student = studentDao.selectById(id);
        return student!=null?new Retust(200,"查询成功",student):new Retust(500,"查询失败",null);
    }
}

 测试运行:

结果与手动的一样,当缓存中没有查询所需的结果就会去数据库中查找返回,且将结果存入缓存中,第二次访问时,就会直接从redis中取所需的值

这里只演示了查询,剩下的只需大家一一测试即可

完整增删改查:【注:service接口+service实现类】

public interface StudentService {
    //查询
    Retust seleid(Integer id);
    //修改
    Retust updata(student student);
    //添加
    Retust insert(student student);
    //删除
    Retust del(Integer id);
}
@Service
public class StudentServiceim implements StudentService {

    @Autowired
    private StudentDao studentDao;

    //该注解用于查询功能方法上。 先查询缓存中是否存在cacheNames+"::"+key是否存在,
    // 如果存在则不执行该方法,如果不存在则执行该方法并把该方法的返回结果存入缓存中。以cacheName+"::"+key作为缓存的key
    @Cacheable(cacheNames = "stu",key = "#id")
    public Retust seleid(Integer id) {
        student student = studentDao.selectById(id);
        return student!=null?new Retust(200,"查询成功",student):new Retust(500,"查询失败",null);
    }

    //更改
    //把该方法的返回结果重新写入到缓存中
    @CachePut(cacheNames = "stu",key="#student.id")
    public Retust updata(student student) {
        int i = studentDao.updateById(student);
        return i!=0?new Retust(200,"修改成功",student):new Retust(500,"修改失败",null);
    }

    //插入 [注:添加不需要进行缓存,因为添加过后的数据会通过查询来直接插入到缓存中]
    @Override
    public Retust insert(student student) {
        int i = studentDao.insert(student);
        return i!=0?new Retust(200,"添加成功",student):new Retust(500,"添加失败",null);
    }
    //删除
    //把缓存中的数据进行移除,以cacheName+"::"+key作为缓存的key移除
    @CacheEvict(cacheNames = "stu",key = "#id")
    public Retust del(Integer id) {
        int i = studentDao.deleteById(id);
        return i!=0?new Retust(200,"删除成功",null):new Retust(500,"删除失败",null);
    }
}

2.redis实现分布式锁

搭建实验环境:

【注;可以继续使用上面的springboot工程模板】

实体类:

@Data
@TableName("tbl_stock")
public class Stock {
    @TableId(type = IdType.AUTO)
    private int id;
    private int goodsnum;
}

数据库展示:

 dao层:

@Mapper
public interface StockDao {
    @Select("select goodsnum from tbl_stock where id=#{id}")
    int findById(Integer productid);

    @Update("update tbl_stock set goodsnum=goodsnum-1 where id=#{id} ")
    void update(Integer productid);
}

 service层:

@Service
public class StockService02 {

    @Autowired
    private StockDao stockDao;
    //---通过jmeter压测后发现商品出现--线程安全问题。
    //--如何解决上面的线程安全问题: 加锁。【自动锁synchronized 或手动锁 Lock】
    //--如果我们现在的项目部署时为一个集群--如果再高并发下使用【自动锁synchronized 或手动锁 Lock】有出现了线程安全问题。【自动锁synchronized 或手动锁 Lock】他们属于jvm锁。
    //--如何解决集群下线程安全问题。
    public String decrement(Integer productid) {
        synchronized (this) {

            int num = stockDao.findById(productid);
            if (num > 0) {
                stockDao.update(productid);
                System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
            } else {
                System.out.println("商品编号为:" + productid + "的商品库存不足。");
                return "商品编号为:" + productid + "的商品库存不足。";
            }
        }
    }
}

 controller:

@RestController
public class StockController {

    @Autowired
    private StockService02 stockService;

    //根据商品编号减库存
    @GetMapping("/incr/{productid}")
    public String incr(@PathVariable Integer productid){
         return stockService.decrement(productid);
    }
}

 测试运行:

现在只是单一测试,在实际应用场景中,有时可能会达到几万的并发量,这里就可能会出现超卖的情况

2.1 模拟多服务

现在我们模拟多服务,多线程并发的场景

这里使用两款工具:

第一,我们需要两台服务共同运行

 

启动两台服务器

2.2使用Nginx代理

这里使用windows版Nginx软件

Nginx百度云盘链接  提取码:6666

下载解压后

打开里面的conf目录,打开nginx.conf配置文件

将我们两台服务代理

#定义被负载均衡的所有服务器地址【upstream: 定义集群信息】
upstream  aaa{
server localhost:8088;
server localhost:8089;
}

#定义负载均衡代理端口
server {
    listen 83;
    server_name localhost;
    
    location /{
    proxy_pass http://aaa;
    }
}

2.3使用JMeter工具

【注:使用Jmeter工具用来模拟多用户多线程并发场景】

jmerer百度云盘链接   提取码:6666

下载后解压

打开文件目录进入到bin目录中,打开jmeter.bat文件进入

点击运行

【注:前提是你的idea两台服务都要启动】

2.4使用线程自动锁测试结果

果然,在多线程的并发下,出现了超卖现象,这种是及不被允许的

【注:这里是已经使用线程自动锁的情况下】

2.5使用Redis分布式锁

service层代码:

@Service
public class StockService02 {

    @Autowired
    private StockDao stockDao;
    @Autowired
    private RedisTemplate redisTemplate;
    //---通过jmeter压测后发现商品出现--线程安全问题。
    //--如何解决上面的线程安全问题: 加锁。【自动锁synchronized 或手动锁 Lock】
    //--如果我们现在的项目部署时为一个集群--如果再高并发下使用【自动锁synchronized 或手动锁 Lock】有出现了线程安全问题。【自动锁synchronized 或手动锁 Lock】他们属于jvm锁。
    //--如何解决集群下线程安全问题。
    public String decrement(Integer productid) {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //使用opsForValue()中的setIfAbsent方法来实现Redis分布式锁
        Boolean flss = valueOperations.setIfAbsent("pro::" + productid, "test", 30, TimeUnit.SECONDS);
        //判断是否已经拿到锁,true为已拿到锁
        if (flss){
            try {
                int num = stockDao.findById(productid);
                if (num > 0) {
                    stockDao.update(productid);
                    System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                    return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
                } else {
                    System.out.println("商品编号为:" + productid + "的商品库存不足。");
                    return "商品编号为:" + productid + "的商品库存不足。";
                }
            }finally {
                //释放锁资源
                redisTemplate.delete("pro::"+productid);
            }//如果没有拿到锁进行以下尝试
        }else {
            //休眠100毫秒再次尝试调用锁
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            //递归锁
            return decrement(productid);
        }


    }
}

再次测压运行测试:

分布式锁成功起到作用

Redis分布式锁测试成功!!!


以上便是Redis实例应用(应用场景+分布式锁)中的内容,如有漏缺请在下方留言告知,我会及时补充