分布式锁机制
在分布式项目中我们在处理线程安全问题时,通过加锁是行不通的,因为我们程序跑在多台机器上,每台机器拿到的是不同的锁对象,因此加锁无法避免线程安全问题,因此需要引入分布式锁的解决方案
引入分布式锁案例
// 库存扣减
package com.zhj.bean;
/**
* @author zhj
*/
public class Stock {
// 库存
private static int num = 1;
// 减少库存
public boolean reduceStock() {
if (num > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
return true;
} else {
return false;
}
}
}
测试方法
package com.zhj.test;
import com.zhj.bean.Stock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zhj
*/
public class stockMain {
private static Lock lock = new ReentrantLock();
static class StockTask implements Runnable {
@Override
public void run() {
lock.lock();
try {
boolean b = new Stock().reduceStock();
if (b) {
System.out.println(Thread.currentThread().getName() + " 减少库存成功");
} else {
System.out.println(Thread.currentThread().getName() + " 减少库存失败");
}
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new StockTask(),"线程" + i).start();
}
}
}
1. 使用数据库解决分布式锁问题
创建数据表
CREATE TABLE `lock_db`.`lock_record` (
`id` int NOT NULL,
`lock_name` varchar(255) NOT NULL COMMENT '唯一锁名称',
PRIMARY KEY (`id`),
UNIQUE INDEX `unique_lock_name`(`lock_name`) COMMENT '唯一索引'
);
通过数据库添加一条唯一数据进行加锁操作,解锁是删除该条数据
package com.zhj.lock;
import com.zhj.bean.LockRecord;
import com.zhj.mapper.LockRecordMapper;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author zhj
*/
@Component
public class DBLock implements Lock {
private static final String LOCK_NAME = "db_lock_stock";
@Resource
private LockRecordMapper lockRecordMapper;
@Override
public void lock() {
while (true) {
boolean flag = tryLock();
if (flag) {
try {
LockRecord lockRecord = new LockRecord();
lockRecord.setLockName(LOCK_NAME);
lockRecordMapper.insert(lockRecord);
return;
} catch (Exception e) {
System.out.println("-------");
}
} else {
System.out.println("等待中...");
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
* 尝试获取锁,根据指定的名称在数据表发起一次查询
* @return
*/
@Override
public boolean tryLock() {
Example example = new Example(LockRecord.class);
example.createCriteria().andEqualTo("lockName", LOCK_NAME);
LockRecord lockRecord = lockRecordMapper.selectOneByExample(example);
if (lockRecord == null) {
return true;
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
Example example = new Example(LockRecord.class);
example.createCriteria().andEqualTo("lockName", LOCK_NAME);
lockRecordMapper.deleteByExample(example);
}
@Override
public Condition newCondition() {
return null;
}
}
2. 使用Redis解决分布式锁问题
通过设置过期时间避免死锁出现
package com.zhj.lock;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author zhj
*/
@Component
public class RedisLock implements Lock {
private static final String LOCK_NAME = "redis_lock_stock";
@Resource
private RedisTemplate redisTemplate;
@Override
public void lock() {
while (true) {
// unlock 未执行会出现死锁,我们可以通过设置过期时间防止死锁
// Boolean flag = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME);
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME,15,TimeUnit.SECONDS);
if (flag) {
return;
} else {
System.out.println("等待获取redis锁");
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
redisTemplate.delete("lockName");
}
@Override
public Condition newCondition() {
return null;
}
}
使用官方推荐redisson实现
为解决单点问题,我们常使用redis集群,那么使用redis时可能出现主节点突然挂掉的事件,没来得及同步数据,这样就会两个客户端同时拥有,出现超卖现象。redis官方推迟Redlock算法解决分布式问题的redisson
1.导入对应依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
2.使用
package com.zhj.test;
import com.zhj.bean.Stock;
import com.zhj.lock.RedisLock;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.annotation.Resource;
/**
* @author zhj
*/
public class stockMain {
// private static Lock lock = new ReentrantLock();
// 分布式锁
@Resource
private static RedisLock redisLock;
// private static DBLock dbLock;
private static RLock rLock;
static {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// dbLock = classPathXmlApplicationContext.getBean(DBLock.class);
redisLock = classPathXmlApplicationContext.getBean(RedisLock.class);
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1);
Redisson redisson = (Redisson) Redisson.create(config);
rLock = redisson.getLock("redis_lock_stock");
}
static class StockTask implements Runnable {
@Override
public void run() {
// dbLock.lock();
// redisLock.lock();
rLock.lock();
boolean b = new Stock().reduceStock();
// dbLock.unlock();
// redisLock.unlock();
rLock.unlock();
if (b) {
System.out.println(Thread.currentThread().getName() + " 减少库存成功");
} else {
System.out.println(Thread.currentThread().getName() + " 减少库存失败");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new StockTask(),"线程" + i).start();
}
}
}
3. 使用zookeeper实现分布式锁
为每一个线程创建一个有序的临时节点,为了确保有序性,在创建完节点,会再回去全部节点,再重新进行一次排序,排序过程中,每个线程要判断自己剩下的临时节点的序号是否是最小。如果是最新,将会获得锁,执行相关操作,释放锁。如果不是最小的,会监听它前的一个节点,当前一个节点被删除时,它会获得锁,依次类推
package com.zhj.lock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author zhj
*/
public class ZKLock implements Lock {
// 客户端
private ZooKeeper zk;
// zk 是一个目录结构,locks
private String root = "/locks";
// 锁的名称
private String lockName;
// 当前线程创建的序列node
private ThreadLocal<String> nodeId = new ThreadLocal<>();
// 用来同步等待zkClient链接到了服务端
private CountDownLatch connectedSignal = new CountDownLatch(1);
private final static int sessionTimeout = 3000;
private final static byte[] data = new byte[0];
public ZKLock(String config, String lockName) {
this.lockName = lockName;
try {
zk = new ZooKeeper(config, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 建立连接
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
}
});
connectedSignal.await();
Stat stat = zk.exists(root, false);
if (null == stat) {
// 创建根节点
zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 监听临时节点的删除行为
class LockWatcher implements Watcher {
private CountDownLatch latch = null;
public LockWatcher(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown();
}
}
}
@Override
public void lock() {
try {
// 创建临时子节点
String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName()+myNode+ "created");
// 取出所有子节点
List<String> subNodes = zk.getChildren(root, false);
TreeSet<String> sortedNodes = new TreeSet<>();
for(String node :subNodes) {
sortedNodes.add(root +"/" +node);
}
String smallNode = sortedNodes.first();
if (myNode.equals(smallNode)) {
// 如果是最小的节点,则表示取得锁
System.out.println(Thread.currentThread().getName()+ myNode+"get lock");
this.nodeId.set(myNode);
return;
}
String preNode = sortedNodes.lower(myNode);
CountDownLatch latch = new CountDownLatch(1);
Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
// 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
if (stat != null) {
System.out.println(Thread.currentThread().getName()+myNode+
" waiting for " + root + "/" + preNode + " released lock");
latch.await();// 等待,这里应该一直等待其他线程释放锁
nodeId.set(myNode);
latch = null;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
try {
System.out.println(Thread.currentThread().getName()+ "unlock ");
if (null != nodeId) {
zk.delete(nodeId.get(), -1);
}
nodeId.remove();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public Condition newCondition() {
return null;
}
}
分布式队列
队列特性:先进先出