Redis分布式解决数据不一致问题

目录

1.Service中缓存一致性分析

第一步:修改TagService接口,添加相关方法,例如:

第二步:修改TagServiceImpl类,在类中重写TagService接口方法,例如:

第三步:将Redis中数据key设置为一致状态

第四步:修改单元测试类,测试缓存数据一致性.

5.在Controller中添加一个本地缓存,减少对远程redis缓存的访问,例如:

Controller中本地缓存一致性分析

Redis集群链接配置实践


 

1.Service中缓存一致性分析

当我们从数据库查询数据以后,假如将数据存入到了缓存,后续更新了数据库的数据,但假如没有更新缓存就会出现缓存数据与数据库数据不一致的这样的现象,对于这样问题,有时允许在一定时间范围之内存在。假如我们希望在更新了数据库数据以后要更新缓存,如何实现呢?接下来通过一个案例,来演示和解决一下这个问题.

第一步:修改TagService接口,添加相关方法,例如:

package com.jt.blog.service;
import com.jt.blog.domain.Tag;
import java.util.List;
public interface TagService {
    /**
     * 查询所有的标签
     * @return
     */
    List<Tag> selectTags();
    /**
     * 创建一个新的tag对象
     * @param tag
     */
    void insertTag(Tag tag);

    /**
     * 更新tag对象
     * @param tag
     * @return
     */
    Tag updateTag(Tag tag);

    /**
     * 基于id查询tag信息
     * @param id
     * @return
     */
    Tag selectById(Long id);
}

第二步:修改TagServiceImpl类,在类中重写TagService接口方法,例如:

package com.jt.blog.service.impl;

import com.jt.blog.dao.TagMapper;
import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
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 java.util.LinkedHashMap;
import java.util.List;

@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagMapper tagMapper;

    /*@Cacheable注解描述的方法为缓存切入点方法
     *访问此方法时系统底层会先从缓存查找数据,假如缓存缓存没有,
     *会查询mysql数据库,这个注解假如想生效需要在启动类或者配置
     *类上添加@EnableCaching注解.
     *其中,这里的value用于指定一个key前缀,
     *没有指定key属性,则默认会使用 KeyGenerator对象创建key
     */
    @Cacheable(value = "tagCache")
    @Override
    public List<Tag> selectTags() {
        return tagMapper.selectList(null);
    }
    
    /**
     * @CacheEvict注解的作用是定义缓存切入点方法,执行此注解描述的方法
     * 时,底层通过AOP方式执行缓存数据的清除操作.
     * 其中,allEntries表示清除指定key所有数据,beforeInvocation用于定义
     * 在何时清除缓存数据,是更新数据库之后还是之前,false表示之后
     */
    @CacheEvict(value = "tagCache",allEntries = true,beforeInvocation = false)
    @Override
    public void insertTag(Tag tag) {
          tagMapper.insert(tag);
    }
    /**
     * 缓存数据时,可以自己指定key,key的值为spring中的el表达式,语法可以打开@Cacheable注解源码进行查看,
     * 这里的#id表示基于id的值作为key
     */
     @Cacheable(value="tagCache",key="#id")
     @Override
     public Tag selectById(Long id){
          return tagMapper.selectById(id);
     }

   /** @CachePut注解描述的方法为缓存切入点方法,系统底层会在执行此方法后,更新缓存数据,
     * 这里更新完数据以后,key为tag对象的id值,值为方法的返回值.
     */
     @CachePut(value = "tagCache",key="#tag.id")
     @Override
      public Tag updateTag(Tag tag){
          tagMapper.updateById(tag);
          return tag;
      }
}

第三步:将Redis中数据key设置为一致状态

package com.jt.blog.damain.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
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.StringRedisSerializer;

import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.time.Duration;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    @Override
    public KeyGenerator keyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target,
                                   Method method,
                                   Object... params) {
                StringBuilder sb=new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append("::");
                sb.append(method.getName());
                for(Object param:params){//方法没有参数就没有这个循环了
                    sb.append(param);
                }
                return sb.toString();
            }
        };
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(60*60))
        .serializeKeysWith(RedisSerializationContext.SerializationPair.
                fromSerializer(new StringRedisSerializer()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.
                fromSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)));

        return RedisCacheManager.builder(connectionFactory).
                cacheDefaults(config).
                transactionAware().
                build();

    }
    @Bean
    @ConditionalOnMissingBean(
            name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object,Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(stringRedisSerializer);
        Jackson2JsonRedisSerializer jsonRedisSerializer= new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator()
                ,ObjectMapper.DefaultTyping.NON_FINAL
                , JsonTypeInfo.As.PROPERTY);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
    @Bean
    public Jackson2JsonRedisSerializer jsonRedisSerializer(){
        Jackson2JsonRedisSerializer jsonRedisSerializer=
                new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(
                PropertyAccessor.GETTER,
                JsonAutoDetect.Visibility.ANY);
        //激活默认类型(存储json时会添加类型信息到json串中)
        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        return jsonRedisSerializer;
    }

}

第四步:修改单元测试类,测试缓存数据一致性.

package com.jt.blog.service;

import com.jt.blog.domain.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class TagServiceTests {
    @Autowired
    private TagService tagService;
    @Test
    void testSelectTags(){
        List<Tag> tags=
        tagService.selectTags();
        System.out.println(tags);
    }
    @Test
     void testInsertTag(){
        Tag tag=new Tag();
        tag.setName("Oracle1");
        tagService.insertTag(tag);
    }
        @Test
    void testSelectById(){
        Tag tag = tagService.selectById(1L);
        System.out.println(tag);
    }
      @Test
    void testUpdateTag(){
        Tag tag=new Tag();
        tag.setId(1L);
        tag.setName("mysql8.0");
        tagService.updateTag(tag);
    }
}

5.在Controller中添加一个本地缓存,减少对远程redis缓存的访问,例如:

package com.jt.blog.controller;

import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@RestController
@RequestMapping("/tag")
public class TagController {
    @Autowired
    private TagService tagService;
    //此对象存哪了?(JVM)
    private List<Tag> tags=new CopyOnWriteArrayList<>();//本地 cache
    @GetMapping
    public  List<Tag> doSelectTags(){
       if(tags.isEmpty()) {
           synchronized (tags) {
               if(tags.isEmpty()) {
                   tags.addAll(tagService.selectTags());//1.redis,2.mysql
               }
           }
       }
       return tags;
    }
}

Controller中本地缓存一致性分析

此次项目案例中,我们在Controller层添加了本地缓存,这个缓存我们也需要考虑其缓存一致性,其相关代码实现如下:

package com.jt.blog.controller;

import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;

@RestController
@RequestMapping("/tag")
public class TagController {
    @Autowired
    private TagService tagService;
    //private List<Tag> tags=new ArrayList<>();
    private List<Tag> tags=new CopyOnWriteArrayList<>();//本地 cache


    @GetMapping("/{id}")
    public Tag doSelectById(@PathVariable("id") Long id){
        //查询本地缓存
        for(Tag t:tags){
            if(t.getId().equals(id)) return t;
        }
        //查询redis,mysql
        return tagService.selectById(id);
    }

    @PostMapping
    public String doInsertTag(Tag tag){
        //向数据库写入数据
        tagService.insertTag(tag);//A
        //更新本地缓存
        tags.add(tag);
        return "insert ok";
    }

    @PutMapping
    public String doUpdateTag(Tag tag){//id=1,name=mysql 8.9
        //向数据库写入数据
        tagService.updateTag(tag);
        //更新本地缓存
        for(Tag t:tags){
            if(t.getId().equals(tag.getId())){
                t.setName(tag.getName());
            }
        }
        return "update ok";
    }


    @GetMapping
    public  List<Tag> doSelectTags(){//B
       if(tags.isEmpty()) {
           synchronized (tags) {
               if(tags.isEmpty()) {
                   tags.addAll(tagService.selectTags());//1.redis,2.mysql
               }
           }
       }
       return tags;
    }
    /**Spring中Bean对象的生命周期方法,对象初始化时执行此方法*/
    @PostConstruct
    public void doInit(){
        doTimerRefreshTask();
    }
    /**Spring中Bean对象的生命周期方法,Bean对象初始化时执行此方法*/
    @PreDestroy
    public void doDestory(){
        //退出定时任务
        timer.cancel();
    }
    private Timer timer;
    //定义刷新任务
    private void doTimerRefreshTask(){
        //构建一个定时任务调度对象
        timer=new Timer();
        //构建一个任务对象
        TimerTask task=new TimerTask() {
            @Override
            public void run() {
                System.out.println("refresh cache");
                tags.clear();
            }
        };
        //执行任务对象(每隔5秒执行一次)
        timer.schedule(task, 5000, 5000);
    }

}

Redis集群链接配置实践

修改项目中的application.yml配置文件,修改redis配置,采用集群方式进行实现,例如:

spring:
  datasource: #默认配置的是HikariDataSource,应用的是HikariCP链接池(HikariPool)
    url: jdbc:mysql:///blog?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
#redis 集群配置
  redis:
    cluster: #redis 集群配置
      nodes: 192.168.126.129:8010,192.168.126.129:8011,192.168.126.129:8012,192.168.126.129:8013,192.168.126.129:8014,192.168.126.129:8015
      max-redirects: 3 #最大跳转次数
    timeout: 5000 #超时时间
    database: 0
    jedis: #连接池
      pool:
        max-idle: 8
        max-wait: 0
#日志配置
logging:
  level:
    com.jt: debug


版权声明:本文为weixin_59868842原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。