Rumah >Java >javaTutorial >Redis mengedarkan kunci|Lima pelan evolusi daripada gangsa kepada berlian
Kandungan utama artikel ini adalah seperti berikut:
Pertama sekali, mari kita semak semula masalah local lock🜎,🜎sewa tempatan: 🜎 soalan dibahagikan kepada empat perkhidmatan mikro Serve. Apabila permintaan bahagian hadapan masuk, mereka akan dimajukan ke perkhidmatan mikro yang berbeza. Jika bahagian hadapan menerima permintaan 10 W, dan setiap perkhidmatan mikro menerima permintaan 2.5 W, jika cache gagal, setiap perkhidmatan mikro dikunci apabila mengakses pangkalan data, melalui kunci (synchronzied
or kunci
) untuk mengunci sumber benangnya sendiri untuk menghalang pecahan cache
.
Ini ialah locallocking
, dalam Diedarkan
akan menyebabkan masalah ketidakkonsistenan data: contohnya, selepas perkhidmatan A memperoleh data, ia mengemas kini kunci cache =100 dan perkhidmatan B Ia tidak dihadkan oleh kunci perkhidmatan A dan secara serentak mengemas kini kunci cache = 99. Keputusan akhir mungkin 99 atau 100, tetapi ini adalah keadaan yang tidak diketahui dan tidak konsisten dengan hasil yang dijangkakan. Carta alir adalah seperti berikut: synchronzied
或 lock
)来锁住自己的线程资源,从而防止缓存击穿
。
这是一种本地加锁
的方式,在分布式
. boleh mengaksesnya. Anda perlu menunggu urutan pertama melepaskan sumber kunci sebelum meneruskan pelaksanaan. Kes dalam hidup
Mari kita lihat prinsip asas kunci teragih, seperti yang ditunjukkan dalam rajah di bawah:
锁
,所有并发线程比作人
Mari kita analisa kunci yang diedarkan dalam gambar di atas:
Penjelasan vernakular: Semua urutan yang diminta pergi ke tempat yang sama"Occupy the pit"
, jika terdapat pit, logik perniagaan akan dilaksanakan Jika tiada pit, benang lain perlu melepaskan "pit". Lubang ini kelihatan kepada semua utas Anda boleh meletakkan lubang ini dalam cache atau pangkalan data Redis Artikel ini membincangkan cara menggunakan Redis untuk melakukan "Lubang teragih"
. “占坑”
,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放“坑位”。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库,这篇讲的就是如何用 Redis 做“分布式坑位”
。
Redis 作为一个公共可访问的地方,正好可以作为“占坑”的地方。
用 Redis 实现分布式锁的几种方案,我们都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况。
我们来看下这个命令,SETNX
是set If not exist
的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。
在 Redis 命令行中是这样执行的:
set <key> <value> NX
我们可以进到 redis 容器中来试下 SETNX
SETNX
ialah Pendek untuk set Jika tidak wujud
. Ini bermakna apabila kunci tidak wujud, tetapkan nilai kunci dan apabila ia wujud, jangan lakukan apa-apa. 🎜🎜Ini adalah cara ia dilaksanakan dalam baris arahan Redis: 🎜docker exec -it <容器 id> redis-cli🎜Kita boleh masuk ke dalam bekas redis dan mencubanya
SETNX
arahan. 🎜🎜Masukkan bekas dahulu: 🎜docker exec -it <容器 id> redis-cli
然后执行 SETNX 命令:将 wukong
这个 key 对应的 value 设置成 1111
。
set wukong 1111 NX
返回 OK
,表示设置成功。重复执行该命令,返回 nil
表示设置失败。
我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。
我们来看下流程图:
代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent
。
setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。
// 1.先抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123"); if(lock) { // 2.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 3.解锁 redisTemplate.delete("lock"); return typeEntityListFromDb; } else { // 4.休眠一段时间 sleep(100); // 5.抢占失败,等待锁释放 return getTypeEntityListByRedisDistributedLock(); }
一个小问题:那为什么需要休眠一段时间?
因为该程序存在递归调用,可能会导致栈空间溢出。
Gangsa dipanggil gangsa kerana ia adalah yang paling asas dan pasti akan menyebabkan banyak masalah.
Bayangkan adegan keluarga: Pada waktu malam, Xiao Kong membuka kunci pintu seorang diri dan memasuki bilik, menghidupkan lampu?, dan kemudian tiba-tiba Kuasa terputus
. Xiao Kong ingin membuka pintu dan keluar, tetapi tidak dapat menjumpai kedudukan kunci pintu. Kemudian Xiao Ming tidak dapat masuk, dan juga tidak dapat sesiapa di luar. 断电
了,小空想开门出去,但是找不到门锁位置,那小明就进不去了,外面的人也进不来。
从技术的角度看:setnx 占锁成功,业务代码出现异常或者服务器宕机,没有执行删除锁的逻辑,就造成了死锁
。
那如何规避这个风险呢?
设置锁的自动过期时间
,过一段时间后,自动删除锁,这样其他线程就能获取到锁了。
上面提到的青铜方案会有死锁问题,那我们就用上面的规避风险的方案来设计下,也就是我们的白银方案。
还是生活中的例子:小空开锁成功后,给这款智能锁设置了一个沙漏倒计时⏳
deadlock
. Masa tamat tempoh automatik
, selepas tempoh masa, kunci dipadamkan secara automatik , jadi benang lain boleh memperoleh kunci. 🎜kira detik jam pasir⏳
, selepas jam pasir selesai, kunci pintu secara automatik Terbuka. Walaupun terdapat gangguan bekalan elektrik secara tiba-tiba di dalam bilik, kunci akan terbuka secara automatik selepas beberapa ketika dan orang lain boleh masuk. 🎜🎜🎜4.2 Skema Teknikal 🎜🎜🎜Perbezaan daripada larutan gangsa ialah selepas kunci berjaya diduduki, masa luput kunci ditetapkan. Seperti yang ditunjukkan dalam gambar di bawah: 🎜清理 redis key 的代码如下
// 在 10s 以后,自动清理 lock redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
完整代码如下:
// 1.先抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123"); if(lock) { // 2.在 10s 以后,自动清理 lock redisTemplate.expire("lock", 10, TimeUnit.SECONDS); // 3.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 4.解锁 redisTemplate.delete("lock"); return typeEntityListFromDb; }
白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:
因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。
所以和青铜方案有一样的问题:锁永远不能过期。
上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。
原子性:多条命令要么都成功执行,要么都不执行。
将两步放在一步中执行:占锁+设置锁过期时间。
Redis 正好支持这种操作:
# 设置某个 key 的值并设置多少毫秒或秒 过期。 set <key> <value> PX <多少毫秒> NX 或 set <key> <value> EX <多少秒> NX
然后可以通过如下命令查看 key 的变化
ttl <key>
下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。
# 设置 key=wukong,value=1111,过期时间=5000ms set wukong 1111 PX 5000 NX # 查看 key 的状态 ttl wukong
执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。
黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:
设置 lock
的值等于 123
,过期时间为 10 秒。如果 10
秒 以后,lock 还存在,则清理 lock。
setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);
我们还是举生活中的例子来看下黄金方案的缺陷。
123
. 123
。123
,并设置了过期时间 10 秒
。产生了冲突
。15 s
后,完成了任务,此时 用户 B 还在执行任务。123
123
, dan tetapkan masa tamat10 saat
. Konflik yang Dihasilkan
. 15 s
kemudian, tugasan selesai dan pengguna B ialah masih dalam misi. 123
lock. 🎜🎜🎜🎜Pengguna B masih menjalankan tugas dan mendapati kunci telah dibuka. 🎜🎜🎜🎜Pengguna B sangat marah: 🎜Saya belum menyiapkan tugasan lagi, kenapa kunci dibuka? 🎜🎜🎜🎜🎜5.4.3 Pengguna C merampas kunci🎜🎜🎜🎜🎜🎜🎜Selepas kunci pengguna B dibuka secara aktif oleh A, A keluar dari bilik semasa B masih menjalankan tugas.Daripada kes di atas, kita boleh tahu bahawa kerana masa yang diperlukan untuk pengguna A untuk memproses tugas adalah lebih besar daripada masa untuk kunci dibersihkan secara automatik (dikunci), jadi selepas kunci dibuka secara automatik, pengguna lain telah mendahului kunci. Apabila pengguna A menyelesaikan tugas, dia akan membuka kunci yang dirampas oleh pengguna lain secara aktif.
Kenapa kunci orang lain dibuka di sini? Disebabkan nombor kunci semua dipanggil “123”
,用户 A 只认锁编号,看见编号为 “123”
, kunci dibuka Akibatnya, kunci pengguna B dibuka Pada masa ini, pengguna B belum menyelesaikan tugas, jadi sudah tentu dia marah.
Kecacatan pelan emas di atas juga boleh diselesaikan dengan mudah. Adalah lebih baik untuk menetapkan nombor yang berbeza untuk setiap kunci~. gambar di bawah Ia menunjukkan bahawa kunci yang didahulukan oleh B berwarna biru, yang berbeza daripada kunci hijau yang didahulukan oleh A. Dengan cara ini ia tidak akan dibuka oleh A.
Membuat animasi untuk memudahkan pemahaman:
// 1.生成唯一 id String uuid = UUID.randomUUID().toString(); // 2. 抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS); if(lock) { System.out.println("抢占成功:" + uuid); // 3.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 4.获取当前锁的值 String lockValue = redisTemplate.opsForValue().get("lock"); // 5.如果锁的值和设置的值相等,则清理自己的锁 if(uuid.equals(lockValue)) { System.out.println("清理锁:" + lockValue); redisTemplate.delete("lock"); } return typeEntityListFromDb; } else { System.out.println("抢占失败,等待锁释放"); // 4.休眠一段时间 sleep(100); // 5.抢占失败,等待锁释放 return getTypeEntityListByRedisDistributedLock(); }
上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。
时刻:0s。线程 A 抢占到了锁。
时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。
时刻:10s。锁自动过期。
时刻:11s。线程 B 抢占到锁。
时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。
时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。
那如何规避这个风险呢?钻石方案登场。
上面的线程 A 查询锁和删除锁的逻辑不是原子性
的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。
如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。
那如何用脚本进行删除呢?
我们先来看一下这段 Redis 专属脚本:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。
那么这段脚本怎么在 Java 项目中执行呢?
分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。
// 脚本解锁 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
上面的代码中,KEYS[1] 对应“lock”
,ARGV[1] 对应 “uuid”
,含义就是如果 lock 的 value 等于 uuid 则删除 lock。
而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。
那钻石方案是不是就完美了呢?有没有更好的方案呢?
下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。
Artikel ini memanjangkan masalah kunci teragih melalui masalah kunci tempatan. Kemudian ia memperkenalkan lima penyelesaian kunci yang diedarkan, dan menerangkan penambahbaikan penyelesaian yang berbeza daripada cetek kepada dalam.
Daripada evolusi berterusan penyelesaian di atas, kami tahu di mana situasi abnormal mungkin wujud dalam sistem dan cara mengendalikannya dengan lebih baik.
Dengan analogi, model pemikiran yang berkembang ini juga boleh diaplikasikan kepada teknologi lain.
Berikut meringkaskan kelemahan dan penambahbaikan lima penyelesaian di atas.
Penyelesaian Gangsa:
Penyelesaian Perak:
Golden Plan:
Penyelesaian platinum:
Pelan Berlian:
The King Plan, jumpa lagi dalam artikel seterusnya~
Atas ialah kandungan terperinci Redis mengedarkan kunci|Lima pelan evolusi daripada gangsa kepada berlian. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!