分布式锁
概念
任何一个系统都无法同时满足一致性(Consistency),可用性(Availability),分区容错性(Partition tolerance), 只能同时满足2个;
分布式锁就是为了解决数据一致性问题.
悲观锁和乐观锁
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据都会上锁,这样别人想拿这个数据就会阻塞,知道锁被释放
悲观锁多用于多写场景,关系型数据库中很多使用了这种悲观锁机制
实现:
redis 实现锁机制
乐观锁
总是假设最好的情况,即每次去拿数据的时候都认为别的线程不会去修改,所以不会上锁,但是在更新数据的时候会判断在此期间有没有其它线程更新了这个数据,可以用版本号机制和CAS算法来实现;
乐观锁多用户多读场景,提高吞吐量,比如数据库提供的write_condition机制
实现:
数据库添加版本号字段: 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
CAS 算法
应用场景
涉及到多个实例进程操作同一份数据时就需要用到锁机制,比如: 下单,修改库存,更新缓存等
分布式锁的特性
分布式环境下同一时刻只能被单个线程获取;
已经获取锁的进程在使用中不需要再次获取;
异常或者超时自动释放锁,避免死锁
高性能,分布式环境下必须性能好;
实现方式
基于redis 缓存实现;
基于zookeeper 临时顺序节点实现;
基于数据库行锁实现;
redis 分布式锁
采用springboot + redis
redis 配置
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
lettuce:
pool:
# 最大活跃链接数 默认8
max-active: 100
# 最大空闲连接数 默认8
max-idle: 10
# 最小空闲连接数 默认0
min-idle: 5
timeout: 30000
分布式锁接口 DistributedLock
public interface DistributedLock {
String lock(String name);
/**
* 加锁,有阻塞
* @param name
* @param expire
* @param timeout
* @return
*/
String lock(String name, long expire, long timeout);
String tryLock(String name);
/**
* 加锁,无阻塞
* @param name
* @param expire
* @return
*/
String tryLock(String name, long expire);
/**
* 解锁
* @param name
* @param token
* @return
*/
boolean unlock(String name, String token);
void close();
}
分布式锁实现 RedisDistributedLockImpl
@Service
@Slf4j
public class RedisDistributedLockImpl implements DistributedLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private RedisLock redisLock;
@PostConstruct
public void init(){
redisLock = new RedisLock(stringRedisTemplate);
}
public int getCount() {
return redisLock.getCount();
}
@Override
public String lock(String name){
return redisLock.lock(name);
}
/**
* 加锁,有阻塞
* @param name
* @param expire
* @param timeout
* @return
*/
@Override
public String lock(String name, long expire, long timeout){
return redisLock.lock(name, expire, timeout);
}
@Override
public String tryLock(String name) {
return redisLock.tryLock(name);
}
/**
* 加锁,无阻塞
* @param name
* @param expire
* @return
*/
@Override
public String tryLock(String name, long expire) {
return redisLock.tryLock(name, expire);
}
/**
* 解锁
* @param name
* @param token
* @return
*/
@Override
public boolean unlock(String name, String token) {
return redisLock.unlock(name, token);
}
@Override
public void close() {
redisLock.close();
}
}
redis 锁具体逻辑 RedisLock
@Slf4j
public class RedisLock {
/**
* 解锁脚本,原子操作
*/
private static final String unlockScript =
"if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
+ "then\n"
+ " return redis.call(\"del\",KEYS[1])\n"
+ "else\n"
+ " return 0\n"
+ "end";
private StringRedisTemplate redisTemplate;
private long timeout = 60000;
private long expire = 300000;
private String PREFIX = "lock:";
private int count = 0;
public RedisLock(StringRedisTemplate redisTemplate) {
init(redisTemplate, this.expire, this.timeout);
}
public RedisLock(StringRedisTemplate redisTemplate, long expire, long timeout) {
init(redisTemplate, expire, timeout);
}
public void init(StringRedisTemplate redisTemplate, long expire, long timeout) {
this.redisTemplate = redisTemplate;
this.expire = expire;
this.timeout = timeout;
}
public int getCount() {
return count;
}
public String lock(String name){
return this.lock(name, this.expire, this.timeout);
}
/**
* 加锁,有阻塞
* @param name
* @param expire
* @param timeout
* @return
*/
public String lock(String name, long expire, long timeout){
long startTime = System.currentTimeMillis();
String token;
do{
token = tryLock(name, expire);
log.debug("lock token:{}", token);
if(token == null) {
if((System.currentTimeMillis()-startTime) > (timeout-10))
return token;
try {
Thread.sleep(500); //try 10 per millis
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}while(token==null);
return token;
}
public String tryLock(String name) {
return this.tryLock(name, this.expire);
}
/**
* 加锁,无阻塞
* @param name
* @param expire
* @return
*/
public String tryLock(String name, long expire) {
String token = UUID.randomUUID().toString();
String key = PREFIX + name;
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
try{
Boolean result = conn.set(key.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),
Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
if(result!=null && result) {
count++;
return token;
}
} catch (Exception e){
log.error("fail to tryLock name:{}", name);
e.printStackTrace();
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
return null;
}
/**
* 解锁
* @param name
* @param token
* @return
*/
public boolean unlock(String name, String token) {
String key = PREFIX + name;
byte[][] keysAndArgs = new byte[2][];
keysAndArgs[0] = key.getBytes(Charset.forName("UTF-8"));
keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
try {
Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
if(result!=null && result>0) {
count--;
return true;
}
} catch (Exception e){
log.error("fail to unlock name:{}, token:{}", key, token);
e.printStackTrace();
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
return false;
}
public void close()
{
log.info("close connect");
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
redis 分布式锁测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SosApplication.class)
@Slf4j
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DistributedLock lock;
private Executor executor = Executors.newFixedThreadPool(100);
private int concurrenceResult = 0;
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void empty() {
}
@Test
public void deleteRedisOrders() {
while (doDelete("order::*", 5000) == 5000) {
log.info("another loop...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private int doDelete(String match, int n) {
AtomicInteger res = new AtomicInteger();
redisTemplate.execute((RedisCallback<?>) (connection) -> {
int i = 0;
Cursor<byte[]> cursors = null;
log.info("start delete ...");
try {
cursors = connection.scan(ScanOptions.scanOptions().match(match).count(n).build());
while (cursors.hasNext()) {
byte[] key = cursors.next();
connection.del(key);
log.info("delete ---> {}", new String(key));
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
log.info("total deleted {}", i);
res.set(i);
if (cursors != null) {
try {
cursors.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
});
return res.get();
}
@Test
public void testLock() {
String token = lock.lock("test");
log.debug("token:{}", token);
Assert.notNull(token, "lock fail");
boolean rc = lock.unlock("test", token);
Assert.isTrue(rc, "unlock fail");
}
@Test
public void testLockWithConcurrence() {
concurrenceResult = 0;
int threadNum= 30;
int loopNum = 100;
int result = threadNum * loopNum;
CountDownLatch latch = new CountDownLatch(threadNum);
log.info("testThreadLock...");
for(int i=0; i< threadNum; i++) {
executor.execute(() -> {
doWorkWithLock(loopNum, latch);
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testThreadLock result:{}", concurrenceResult);
Assert.isTrue(concurrenceResult==result, "testLockWithConcurrence result should be " + result);
}
private void doWork(int n, CountDownLatch latch){
try {
Random rand = new Random();
for(int i=0; i<n; i++) {
long randNum = rand.nextInt(20);
log.debug("doWork sleep:{}, thread:{}", randNum, Thread.currentThread().getName());
concurrenceResult++;
Thread.sleep(randNum);
}
if(latch != null) {
latch.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doWorkWithLock(int n, CountDownLatch latch){
String name = "lockName";
String token = lock.lock(name);
long s = new Date().getTime();
log.info("doWorkWithLock lock name:{}, token:{}, thread:{}", name, token, Thread.currentThread().getName());
doWork(n, latch);
boolean rc = lock.unlock(name, token);
long e = new Date().getTime();
log.info("doWorkWithLock unlock name:{}, token:{}, rc:{}, times:{}, thread:{}", name, token, rc, e-s, Thread.currentThread().getName());
}
}
使用案例
//使用分布式锁获取log,防止多进程运行时重复获取
String token = lock.tryLock(LOCK_NAME);
if (token != null) {
try {
// 具体业务
} finally {
lock.unlock(LOCK_NAME, token);
}
}
本文分享自微信公众号 - 码小山(gh_d9b3dc861714)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/2897815/blog/4900798