单机锁
// 单机加锁可以,但是分布式系统中会出现超卖现象 1.0
public String sale() {
private Lock lock = new ReentrantLock();
String retMessage = "";
lock.lock();
try {
// 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+ port);
} else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
lock.unlock();
}
return retMessage+"\t"+"服务端口号"+port;
}
上述简单的例子的背景条件是在下完订单后去扣减库存的过程中
出现的问题:单机加锁会出现超卖现象,超卖指超出卖出商品的数量,本来100个线程进来,应该消耗100个商品,减少100个库存,但由于订单模块在不同的虚拟机,普通的加锁并不能满足需求,他们可能同时拿到相同的库存数量,然后减1,最后导致实际商品已经卖完,但库存显示还有
在单机环境下,可以使用synchronized或Lock来实现
但是在分布式系统中,因为竞争的线程可能不在同一个节点上同一个ivm中)所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zokeeper来构建)
不同进程ivm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻寒当前想要运行的线程
引入分布式锁,谁在redis上获取到锁,谁才能去访问库存
接下来的代码将从各个版本逐步升级,相关解说请看代码注释
2.0 引入分布式锁,递归重试可能会导致stackoverflowerror,另外,高并发唤醒后推荐用while判断而不是if
// 2.0 引入分布式锁,递归重试可能会导致stackoverflowerror,另外,高并发唤醒后推荐用while判断而不是if
public String sale() {
String retMessage = "";
String key = "lazyRedisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// 分布式锁
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
// 抢不到锁 递归重试
if (!flag) {
//暂停20毫秒,进行递归重试.....
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
} else {
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:" + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号" + port);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
// 扣减成功后要删除锁,其他线程才能进来
stringRedisTemplate.delete(key);
}
}
return retMessage + "\t" + "服务端口号" + port;
}
升级V3.0
//3.0 使用自旋并加上过期时间
// 缺点: 由于加了过期时间,A线程加锁进来,但处理业务用了32s,这时锁过期了,
// B线程进来,也做同样的操作,执行到第2s的时候,A线程处理完后删除锁,这时删除的是B线程的锁,误删了
public String sale() {
String retMessage = "";
String key = "lazyRedisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// 不使用递归 改用自旋 抢不到锁就等待一会然后重试
// 并且为了如果在删除锁的时候宕机导致key删除不了,需要加个过期时间
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)) {
//暂停20毫秒,进行递归重试.....
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:" + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号" + port);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
// 扣减成功后要删除锁,其他线程才能进来
stringRedisTemplate.delete(key);
}
return retMessage + "\t" + "服务端口号" + port;
}
升级 V4.0
// 4.0 改进了误删操作 但是这个操作不是原子性的
public String sale() {
String retMessage = "";
String key = "lazyRedisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// 不使用递归 改用自旋 抢不到锁就等待一会然后重试
// 并且为了如果在删除锁的时候宕机导致key删除不了,需要加个过期时间
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停20毫秒,进行递归重试.....
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:" + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号" + port);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
//改进点,只能删除属于自己的key,不能删除别人的
// 判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)) {
stringRedisTemplate.delete(key);
}
}
return retMessage + "\t" + "服务端口号" + port;
}
升级 V5.0
// 5.0 使用lua脚本改进删除操作,不满足可重入性
public String sale() {
String retMessage = "";
String key = "lazyRedisLock";
String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// 不使用递归 改用自旋 抢不到锁就等待一会然后重试
// 并且为了如果在删除锁的时候宕机导致key删除不了,需要加个过期时间
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
//暂停20毫秒,进行递归重试.....
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:" + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号" + port);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
"then return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript(script,Boolean.class), Arrays.asList(key),uuidValue);
}
return retMessage + "\t" + "服务端口号" + port;
}
分布锁需要具备特征:独占性,高可用,防死锁,不乱抢,重入性
上述基本满足,但还没有考虑到可重入性问题,只是使用setnx
这个来实现分布式锁,真正要考虑到分布式锁需要使用hset
这种数据结构,以下代码将自研一把分布式锁,并考虑可重入性
要自研一把锁,那么
1、首先必须实现Lock接口并实现其中的方法
2、考虑可重入性
3、考虑续期问题
4、考虑可扩展性,使用设计模式的工厂模式,可以根据我们的需求使用不同的分布式锁,但以下代码是以redis分布式锁为例
建造一个工厂
/**
* 使用工厂模式 根据不同的参数 调用不同的分布式锁
*
*/
@Component
public class DistributedLockFactory {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuid;
public DistributedLockFactory() {
this.uuid = IdUtil.simpleUUID();
}
public Lock getDistributedLock(String lockType) {
if (lockType == null) return null;
if (lockType.equalsIgnoreCase("REDIS")) {
this.lockName = "lazyRedisLock";
return new RedisDistributedLock(stringRedisTemplate, lockName, uuid);
} else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
this.lockName = "lazyZookeeperLockNode";
//TODO zookeeper版本的分布式锁
return null;
} else if (lockType.equalsIgnoreCase("MYSQL")) {
//TODO MYSQL版本的分布式锁
return null;
}
return null;
}
}
分布式锁实现
/**
* 自研redis分布式锁
*/
public class RedisDistributedLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
// 使用这个构造方法会导致每次都产生不同的uuid 删锁会报错
/*public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
this.expireTime = 25L;
}*/
// 这是从最顶层传过来的,没问题
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuid + ":" + Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L) {
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
// 加锁失败 自旋
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
//暂停60毫秒
try {
TimeUnit.MILLISECONDS.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2 1/3来实现续期
renewExpire();
return true;
}
return false;
}
// 后台续期的监视线程
private void renewExpire() {
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
renewExpire();
}
}
}, (this.expireTime * 1000) / 3);
}
@Override
public void unlock() {
System.out.println("unlock(): lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
// nil = false 1 = true 0 = false
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if (null == flag) {
throw new RuntimeException("this lock doesn't exists,o(╥﹏╥)o");
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
测试并使用
//V6.0版本,如何将我们的lock/unlock+lua脚本自研版的redis分布式锁搞定 满足可重入性
// 重入性就是拿到一把锁后 再次拿同一把锁 无需重新加锁 只需记录锁的次数
// 并且开启续期功能
// 使用hset数据结构
// 自研分布式锁工厂
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale() {
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:" + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号" + port);
testReEntry();
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
redisLock.unlock();
}
return retMessage + "\t" + "服务端口号" + port;
}
//用在V6.0版本程序作为测试可重入性
private void testReEntry() {
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
System.out.println("===========测试可重入锁========");
} finally {
redisLock.unlock();
}
}
V7.0,引入Redisson对应的官网推荐RedLock算法实现类
@Autowired
private Redisson redisson;
public String saleByRedisson() {
String retMessage = "";
RLock redissonLock = redisson.getLock("lazyRedisLock");
redissonLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存,每次减少一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:" + inventoryNumber;
System.out.println(retMessage + "\t" + "服务端口号" + port);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
//改进点,只能删除属于自己的key,不能删除别人的
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
return retMessage + "\t" + "服务端口号" + port;
}
最后是redis的简单配置
@Configuration
public class RedisConfig
{
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public Redisson redisson()
{
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
以上是锁的升级过程以及心得,但关于红锁算法,看门狗策略这些知识看到时评论如何,可以的话会再更一期,上面的代码可以复制为两个模块,然后通过nginx代理进行访问测试,模拟多个机器访问库存模块
参考链接:视频链接