目录
概述
什么是分布式,什么是分布式锁,为什么使用分布式锁
分布式锁应该具备哪些条件
分布式锁应用案例和效率分析
redis实现分布式原理
redis实现分布式锁方法:
第一种加锁(错误),使用setnx,和del(String key)。
第二种加锁(错误),使用setnx,和del和expire。
第三种加锁(错误),使用setnx,和del和getSet。
第四种加锁(错误),使用set,加Lua脚本
第五种加锁(正确),就是给第四种方法加守护线程。实现起来比较复杂,但已有人开发为工具就是Redisson: Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】,
解锁一(错误)
解锁二,(错误)
解锁三,(正确)
小编最近学习redis实现分布式,看了广大网友的博客,学习完后,发现有一半以上的博客redis分布式锁是有错误的。所以写此文章让大家学习正确的redis分布式锁。本文会讲用setnx,和set等五种加锁方法和三种解锁方法,并指出其错误点。
1.什么是分布式?来一张图
当大量用户请求服务器时,如果一个服务器去相应的话,那么这台服务器的压力会特别大,有可能会崩溃,最好的方式是分发给此刻压力较小的服务器去处理。这个服务器可以是单独一台电脑,也可以同一台电脑中的其他进程。但是他们的读数据都是从数据库中读。
2.什么是分布式锁,为什么使用分布式锁,如下场景
假如双11有一个秒杀活动,秒杀100件短袖,同一秒有上万人秒杀,中转服务器将上万个请求散发给处理服务器们,他们处理时最重要的业务(比如删除修改)同一时刻只能一个服务器处理,好比如单机下给一个方法加synchronized,否则就会出现多卖的情况。这时候就要给最重要的业务或方法加分布式锁。保证不会出现多卖情况
场景:假如双11有一个秒杀活动,秒杀100件短袖,同一秒有上万人秒杀,
方法:由于是单机情况,所以我用10000线程(模拟进程)同一时刻去提交抢购短袖。
package com.util.test; public class TenProgress implements Runnable{ Seriver seriver; public TenProgress(Seriver seriver) { this.seriver = seriver; } public void start(){ new Thread(this,"秒杀线程").start(); } @Override public void run() { for(int i = 0; i < 10000; i++){ new Thread(new seckill("短袖"), "秒杀线程" + i).start(); } } class seckill implements Runnable{ String killName; public seckill(String Killname) { this.killName = Killname; } @Override public void run() { seriver.order1(killName); } } } public class Seriver { //商品总数 private static HashMap<String, Integer> product = new HashMap<>(); //订单表 private static HashMap<String, String> orders = new HashMap<>(); //库存表 private static HashMap<String, Integer> stock = new HashMap<>(); static { product.put("短袖", 100); stock.put("短袖", 100); } //开启抢购线程 public void startKill() { TenProgress progress = new TenProgress(this); progress.start(); } public void select_info(String product_id,String id) { System.out.println("限量抢购商品XXX共" + product.get(product_id) + ",现在成功下单" + orders.size() + ",剩余库存" + stock.get(product_id) + "件,当前" + "订单号:" + id); } //加reids分布式锁 public void order1(String product_id) { String value = System.currentTimeMillis() + 2000 + ""; if(redis.lock4(product_id, value)){ if (stock.get(product_id) == 0) { System.out.println(Thread.currentThread().getName() + "没有抢到"); } else { String str = UUID.randomUUID().toString(); orders.put(UUID.randomUUID().toString(), product_id); stock.put(product_id, stock.get(product_id) - 1); select_info(product_id,str); } redis.unlock3(product_id, value); } else{ System.out.println(Thread.currentThread().getName() + "没有抢到"); } } //加synchronized锁 public synchronized void order2(String product_id) { if (stock.get(product_id) <= 0) { System.out.println(Thread.currentThread().getName() + "没有抢到"); //已近买完了 } else { //还没有卖完 try { //模拟操作数据库 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } String id= UUID.randomUUID().toString(); orders.put(id, product_id); stock.put(product_id, stock.get(product_id) - 1); select_info(product_id,id); } } //不加锁 public void order3(String product_id) { if (stock.get(product_id) <= 0) { System.out.println(Thread.currentThread().getName() + "没有抢到"); //已近买完了 } else { //还没有卖完 try { //模拟操作数据库 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } String id= UUID.randomUUID().toString(); orders.put(id, product_id); stock.put(product_id, stock.get(product_id) - 1); select_info(product_id,id); } } }不加锁的情况下,运行情况如下
程序执行时间大概是3s左右,可以看到首先是多卖了,其次库存有100件变为3674件,明显出现混乱,
加synchronized测试如下:
程序完美运行,但是程序执行时间为13s,秒杀活动是秒杀,耗时13秒明显不合理,况且并发量不到1000,如果上万那么就会更慢
加redis分布式锁,测试如下:
程序完美运行,程序执行时间为2s左右,效率最高
1.相关配置,重点不在配置,这里为了让大家看清楚,我就简单的实现其配置。太多反而不利于阅读
public Redis() { //配置连接池 JedisPoolConfig config = new JedisPoolConfig(); //设置最大连接数 config.setMaxTotal(200); //设置最小空闲数 config.setMaxIdle(8); //设置最大等待时间 config.setMaxWaitMillis(1000 * 1000); config.setTestOnBorrow(true); jedispool = new JedisPool(config,"127.0.0.1",6379); }2.必须要了解的方法,尤其是set方法
(1)jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:第一个第二个就不用说了,第三个是填写方式,一个有两个方式,1."nx" 2."xx"。填写"nx",意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作,xx意思SET IF EXIST;第四个参数是时间单位格式,有两种1."ex" "px",ex是秒,px是毫秒,最后一个是过期时间值,如果写1,和第四参数ex组合就是1s后失效。和px组合就是1毫秒后失效
(2)setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。
(3)get(key):获得key对应的value值,若不存在则返回null。
(4)getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。
(5)expire(key, seconds):设置key-value的有效期为seconds秒
(6)del(String key):删除对应的键
3.用redis实现分布式锁原因
(1)在分布式系统下,使用redis集群,所有进程可以操作一个redis缓存,并且redis读写效率远高于sql数据库
(2)redis是单线程工作模式,也就是同一时刻,一个方法只能有一个人执行,且运行速度快
(3)redis可以设置失效时间,以及续加时间。
错误说明:如果某个客户端在获取锁后程序崩溃,或断电则锁永远不会被释放
错误说明:锁有可能永远不会被释放
错误如下:1需要分布式下每个客户端的时间必须同步 2.锁不具有拥有者标识,解锁直接根据key进行解锁,即任何客户端都可以解锁,不安全。 3.jedis.getSet(key, expiretimeStr);这条语句可能被多次执行,虽然获取了锁,但是锁的失效时间可能被其他客户端覆盖
错误点:当客户端A拿到了锁,但是客户端A因为系统卡顿,或业务多原因处理该业务需要8秒时间,假如锁的失效时间设置为5s,5秒后就会自动释放,此时客户端B就可以获取锁,也就是该业务同时两个人在做,这明显会造成不安全行为。
因为网上大多数博客都是这种方法所有小编图解下错误,帮助大家理解
这是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是 5 秒。
如果某些原因导致线程 A 执行的很慢很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
随后,线程 A 接着执行任务,也就是同一时间有 A,B 两个线程在访问代码块,如果此时线程B已经抢光短袖了,但线程A已经下单了,却没有货了。这明显是不合理的情况。
解决方法为:A客户端自己为自己,开启一个守护线程,当获取锁时代码未执行完,续加失效时间。那么客户端宕机了怎么办?啥都不做,它宕机了,他就不能为自己续加时间。时间到锁就过期了,完美解决这一问题。
当线程 A 执行完任务,会显式关掉守护线程
加锁如下:
RLock mylock = redisson.getLock(key); 设置时间 mylock.lock(2, TimeUnit.MINUTES);解锁如下:
RLock mylock = redisson.getLock(key); //释放锁(解锁) mylock.unlock();
