使用 Spring Boot + Redis 协同打造延时双删实战指南!

原创 编程疏影 路条编程 2024年08月10日 08:00 河北

图片

使用 Spring Boot + Redis 协同打造延时双删实战指南!

在当今的软件开发领域,数据的一致性是一个至关重要的问题。当我们使用Redis作为缓存时,如何确保Redis和数据库之间的数据一致性是一个常见的挑战。本文将介绍如何使用Spring Boot和Redis来协同实现延时双删策略,以解决在多线程并发情况下数据库与Redis数据不一致的问题。

业务场景

在多线程并发的环境中,假设有两个数据库修改请求。为了确保数据库与 Redis 数据的一致性,修改请求的实现需要在修改数据库后,级联修改 Redis 中的数据。例如,请求一由 A 修改数据库数据,B 修改 Redis 数据;请求二则由 C 修改数据库数据,D 修改 Redis 数据。在并发情况下,可能会出现 A→C→D→B 的执行顺序。需要理解的是,线程并发执行多组原子操作时,执行顺序可能存在交叉现象。

此时可能出现的问题是,A 修改数据库的数据最终保存到了 Redis 中,而 C 在 A 之后也修改了数据库数据,这就导致 Redis 中的数据与数据库数据不一致。在后续的查询过程中,可能会长时间先查询 Redis,从而出现查询到的数据并非数据库中的真实数据的严重问题。

为了解决这个问题,我们采用了延时双删策略。具体来说,就是在更新数据库后,先删除 Redis 中的数据,然后经过一段时间的延迟(例如 500 毫秒),再删除一次 Redis 中的数据。这样可以确保在第二次删除 Redis 之前,数据库的更新操作已经完成,从而避免了数据不一致的情况。

代码实践

引入Redis和Spring Boot AOP依赖
在pom.xml文件中,添加以下依赖:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>spring-boot-redis-delay-delete</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <name>spring-boot-redis-delay-delete</name>
  <description>Spring Boot + Redis Delay Delete Example</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
  </parent>

  <properties>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
     </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

编写自定义AOP注解和切面
创建一个自定义注解@DelayDelete,用于标记需要延时双删的方法。然后,创建一个切面类DelayDeleteAspect,用于实现延时双删的逻辑。

自定义注解@DelayDelete

package com.icoderoad.delaydelete.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DelayDelete {
    int delayMillis();
}

切面类DelayDeleteAspect

package com.icoderoad.delaydelete.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import com.icoderoad.delaydelete.annotation.DelayDelete;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class DelayDeleteAspect {

    private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

    @Around("@annotation(delayDelete)")
    public Object around(ProceedingJoinPoint joinPoint, DelayDelete delayDelete) throws Throwable {
        Object result = joinPoint.proceed();

        int delayMillis = delayDelete.delayMillis();
        executorService.schedule(() -> {
            // 执行第二次删除 Redis 数据的逻辑
        }, delayMillis, TimeUnit.MILLISECONDS);

        return result;
    }
}

application.yml
在application.yml文件中,配置Redis的连接信息。

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456

user.sql脚本
创建一个user表,用于存储用户信息。

CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `username` varchar(50) DEFAULT NULL,
    `password` varchar(50) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

RedisConfig 配置类

package com.icoderoad.config;  

import org.springframework.beans.factory.annotation.Value;
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.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Value("${redis.password}")
    private String redisPassword;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisHost, redisPort);
        configuration.setPassword(redisPassword);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

UserController
创建一个UserController类,用于处理用户相关的请求。

package com.icoderoad.user.controller;

import org.springframework.web.bind.annotation.*;

import com.icoderoad.user.service.UserService;

@RestController
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public User getUser(@PathVariable int id) {
        return userService.getUser(id);
    }

    @PostMapping
    public void addUser(@RequestBody User user) {
        userService.addUser(user);
    }

    @PutMapping("/{id}")
    public void updateUser(@PathVariable int id, @RequestBody User user) {
        userService.updateUser(id, user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable int id) {
        userService.deleteUser(id);
    }
}

UserService
创建一个UserService类,用于处理用户相关的业务逻辑。

package com.icoderoad.user.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.icoderoad.delaydelete.annotation.DelayDelete;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @DelayDelete(delayMillis = 500)
    public User getUser(int id) {
        // 从Redis中获取用户信息
        User user = (User) redisTemplate.opsForValue().get("user:" + id);

        // 如果Redis中没有用户信息,则从数据库中获取
        if (user == null) {
            user = userMapper.getUser(id);

            // 将用户信息存入Redis
            redisTemplate.opsForValue().set("user:" + id, user);
        }

        return user;
    }

    @Transactional
    public void addUser(User user) {
        userMapper.addUser(user);

        // 将用户信息存入Redis
        redisTemplate.opsForValue().set("user:" + user.getId(), user);
    }

    @Transactional
    public void updateUser(int id, User user) {
        userMapper.updateUser(id, user);

        // 第一次删除 Redis 中的用户信息
        redisTemplate.delete("user:" + id);

    }

    @Transactional
    public void deleteUser(int id) {
        userMapper.deleteUser(id);

        // 第一次删除 Redis 中的用户信息
        redisTemplate.delete("user:" + id);
    }
}

@DelayDelete是一个自定义注解,用于标记需要延时双删的方法。它有一个delayMillis属性,用于指定延时的时间(以毫秒为单位)。在上述示例中,我们将delayMillis设置为 500,表示在执行方法后,需要等待 500 毫秒才能执行延时双删操作。

使用@DelayDelete注解的方法,在执行时会先执行方法本身的逻辑,然后等待指定的延时时间,最后再执行延时双删操作。这样可以确保在数据库更新完成后,再删除 Redis 中的数据,从而避免数据不一致的问题。

前端代码:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户管理</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>

<h2>用户列表</h2>
<ul id="userList">
    <li th:each="user : ${users}" th:text="${user.name}" data-id="${user.id}">
        <button onclick="editUser(${user.id})">编辑</button>
        <button onclick="deleteUser(${user.id})">删除</button>
    </li>
</ul>

<h2>添加用户</h2>
<form th:action="@{/user}" method="post">
    <label for="name">姓名:</label><input type="text" id="name" name="name" />
    <button type="submit">添加</button>
</form>

<h2>编辑用户</h2>
<form id="editForm" th:action="@{/user}" method="put" style="display: none;">
    <input type="hidden" id="editUserId" name="id" />
    <label for="editName">姓名:</label><input type="text" id="editName" name="name" />
    <button type="submit">保存</button>
</form>

<script>
    $(document).ready(function() {
        // 获取用户列表
        $.get('/users', function(data) {
            $('#userList').html('');
            for (let user of data) {
                $('#userList').append('<li>' + user.name + 
                    '<button onclick="editUser(' + user.id + ')">编辑</button>' + 
                    '<button onclick="deleteUser(' + user.id + ')">删除</button>' + 
                    '</li>');
            }
        });

        // 提交添加用户表单
        $('form').submit(function(e) {
            e.preventDefault();
            let name = $('#name').val();
            $.post('/user', {name: name}, function() {
                $('#name').val('');
                $.get('/users', function(data) {
                    $('#userList').html('');
                    for (let user of data) {
                        $('#userList').append('<li>' + user.name + 
                            '<button onclick="editUser(' + user.id + ')">编辑</button>' + 
                            '<button onclick="deleteUser(' + user.id + ')">删除</button>' + 
                            '</li>');
                    }
                });
            });
        });

        // 编辑用户
        function editUser(userId) {
            $.get('/user/' + userId, function(user) {
                $('#editUserId').val(user.id);
                $('#editName').val(user.name);
                $('#editForm').show();
            });
        }

        // 提交编辑用户表单
        $('#editForm').submit(function(e) {
            e.preventDefault();
            let id = $('#editUserId').val();
            let name = $('#editName').val();
            $.ajax({
                url: '/user/' + id,
                type: 'PUT',
                data: {name: name},
                success: function() {
                    $('#editForm').hide();
                    $.get('/users', function(data) {
                        $('#userList').html('');
                        for (let user of data) {
                            $('#userList').append('<li>' + user.name + 
                                '<button onclick="editUser(' + user.id + ')">编辑</button>' + 
                                '<button onclick="deleteUser(' + user.id + ')">删除</button>' + 
                                '</li>');
                        }
                    });
                }
            });
        });

        // 删除用户
        function deleteUser(userId) {
            if (confirm('确定要删除该用户吗?')) {
                $.ajax({
                    url: '/user/' + userId,
                    type: 'DELETE',
                    success: function() {
                        $.get('/users', function(data) {
                            $('#userList').html('');
                            for (let user of data) {
                                $('#userList').append('<li>' + user.name + 
                                    '<button onclick="editUser(' + user.id + ')">编辑</button>' + 
                                    '<button onclick="deleteUser(' + user.id + ')">删除</button>' + 
                                    '</li>');
                            }
                        }
                    );
                }
            );
        }
    });
</script>
</body>
</html>

测试验证

  1. ID=10,新增一条数据
    通过调用addUser方法新增一个用户,数据会同时存储到数据库和 Redis 中。

  2. 第一次查询数据库,Redis 会保存查询结果
    调用getUser方法获取用户信息,若 Redis 中不存在,则从数据库获取并保存到 Redis 中。

  3. 第一次访问 ID 为 10
    通过getUser方法获取用户信息,直接从 Redis 中获取。

  4. 第一次访问数据库 ID 为 10,将结果存入 Redis
    在第一次从数据库获取用户信息后,将其存入 Redis 以便后续快速访问。

  5. 更新 ID 为 10 对应的用户名(验证数据库和缓存不一致方案)
    调用updateUser方法更新用户信息,并执行第一次删除 Redis 中的数据。

  6. 采用第二次删除
    在经过设定的延时时间后,进行第二次 Redis 数据删除,确保数据一致性。

通过以上的实现和测试,我们成功地使用 Spring Boot 和 Redis 协同实现了延时双删策略,有效地解决了数据库与 Redis 数据不一致的问题,提高了系统的稳定性和可靠性。在实际应用中,大家可以根据具体的业务需求调整延时时间和相关逻辑,以达到最佳的性能和数据一致性效果。希望本文能够为大家在解决类似数据一致性问题时提供有价值的参考和帮助。

今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。

图片

图片

图片

图片

作者:路条编程(转载请获本公众号授权,并注明作者与出处)

应用程序24

应用程序 · 目录

上一篇使用 Spring Boot 实现动态加载 jar 包,动态配置功能太赞了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值