首页 > 解决方案 > Java Spring Boot 会以线程安全的方式在 Redis 中设置和获取值吗?

问题描述

我需要存储经常更改的 ArrayList 的值,并在应用程序崩溃的情况下保留这些值。我正在开发的应用程序已经使用了 Redis 数据库,所以这似乎是一个不错的选择。

下面,我总结了一个 Spring Boot 控制器的最小示例,该控制器连接到 Redis 的 localhost 实例并使用它来存储序列化对象。可以从控制器端点修改该值,也可以通过每 5 秒运行一次的计划作业修改该值。如果您对 执行一系列获取请求localhost:8080/test,您将看到计划的作业一次从 ArrayList 中删除一个项目。

是否有可能错过某个值,或者这里发生的不是线程安全的事情?我担心如果他们尝试修改对象或同时设置 Redis 值,计划的作业可能会与控制器端点所做的更改发生冲突,尤其是在网络速度变慢的情况下,但我不确定这是否真的会一个问题。当它在我的本地主机上运行时,一切似乎都运行良好,但我仍然持怀疑态度。

我阅读了这篇关于线程安全的文章,但它没有回答是否有任何这些东西对于这种特殊情况是必要的。我也知道Redis 读写是 atomic,但我想,如果命令以错误的顺序发送到 Redis 怎么办?

我在想,如果这个实现有问题,那么 Lombok 的 @Syncronized 注释可能对 IO 的抽象方法有用。我感谢任何投入和所花费的时间。

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;

@RestController
public class Example {

    RedisClient redisClient = RedisClient.create("redis://localhost:6379/");
    StatefulRedisConnection<String, String> connection = redisClient.connect();
    RedisCommands<String, String> redis = connection.sync();

    Gson gson = new Gson();

    ArrayList<String> someList = new ArrayList<>();


    public Example() {
        if(redis.exists("somekey") == 1){
            Type collectionType = new TypeToken<Collection<VideoDAO>>(){}.getType();
            someList = new ArrayList<>(gson.fromJson(redis.get("somekey"), collectionType));
        }
    }

    @GetMapping("/test")
    public void addToSomeList(){
        someList.add("sample string");
        redis.set("somekey",gson.toJson(someList));
        System.out.println("New item added. " + someList.size() + " items in array");
    }

    @Scheduled(fixedRate = 5000)
    public void popFromSomeList() {
        if (!someList.isEmpty()) {
            someList.remove(0);
            redis.set("somekey", gson.toJson(someList));
            System.out.println("Item removed. " + someList.size() + " items in array");
        }
    }

}

我正在使用java 1.8。

标签: javamultithreadingspring-bootredis

解决方案


最明显someList的是不是线程安全的,所以即使你忽略 Redis,代码也会被破坏。

假设我们使用Collections.synchronizedList(new ArrayList<>());. 然后仍然不是原子的,尽管这对于功能来说可能add并不重要。您最终可能会得到(例如)以下类型的执行pop

someList.add("sample string");
someList.remove(0);
redis.set("somekey", gson.toJson(someList));
redis.set("somekey", gson.toJson(someList));

并且消息可能会令人困惑,因为它可能显示“添加了新项目。数组中有 4 个项目”、“添加了新项目。数组中有 4 个项目”、“删除了项目。数组中有 4 个项目”,因为添加/删除发生了在打印之前。

因此,对于给定代码(或类似代码)的正确功能,您必须同步方法或使用显式共享锁。有可能以错误的顺序发送命令,但在给定的示例中(假设列表是线程安全的)没有真正危险的机会,因为它只会导致相同数据的重复集。


推荐阅读