Berbagi teknologi

SpringBoot menggunakan Redisson untuk mengoperasikan Redis dan skenario penggunaan sebenarnya

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Kata pengantar

adaSpringBoot menggunakan RedisTemplate dan StringRedisTemplate untuk mengoperasikan Redis, kami memperkenalkan RedisTemplate dan cara SpringBoot mengoperasikan Redis melalui RedisTemplate dan StringRedisTemplate.
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

Jadi karena SrpingBoot dapat mengoperasikan Redis melalui RedisTemplate, mengapa Redisson muncul lagi?Dokumentasi Rddisson berbahasa Mandarin
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. Ikhtisar

Mulai dari spring-boot versi 2.x, spring-boot-data-redis menggunakan klien Lettuce untuk mengoperasikan data secara default.

1.1 Selada

SpringBoot2之后,默认就采用了lettuce。
Adalah klien Redis tingkat lanjut, lapisan komunikasi berbasis peristiwa berdasarkan kerangka Netty untuk sinkronisasi thread-safe, penggunaan asinkron dan reaktif, mendukung cluster, Sentinel, pipeline, dan encoder.
API Lettuce aman untuk thread dan dapat mengoperasikan satu koneksi Lettuce untuk menyelesaikan berbagai operasi. Contoh koneksi (StatefulRedisConnection) dapat diakses secara bersamaan di antara beberapa thread.

1.2 Reddisson

Lapisan komunikasi berbasis peristiwa berdasarkan kerangka Netty, metodenya asinkron, API-nya aman untuk thread, dan dapat mengoperasikan satu koneksi Redisson untuk menyelesaikan berbagai operasi.
Ini mengimplementasikan struktur data Java yang terdistribusi dan dapat diskalakan, tidak mendukung operasi string, dan tidak mendukung fitur Redis seperti pengurutan, transaksi, saluran pipa, dan partisi.
Menyediakan banyak layanan operasi terkait terdistribusi, seperti kunci terdistribusi, koleksi terdistribusi, dan dapat mendukung antrian penundaan melalui Redis.

Meringkaskan: Prioritaskan penggunaan Selada, yang memerlukan fitur terdistribusi tingkat lanjut seperti kunci terdistribusi dan koleksi terdistribusi. Tambahkan Redisson untuk digabungkan dengannya.

2. Spring-Boot mengintegrasikan Redisson

2.1 Memperkenalkan ketergantungan

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

Melihat: Setelah memperkenalkan ketergantungan ini, tidak perlu memperkenalkannya lagispring-boot-starter-data-redis,Ituredisson-spring-boot-starter Ini telah diperkenalkan secara internal dan tidak termasuk klien Redis Luttuce dan Jedis. Oleh karena itu, konfigurasi Luttuce dan Jedis di application.yaml tidak akan berpengaruh.
Masukkan deskripsi gambar di sini

Saat menggunakan Redisson dalam sebuah proyek, kami biasanya menggunakan RedissonClient untuk operasi data. Namun, beberapa teman mungkin menganggap RedissonClient tidak nyaman untuk dioperasikan, atau lebih suka menggunakan RedisTemplate untuk operasi Kategori Templat Ulang.mengacu padaSpringBoot menggunakan RedisTemplate dan StringRedisTemplate untuk mengoperasikan Redis

Ditemukan bahwa setelah proyek memperkenalkan Redisson, pabrik koneksi yang digunakan di bagian bawah RedisTemplate juga merupakan Redisson.
Masukkan deskripsi gambar di sini

2.2 File konfigurasi

Tambahkan informasi konfigurasi redis di application.yaml.

spring:
  data:
    redis:
      mode: master
      # 地址
      host: 30.46.34.190
      # 端口,默认为6379
      port: 6379
      # 密码,没有不填
      password: ''
      # 几号库
      database: 1
      sentinel:
        master: master
        nodes: 30.46.34.190
      cluster:
        nodes: 30.46.34.190
      lettuce:
        pool:
          # 连接池的最大数据库连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 50
          # 连接池中的最小空闲连接
          min-idle: 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

2.3 Kelas konfigurasi

@Configuration
@EnableConfigurationProperties({RedisProperties.class})
public class RedissonConfig {

    private static final String REDIS_PROTOCOL_PREFIX = "redis://";

    @Value("${spring.data.redis.mode}")
    private String redisMode;

    private final RedisProperties redisProperties;

    public RedissonConfig(RedisProperties redisProperties) {
        this.redisProperties = redisProperties;
    }

    /**
     * 逻辑参考 RedissonAutoConfiguration#redisson()
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson(@Autowired(required = false) List<RedissonAutoConfigurationCustomizer> redissonAutoConfigurationCustomizers) throws IOException {
        Config config = new Config();
        config.setCheckLockSyncedSlaves(false);

        int max = redisProperties.getLettuce().getPool().getMaxActive();
        int min = redisProperties.getLettuce().getPool().getMinIdle();

        switch (redisMode) {
            case "master": {
                SingleServerConfig singleConfig = config.useSingleServer()
                        .setAddress(REDIS_PROTOCOL_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort())
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    singleConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                singleConfig.setConnectionPoolSize(max);
                singleConfig.setConnectionMinimumIdleSize(min);
            }
            break;
            case "sentinel": {
                String[] nodes = convert(redisProperties.getSentinel().getNodes());

                SentinelServersConfig sentinelConfig = config.useSentinelServers()
                        .setMasterName(redisProperties.getSentinel().getMaster())
                        .addSentinelAddress(nodes)
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    sentinelConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                sentinelConfig.setMasterConnectionPoolSize(max);
                sentinelConfig.setMasterConnectionMinimumIdleSize(min);
                sentinelConfig.setSlaveConnectionPoolSize(max);
                sentinelConfig.setSlaveConnectionMinimumIdleSize(min);
            }
            break;
            case "cluster": {
                String[] nodes = convert(redisProperties.getCluster().getNodes());

                ClusterServersConfig clusterConfig = config.useClusterServers()
                        .addNodeAddress(nodes)
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    clusterConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                clusterConfig.setMasterConnectionMinimumIdleSize(min);
                clusterConfig.setMasterConnectionPoolSize(max);
                clusterConfig.setSlaveConnectionMinimumIdleSize(min);
                clusterConfig.setSlaveConnectionPoolSize(max);
            }
            break;
            default:
                throw new IllegalArgumentException("无效的redis mode配置");
        }

        if (redissonAutoConfigurationCustomizers != null) {
            for (RedissonAutoConfigurationCustomizer customizer : redissonAutoConfigurationCustomizers) {
                customizer.customize(config);
            }
        }

        return Redisson.create(config);

    }

    private String[] convert(List<String> nodesObject) {
        List<String> nodes = new ArrayList<String>(nodesObject.size());
        for (String node : nodesObject) {
            if (!node.startsWith(REDIS_PROTOCOL_PREFIX)) {
                nodes.add(REDIS_PROTOCOL_PREFIX + node);
            } else {
                nodes.add(node);
            }
        }
        return nodes.toArray(new String[0]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

2.4 Cara menggunakan

@Component
public class RedissonService {
	@Resource
    protected RedissonClient redissonClient;

	public void redissonExists(String key){
		RBucket<String> rBucketValue = redissonClient.getBucket(key, StringCodec.INSTANCE);
		if (rBucketValue.isExists()){
            String value = rBucketValue.get();
            // doSomething
        } else {
           // doElseSomething
        }
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

2.5 Skenario praktis

2.5.1 Kunci terdistribusi

Siswa dengan beberapa pengalaman memikirkan redis ketika mereka menyebutkan penggunaan kunci terdistribusi. Jadi bagaimana redis mengimplementasikan kunci terdistribusi?

Tujuan penting dari kunci terdistribusi adalah untuk menempati lubang di Redis (sederhananya, ini adalah prinsip wortel yang menempati lubang tersebut, ketika proses lain juga ingin menempati lubang tersebut, mereka menemukan bahwa sudah ada lobak besar di dalam lubang tersebut. Anda harus menyerah atau mencoba lagi nanti.

Metode yang umum digunakan untuk kunci terdistribusi

1. Gunakan perintah setNx
Penjelasan rinci dari perintah ini adalah (diatur jika tidak ada). Jika kunci yang ditentukan tidak ada, maka akan disetel (berhasil menempati lubang). Setelah eksekusi bisnis selesai, panggil perintah del untuk menghapus kunci (lepaskan lubang). Misalnya:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

del  distribution-lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Namun ada masalah dengan perintah ini. Jika ada masalah pada logika eksekusi, instruksi del mungkin tidak dapat dijalankan, dan kuncian akan menemui jalan buntu.
Mungkin beberapa teman telah berpikir dengan serius bahwa kita dapat mengatur waktu kedaluwarsa lain untuk kunci ini. Misalnya:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

del  distribution-batch
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Bahkan setelah melakukan ini, masih ada masalah dengan logikanya. Karena setnx dan expired adalah dua perintah, jika server redis terputus antara setnx dan expired, expired tidak akan dijalankan, dan pengaturan waktu kedaluwarsa akan gagal, dan kunci akan gagal. masih diatur. Ini akan menjadi kebuntuan.

Penyebab utamanya adalah kedua perintah setnx dan expired bukanlah perintah atomik.

Dan redis things tidak bisa menyelesaikan masalah setnx dan expired, karena expired tergantung dari hasil eksekusi setnx. Jika setnx tidak berhasil maka expired tidak boleh dijalankan. Hal-hal tidak dapat dinilai jika tidak, sehingga metode setnx+expire untuk mengimplementasikan kunci terdistribusi bukanlah solusi optimal.

2. Gunakan perintah setNx Ex
Masalah setNx+expire telah disebutkan di atas. Untuk mengatasi masalah ini, pejabat Redis memperkenalkan parameter tambahan dari perintah set di versi 2.8, sehingga perintah setnx dan expired dapat dijalankan secara bersamaan. Misalnya:

# set 锁名 值 ex 过期时间(单位:秒) nx
set distribution-lock locked ex 5 nx

// doSomthing

del distribution-lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Logikanya, setNx Ex sudah menjadi solusi optimal dan tidak akan menyebabkan kunci terdistribusi menjadi kebuntuan.

Namun masalah mungkin masih muncul selama perkembangan kita. Mengapa?
Karena kita menetapkan waktu kedaluwarsa untuk kunci ini di awal, bagaimana jika waktu eksekusi logika bisnis kita melebihi waktu kedaluwarsa yang ditetapkan? Akan ada situasi di mana satu thread belum menyelesaikan eksekusi dan thread kedua mungkin menahan kunci terdistribusi.
Oleh karena itu, jika Anda menggunakan kombinasi setNx Ex, Anda harus memastikan bahwa batas waktu penguncian Anda lebih besar daripada waktu eksekusi bisnis setelah penguncian.

3. Gunakan skrip lua + mekanisme ekstensi otomatis anjing penjaga
Saya dapat menemukan banyak solusi ini secara online, jadi saya tidak akan membahas detailnya di sini.

Redisson mengimplementasikan kunci terdistribusi

Perintah setNx dan setNx Ex yang diperkenalkan di atas adalah perintah asli yang disediakan oleh server Redis, dan ada lebih banyak atau lebih sedikit masalah bahwa logika bisnis dari perintah setNx Ex lebih besar daripada batas waktu kunci, Redisson menyediakan internal. Sebuah pengawas dipasang untuk memantau kunci tersebut. Fungsinya adalah untuk terus memperpanjang masa berlaku kunci sebelum instance Redisson ditutup. Secara default, waktu tunggu kunci pemeriksaan pengawas adalah 30 detik (yaitu, perpanjangan adalah 30 detik). Ini juga dapat ditentukan secara terpisah dengan memodifikasi Config.lockWatchdogTimeout. Waktu kedaluwarsa awal kunci juga adalah 30 detik.

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
@Resource
RedissonClient redissonClient;

@GetMapping("/testDistributionLock")
public BaseResponse<String> testRedission(){
    RLock lock = redissonClient.getLock("redis:distributionLock");
    try {
        boolean locked = lock.tryLock(10, 3, TimeUnit.SECONDS);
        if(locked){
            log.info("获取锁成功");
            Thread.sleep(100);
            return ResultUtils.success("ok" );
        }else{
            log.error("获取锁失败");
            return ResultUtils.error(ErrorCode.SYSTEM_ERROR);
        }
    } catch (InterruptedException e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR,"出异常了");
    } finally {
        lock.unlock();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

2.5.2 Pembatasan arus

Kita dihadapkan pada masalah perlunya membatasi aliran antarmuka atau logika bisnis saat ini dalam konkurensi tinggi. Kita dapat menggunakan implementasi RateLimiter berdasarkan Guaua. Faktanya, Redisssion juga memiliki fungsi pembatas arus yang serupa.

RateLimiter disebut pembatasan arus keranjang token. Jenis pembatasan arus ini adalah dengan terlebih dahulu menentukan keranjang token, menentukan berapa banyak token yang dihasilkan dalam jangka waktu tertentu, dan mendapatkan jumlah token yang ditentukan dari keranjang token setiap kali diakses. . Jika Anda memperoleh Jika berhasil, itu ditetapkan sebagai akses yang valid.