1 redis分布式加锁
现在业务并发量越来越大,像传统的数据库操作,已经不能满足要求了,这个时候可以使用redis来提升性能,同时也可以使用redis实现分布式锁。
使用redis实现分布式锁,与java的synchronize类似,只不过是synchronize锁单对象,而分布式锁是锁进程或者线程,同样的它是一个独占锁,一旦被某个线程拿到锁,其他的线程或者进程,只能进行等待后再获取。当线程或者进程使用完毕释放锁后,其他线程才能获取到锁。
下面将基于redis实现一个分布式锁。
代码实现
先看Redis的分布式加锁的实现.
public class RedisLock {
/** 加锁的key */
private static final String LOCK_KEY = "lock.key";
/** 超时时间 */
private static final int TIME_OUT = 1000 * 60;
/** 执行解锁的命令 */
private static final String UNLOCK_COMMAND =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 分布式加锁操作
*
* @param dataId 用于标识唯一的id
* @return 加锁的结果
*/
public boolean lock(String dataId) {
// nx不存在时进行
// px超时时间
SetParams param = SetParams.setParams().nx().px(TIME_OUT);
Jedis connect = RedisConn.getRedisConn();
String result;
try {
result = connect.set(LOCK_KEY, dataId, param);
} finally {
if (connect != null) {
connect.close();
}
}
System.out.println(
" lock thread id :"
+ Thread.currentThread().getId()
+ ",rsp :"
+ result
+ ",compare rsp : "
+ (RedisConn.SUCCESS.equals(result)));
// 检查结果是否设置成功
return RedisConn.SUCCESS.equals(result);
}
AtomicInteger num = new AtomicInteger(0);
/**
* 执行解锁操作
*
* @return true 解锁成功 false 解锁失败
*/
public boolean unlock(String dataId) {
Jedis connect = RedisConn.getRedisConn();
Object result;
try {
result = connect.eval(UNLOCK_COMMAND, 1, LOCK_KEY, dataId);
} finally {
if (connect != null) {
connect.close();
}
}
System.out.println(
" unlock thread id :"
+ Thread.currentThread().getId()
+ ",rsp :"
+ result
+ ",compare rsp : "
+ (RedisConn.RELEASE_SUCCESS.equals(result))
+ "run Num "
+ num.incrementAndGet());
// Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
return RedisConn.RELEASE_SUCCESS.equals(result);
}
}
先说加锁吧:
在redis中加锁的命令是set,设置参数nx表示不存在时创建,px设置加锁的超时时间,这样加锁命令才是原子操作。
注:在很多实现版本中使用的是setnx加expire组合,这个组存着一个问题,那就是客户端执行setnx后宕机了,超时时间是没有被设置的,极端情况将导致分布式锁不可用。
再来说解锁操作吧:
解锁使用是redsi执行lua脚本的功能,来完成解锁的功能。解锁操作的流程是判断当前加锁的id与解锁的id是否一致,如果一致,则可以执行解锁命令del操作。如果不一致,则直接返回。那为什么要使用lua执行命令,而不是使用命令行判断再删除呢?
这两种操作表面看起来达到的效果是一致的。都是先检查对比id是否一致,再执行删除操作。 这是因为检查与删除是两个命令操作,在某些情况下,如果存储的id一致,将导致ABA问题,意外的释放了锁。
Redis 使用单个 Lua 解释器去运行所有脚本,并且 Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。
redis的连接处理.
public class RedisConn {
/** 成功的标识 */
public static final String SUCCESS = "OK";
/** 命令操作操作成功 */
private static final int OPERATOR_OK = 1;
/** 执行操作成功的标识 */
public static final Long RELEASE_SUCCESS = 1L;
/** 密码 */
private static final String PASSWORD = "123456";
private static final JedisPoolConfig CONFIG = new JedisPoolConfig();
/** 连接信息 */
private static final JedisPool POOL = new JedisPool(CONFIG, "localhost");
/**
* 获取连接
*
* @return
*/
public static Jedis getRedisConn() {
Jedis jedis = POOL.getResource();
// 密码操作
String authRsp = jedis.auth(PASSWORD);
if (SUCCESS.equals(authRsp)) {
return jedis;
}
throw new IllegalArgumentException("password error");
}
}
商品
public class Goods {
/** 商品名称 */
private String name;
/** 商品数量操作的key */
private static final String GOODS_NUM = "GOODS.NUM";
/** 使用redis进行分布式锁操作 */
private RedisLock lock;
public Goods(String name, int goodsNum, RedisLock lock) {
this.name = name;
// 设置商品数量
this.updateGoodsNum(goodsNum);
this.lock = lock;
}
/** 商品的库存扣减操作 */
public int minusGoods(int num) {
String dataId = UUID.randomUUID().toString();
int rsp = 0;
boolean lockRsp;
do {
lockRsp = lock.lock(dataId);
try {
// 加锁成功进行业务操作
if (lockRsp) {
// 获取当前的数量
int getGoods = this.getGoodsNum();
// 执行库存的扣除操作
if (getGoods >= num) {
this.updateGoodsNum(getGoods - num);
rsp = num;
}
}
} finally {
// 仅当加锁成功时才执行解锁操作
if (lockRsp) {
lock.unlock(dataId);
}
}
// 当加锁失败时,则继续尝试,不要一直轮训,休息个随机时间,2毫秒,到10毫秒之间
if (!lockRsp) {
int randSleep = ThreadLocalRandom.current().nextInt(2, 10);
currSleep(randSleep);
}
// 当加锁失败时继续
} while (!lockRsp);
return rsp;
}
/**
* 进行设置的库最最新修改操作
*
* @param nums 最新的商品数量
*/
private void updateGoodsNum(int nums) {
Jedis jedis = RedisConn.getRedisConn();
try {
jedis.set(GOODS_NUM, String.valueOf(nums));
} finally {
jedis.close();
}
}
/**
* 获取当前最新的数量
*
* @return
*/
private int getGoodsNum() {
Jedis jedis = RedisConn.getRedisConn();
try {
String value = jedis.get(GOODS_NUM);
if (StringUtils.isNotEmpty(value)) {
return Integer.parseInt(value);
}
} finally {
jedis.close();
}
return 0;
}
/**
* 休眠的时间
*
* @param sleepTIme
*/
private void currSleep(long sleepTIme) {
try {
Thread.sleep(sleepTIme);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 获取商品数量
*
* @return 当前商品的数量
*/
public int getGoods() {
return this.getGoodsNum();
}
}
订单:
public class Orders {
/** 商品服务 */
private Goods goods;
public Orders(Goods goods) {
this.goods = goods;
}
/**
* 创建订单
*
* @return
*/
public boolean createOrder(int num) {
// 执行扣减库存操作
int rsp = goods.minusGoods(num);
return rsp > 0;
}
}
单元测试:
public class TestRedisLock {
@Test
public void useOrder() throws InterruptedException {
int orderNumSum = 800;
RedisLock redisLock = new RedisLock();
Goods goods = new Goods("mac", orderNumSum, redisLock);
// 并发进行下单操作
int maxOrder = 4;
int count = 0;
for (int i = 0; i < orderNumSum / maxOrder; i++) {
CountDownLatch startLatch = new CountDownLatch(maxOrder);
for (int j = 0; j < maxOrder; j++) {
TaskThreadPool.INSTANCE.submit(
() -> {
startLatch.countDown();
Orders instance = new Orders(goods);
instance.createOrder(1);
});
count++;
}
// 执行等待结果
try {
startLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("结束,共运行:" + count + "次");
TaskThreadPool.INSTANCE.shutdown();
Thread.sleep(500);
System.out.println("shutdown status:" + TaskThreadPool.INSTANCE.getPool().isShutdown());
}
}
最后查看控制台的输出
lock thread id :15,rsp :OK,compare rsp : true
lock thread id :18,rsp :null,compare rsp : false
lock thread id :20,rsp :null,compare rsp : false
lock thread id :19,rsp :null,compare rsp : false
lock thread id :16,rsp :null,compare rsp : false
lock thread id :17,rsp :null,compare rsp : false
lock thread id :21,rsp :null,compare rsp : false
lock thread id :14,rsp :null,compare rsp : false
unlock thread id :15,rsp :1,compare rsp : truerun Num 1
lock thread id :15,rsp :OK,compare rsp : true
unlock thread id :15,rsp :1,compare rsp : truerun Num 2
lock thread id :15,rsp :OK,compare rsp : true
unlock thread id :15,rsp :1,compare rsp : truerun Num 3
lock thread id :15,rsp :OK,compare rsp : true
unlock thread id :15,rsp :1,compare rsp : truerun Num 4
lock thread id :14,rsp :OK,compare rsp : true
lock thread id :19,rsp :null,compare rsp : false
lock thread id :15,rsp :null,compare rsp : false
unlock thread id :14,rsp :1,compare rsp : truerun Num 5
lock thread id :14,rsp :OK,compare rsp : true
lock thread id :18,rsp :null,compare rsp : false
unlock thread id :14,rsp :1,compare rsp : truerun Num 6
lock thread id :14,rsp :OK,compare rsp : true
......
lock thread id :18,rsp :null,compare rsp : false
unlock thread id :21,rsp :1,compare rsp : truerun Num 792
结束,共运行:800次
lock thread id :21,rsp :OK,compare rsp : true
unlock thread id :21,rsp :1,compare rsp : truerun Num 793
lock thread id :15,rsp :OK,compare rsp : true
unlock thread id :15,rsp :1,compare rsp : truerun Num 794
lock thread id :20,rsp :OK,compare rsp : true
lock thread id :14,rsp :null,compare rsp : false
unlock thread id :20,rsp :1,compare rsp : truerun Num 795
lock thread id :17,rsp :OK,compare rsp : true
unlock thread id :17,rsp :1,compare rsp : truerun Num 796
lock thread id :16,rsp :OK,compare rsp : true
unlock thread id :16,rsp :1,compare rsp : truerun Num 797
lock thread id :18,rsp :OK,compare rsp : true
unlock thread id :18,rsp :1,compare rsp : truerun Num 798
lock thread id :14,rsp :OK,compare rsp : true
lock thread id :19,rsp :null,compare rsp : false
unlock thread id :14,rsp :1,compare rsp : truerun Num 799
lock thread id :19,rsp :OK,compare rsp : true
unlock thread id :19,rsp :1,compare rsp : truerun Num 800
shutdown status:true
最后检查下redis中是否正确的处理:
D:\java\soft\redis\Redis-x64-5.0.10>redis-cli.exe
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> keys *
1) "GOODS.NUM"
127.0.0.1:6379> get GOODS.NUM
"0"
127.0.0.1:6379>
从结果中可以看到,商品的数量已经正确的扣减到了0。
劣势
锁超时时间的设置,但这个超时时间并没有一个统一的设置,需要根据业务的情况设置锁超时的时间,但是由于业务的复杂性,导致超时时间的粒度不同,如何根据业务匹配一个合适的超时间时间是一个比较麻烦的事情。需要做大量的测试,最终给出一个合理的超时时间。
如果在业务执行过程中,意外的宕机,锁将不能正常的释放,需要加入其他机制来保证。
业务在操作过程中也需要特别的小心,需要在异常的情况下也能正确的释放锁。
引入了redis的组件,维护的成本是必然上升的。
适用场景
相对于数据库来说,redis的并发度已经提升相当大了,可达十几万的并发,提升相当的高。针对大量并发来说,redis是一个抗流量的利器,但由于redis的操作锁的复杂度。操作需要特别小心,尤其是超时间,需要针对业务做大量的测试,确实一个合适的超时时间,过大,导致资源浪费,过小,锁将出现我意外释放,所以需要终合考量。
2 使用redis的cas机制
代码实现
还是以代码来说话吧:
public class Goods {
/** 商品名称 */
private String name;
/** 商品数量操作的key */
private static final String GOODS_NUM = "GOODS.NUM";
/** 执行5次的连续尝试 */
private static final int DEFAULT_MAX_TRY = 5;
/** redis的比较交换操作 */
private static final String REDIS_CAS =
"if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('set', KEYS[1], ARGV[2]) return 1 else return 0 end ";
public Goods(String name, int goodsNum) {
this.name = name;
// 设置商品数量
this.updateGoodsNum(goodsNum);
}
/** 商品的库存扣减操作 */
public int minusGoods(int num) {
int rsp = 0;
int index = 0;
boolean casRsp;
do {
int goodsNum = this.getGoodsNum();
casRsp = this.compareAndSet(goodsNum, goodsNum - num);
System.out.println(
"当前线程:"
+ Thread.currentThread().getId()
+ ",商量数量:"
+ goodsNum
+ ",更新后:"
+ (goodsNum - num)
+ ",结果:"
+ casRsp);
// 当库存更新成功时,则退出操作
if (casRsp) {
rsp = num;
break;
}
// 当加锁失败时,则继续尝试,不要一直轮训,休息个随机时间,5毫秒,到10毫秒之间
if (index > DEFAULT_MAX_TRY) {
int randSleep = ThreadLocalRandom.current().nextInt(5, 10);
currSleep(randSleep);
}
// 当加锁失败时继续
} while (true);
return rsp;
}
/**
* 进行设置的库最最新修改操作
*
* @param num 最新的商品数量
*/
private void updateGoodsNum(int num) {
RedisConn.getRedisConn().set(GOODS_NUM, String.valueOf(num));
}
/**
* 基于比较交换的思想进行数据的更新操作,可以理解为lock free
*
* @param before 之前的数据
* @param after 待修改后的数据
* @return true cas操作成功 false cas操作失败
*/
private boolean compareAndSet(int before, int after) {
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(GOODS_NUM);
values.add(String.valueOf(before));
values.add(String.valueOf(after));
Jedis conn = RedisConn.getRedisConn();
Long casRsp;
try {
casRsp = (Long) conn.eval(REDIS_CAS, keys, values);
} finally {
if (null != conn) {
conn.close();
}
}
return RedisConn.RELEASE_SUCCESS.equals(casRsp);
}
/**
* 获取当前最新的商品数量
*
* @return 当前数量的值
*/
private int getGoodsNum() {
Jedis conn = RedisConn.getRedisConn();
try {
String value = conn.get(GOODS_NUM);
if (StringUtils.isNotEmpty(value)) {
return Integer.parseInt(value);
}
} finally {
if (null != conn) {
conn.close();
}
}
return 0;
}
/**
* 休眠的时间
*
* @param sleepTime 休眠的时间
*/
private void currSleep(long sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 获取商品数量
*
* @return 当前商品的数量
*/
public int getGoods() {
return this.getGoodsNum();
}
}
这是一个商品库操作的类,使用redis执行lua脚本的原子特性,将多个操作封装到一个lua脚本中,以保证执行的原子性。这样相对分布式锁来说,资源的消耗更低,将以前三个操作,直接合并为一个开销,这样就直接减少了60%的操作,直接一次就可完成对应的操作。再加上是无锁的,这样就无用担心锁的释放问题。也不会发生锁长时间释放不掉的问题。
订单:
public class Orders {
/** 商品服务 */
private Goods goods;
public Orders(Goods goods) {
this.goods = goods;
}
/**
* 创建订单
*
* @return
*/
public boolean createOrder(int num) {
// 执行扣减库存操作
int rsp = goods.minusGoods(num);
return rsp > 0;
}
}
redis连接管理类
public class RedisConn {
/** 成功的标识 */
public static final String SUCCESS = "OK";
/** 命令操作操作成功 */
private static final int OPERATOR_OK = 1;
/** 执行操作成功的标识 */
public static final Long RELEASE_SUCCESS = 1L;
/** 密码 */
private static final String PASSWORD = "123456";
private static final JedisPoolConfig CONFIG = new JedisPoolConfig();
/** 连接信息 */
private static final JedisPool POOL = new JedisPool(CONFIG, "localhost");
/**
* 获取连接
*
* @return
*/
public static Jedis getRedisConn() {
Jedis jedis = POOL.getResource();
// 密码操作
String authRsp = jedis.auth(PASSWORD);
if (SUCCESS.equals(authRsp)) {
return jedis;
}
throw new IllegalArgumentException("password error");
}
}
劣势:
由于使用cas机制后,redis只保证当前执行的一个原子性,但针对每次都需要执行成功的场景,就需要不断尝试,直到尝试成功,这样就会导致更多的资源的消耗,所以如果此CAS机制不建议使用到并发度高且每次都需要成功的的应用,这样所导致的资源消耗是加倍的,极端点会跑满CPU,导致不能正常的响应。如果每次都需要成功的并且并发度很高,还是使用锁吧。
由于CAS操作是无锁的,所以代码比有锁的代码更复杂,需要考滤的情况也更多。
适用场景:
相对于分布式锁的redis的方案来说,redis的CAS方案所能承受的并发度也更大,同等数量级的情况下,比分布式锁大约有20%的提升。
redis的CAS机制适用于并发度高,但允许接受结果失败的场景; 也适用于并发度不是特别高,但必须成功的场景。这种场景可使用重试策略来进行结果的保证。