关于Redis分布式锁的简单实现
在当下单机应用部署的线上环境是越来越少了,稍微大一点的应用都是集群部署,特别是当下微服务流行的时候。在集群部署的情况下就涉及到不同机器上的服务同时去处理共享资源,这个共享资源可以是在数据库上 也有可能在缓存上。如果是单机应用 就是一个jvm进程,这个时候我们可以使用jdk提供的锁进行同步,如果集群环境下 就有多个进程的jvm在工作,如果我们使用jdk提供的锁,他只能保证他所属的jvm进程中的处理的请求可以同步,但是和其他jvm进程无法做到同步。这个时候我们就需要依靠第三方工具去生成锁。
目前分布式锁的设计方案比较常用的就是以下三种
1.利用数据库事物的ACID特性
设计方案:给资源表的添加一个额外的字段就是版本好version,每次更新资源之前,先查询当前资源的版本好version,然后进行update操作的时候 带上这个版本号,如果在当前线程更新之前,其他线程修改了资源,同时版本号也会修改。那么当前线程的update操作就更新不了数据 这个时候就相当于拿到了锁。这就是数据库的乐观锁实现。但是因为数据库的磁盘io效率实在是太低了,使用数据库去做分布式锁,会使后台服务的响应变慢,降低后台服务的qps。
2,使用zookeeper的结点特性和事件观察机制
以上两种只是简单介绍一下他的实现原理。本篇我们重点讲的是通过redis实现的分布式锁。分什么redis能够实现分布式锁呢,这得益于redis的设计是一个单线程的内存存储数据库,首先单线程就可以避免并发的数据错误,不管客户端的多大的并发请求,到了redis服务端所有的请求都是单条执行的,不会存在并发执行的情况。同时redis他还是一个内存数据库,内存有什么最大的优势就是他的吞吐能力,那么他获取锁和释放锁的速度就快,这样服务的响应就更加快。
这里有一个我自己写的商品秒杀的一个小demo,通过这个小demo我来分析一下redis分布式锁的实现方式。
package it.chenzk.demo.service;
import it.chenzk.demo.exception.SelfException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
public class SeckillGoodsServie {
@Autowired
private RedisLockServie redisLockServie;
/**
* 国庆活动iphoen11 特价 限量100000份
*/
//模拟的商品表 库存表 订单表
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
{
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123456",10000);
stock.put("123456",10000);
}
private String queryMap(String productId){
return "国庆活动,iphone11特价,限量份"+
products.get(productId)+
" 还剩: "+stock.get(productId)+"份 "+
"该商品成功下单的用户数量是: "+orders.size()+"人";
}
//查询秒杀的商品信息
public String querySecKillProdutInfo(String productId){
return this.queryMap(productId);
}
//秒杀业务执行方法
public void orderProductMockDiffUser(String productId) {
boolean flag = redisLockServie.lock();
if(flag){
//查询该商品库存,如果库存为0结束秒杀活动
int socktNum = stock.get(productId);
if (socktNum == 0) {
throw new SelfException("活动已经结束");
} else {
//模拟下单 不同用户生成的下单id不同
orders.put(UUID.randomUUID().toString(), productId);
socktNum--;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//下单成功减去库存
stock.put(productId, socktNum);
}
}else{
throw new SelfException("当前秒单认识 太多,请稍后在尝试");
}
redisLockServie.unLock();
}
}
上面粘贴的代码是一个简单的秒杀服务services,在这里我把商品信息,库存,和订单表 都用一个静态的hashmap模拟 ,本来想吧这些数据放在数据库 或者 缓存中,但是为了追求代码的简洁就直接用静态变量进行模拟数据库或者缓存
package it.chenzk.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Timer;
@Service
public class RedisLockServie {
@Autowired
private RedisTemplate redisTemplate;
public boolean lock() {
//通过redis的sentx命令设置key-value值 当key-value在redis服务器中不存在返回1,如果可以值存在返回0,其中value值
//设置为value=当前系统时间+加上自定义的超时时间
//sentx是在key值成功代表当前线程获取到锁
//如果sentx设置key值不成功 则这个时候通过get(key) 命令获取value值
//判断value值是不是比当前时间小,如果value值比当前时间小说明key是已经超时的 这个时候我们用getset命令重新设置新的key-value
//并且这个时候是表示当前线程已经获取到锁
//如果当前value值比当前时间大,说明有线程在占用这把锁,同时表示当前线程获取锁失败需要重试
//获取系统当前时间
long currentTime = System.currentTimeMillis();
//通过sentx设置key-value值,如果成代表获取到锁,退出当前方法
boolean flag = redisTemplate.opsForValue().setIfAbsent("redislock", currentTime + 10000 + "");
if (flag) {
//sentx命令设置成功说明当前线程获取到锁了
return true;
} else {
String oldtime = (String) redisTemplate.opsForValue().get("redislock");
//如果oldTtime小于currentTime说明某个线程超时了没有释放锁
//这个时候当前线程可以尝试去获取锁
if (!StringUtils.isEmpty(oldtime)&&Long.parseLong(oldtime) < currentTime) {
//通过getset命令去取代 redis中的value值,返回上次的value值
//如果当前返回的value值和上一步get方法返回的value值是一样的说明
//在上一部的get命令和这一步的getset命令过程中 没有其他线程尝试去获取锁
String oldtime2 = (String) redisTemplate.opsForValue().getAndSet("redislock", currentTime + 10000+"");
if (oldtime.equals(oldtime2)) {
return true;
}
}
}
return false;
}
public void unLock() {
String oldTime = (String) redisTemplate.opsForValue().get("redislock");
if (!StringUtils.isEmpty(oldTime)) {
redisTemplate.opsForValue().getOperations().delete("redislock");
}
}
}
上面这段代码是我自己实现的redis分布式锁服务,这里我来讲解一下这段代码的设计原理。在这段代码中用到了三个redis命令 sentx、get、getset。
- sentx命令的作用就是sentx(key,value) 他的意思就是如果redis中已经存在key,那么直接返回-1 不会去替换这个value值,如果redis中还没有存在这个key 那么就把这个key-value键值对存入redis中。有同学问如果客户端并发的执行很多sentx(key,value)命令,会不会造成好几个都成功返回?这完全可以放心,因为redis是单线程的 ,不管客户端并发执行多少个sentx(key,value),在redis后台服务中,只能一个一个去执行,所以当第一个sentx(key-value)执行成功后,其他的都会执行失败。所以在获取redis锁的代码中 boolean flag = redisTemplate.opsForValue().setIfAbsent(“redislock”, currentTime + 10000 + “”);如果flag是true,说明这个sentx命令执行成功了,就代表执行这条sentx命令的线程获取到redis锁。这里要说一下value值设计成当前时间+超时时间 是为了防止某个线程在处理业务超时了或者异常了 然后造成死锁。
- get命令get(key)就是获取当前key对应的value值,若果在第一步中sentx(key-value)返回失败,说明其他线程已经获取到锁了,这个时候我们通过get命令拿到这个value值去判断这个value值有没有超时,如果没有超时说明某个线程依然持有锁在继续执行业务方法。假如超时了,那么我们就进入第三部
- getset(key,value) 这个命令的作用是设置新的value值返回旧的value值,当第二部是超时的话,那么我们就设置新的“当前时间+超时时间“,这个时候返回的旧value值要去和第二部的get方法返回的value值要进行对比,如果相等的话,那么说明在第二步的get 命令 和 第三步中的getset命令执行的过程中,没有其他线程获取到锁,这个时候我们就相当于获取到了锁。
package it.chenzk.demo.controller;
import it.chenzk.demo.service.SeckillGoodsServie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@Controller
public class SceKillGoodsController {
@Autowired
private SeckillGoodsServie seckillGoodsServie;
static {
HashMap<String,String> hashMap = new HashMap<String,String>();
for(int i=0;i<100;i++){
hashMap.put(i+"","chenzk"+i);
}
}
@GetMapping("/query/{productId}")
@ResponseBody
public String query(@PathVariable("productId") String productId){
return seckillGoodsServie.querySecKillProdutInfo(productId);
}
@GetMapping("/order/{productId}")
@ResponseBody
public String secKill(@PathVariable("productId") String procuctId){
seckillGoodsServie.orderProductMockDiffUser(procuctId);
return seckillGoodsServie.querySecKillProdutInfo(procuctId);
}
}
这个是我用ab压力压力500个请求,10个并发条件下的秒杀情况,对比库存和下单数量 加起来刚好是原库存10000。说明这个redis锁是成功实现了共享资源的同步