redis分布式锁redisson
分布式框架中,普通锁是满足不了业务需求的,分布式锁在分布式框架中不可缺失;比如互联网秒杀、抢优惠券、接口幂等性校验。redis中存在redisson工具包专门处理redis在分布式锁的应用。
java中redisson的实现
<!--添加依赖-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
@Bean
public Redisson redisson() {
// 此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.0.60:6379").setDatabase(0);
// 此为集群模式
/*config.useClusterServers()
.addNodeAddress("redis://192.168.0.61:8001")
.addNodeAddress("redis://192.168.0.62:8002")
.addNodeAddress("redis://192.168.0.63:8003")
.addNodeAddress("redis://192.168.0.61:8004")
.addNodeAddress("redis://192.168.0.62:8005")
.addNodeAddress("redis://192.168.0.63:8006");*/
//还有哨兵模式
return (Redisson) Redisson.create(config);
}
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock() throws InterruptedException {
String lockKey = "product_001";
RLock redissonLock = redisson.getLock(lockKey);
try {
// 加锁,实现锁续命功能
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
}finally {
redissonLock.unlock();
}
return "end";
}
- 使用了jedis.setnx方法加锁,只允许设置一次;删除对应的key进行解锁。
- 每个线程设置的key对应的value值具有唯一性,最后解锁删除value值判断是否为当前加锁的线程,防止了因为当前线程处理时间过长redis本身删除解锁后其它线程进来而导致当前线程解了其他线程的锁,保证了自己加锁自己解锁。
- 增加了锁续命的功能。
Redis Lua脚本
Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:
- 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。这点跟管道类似。
- 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过 redis的批量操作命令(类似mset)是原子的。
- 替代redis的事务功能:redis自带的事务功能很鸡肋,报错不支持回滚,而redis的lua脚本几乎实现了常规的事务功能,支持报错回滚操作,官方推荐如果要使用redis的事务功能可以用redis lua替代。
从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。EVAL命令的格式如下:
EVAL script numkeys key [key ...] arg [arg ...]
script:lua脚本。
numkeys:key的数量。
key:key值,这里为list。
arg:value值,这里为list,与key相对应。
redis中使用lua脚本
//******* lua脚本示例 ********
//模拟一个商品减库存的原子操作
//lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10
jedis.set("product_stock_10016", "15"); //初始化商品10016的库存
String script = " local count = redis.call('get', KEYS[1]) " +
" local a = tonumber(count) " +
" local b = tonumber(ARGV[1]) " +
" if a >= b then " +
" redis.call('set', KEYS[1], count-b) " +
//模拟语法报错回滚操作" bb == 0 " +
" return 1 " +
" end " +
" return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);
注意1:多线程在执行lua脚本的时候,是不存在并发问题的,原因是所有的命令在redis中都会以单线程的形式执行。
注意2:key与value传参都是以list的类型传入,同时下标从1开始而不是0。
lua脚本缺点
redis在高并发执行指令都是串行化,单线程的,如果lua脚本业务逻辑比较复杂执行时间比较长,这会影响redis的性能;如果lua脚本中出现死循环的现象,执行该脚本的redis集群基本上瘫痪了。
注意1:redisson加的锁为可重入锁,前提是同一个线程。
注意2:redisson在加锁后会存在主节点重新选举的情况,这期间可能会导致锁数据丢失,从而出现锁失效的现象。
redis RedLock
为解决主节点重新选举的情况,这期间可能会导致锁数据丢失问题,引入RedLock可解决问题。但是RedLock性能比较差,而且有存在Bug。不推荐使用,建议容忍该问题,若不能容忍,推荐使用zookeeper。
@RequestMapping("/redlock")
public String redlock() throws InterruptedException {
String lockKey = "product_001";
//这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
/**
* 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* 4.尝试获取锁
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}
return "end";
}
高并发场景分布式锁性能提升
把资源库存分成多份,别分存储在不同的集群内,把锁分段分发。比如一个商品库存数量1000个,把库存分为3份,应用key的hash算法把3份库存分发的不同的redis子集群内。