1.分布式锁介绍

  在计算机系统中,锁作为一种控制并发的机制无处不在。

  单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为。而在如今的大型复杂系统中,通常采用的是分布式架构提供服务。

  分布式环境下,基于本地单机的锁无法控制分布式系统中分开部署客户端的并发行为,此时分布式锁就应运而生了。

一个可靠的分布式锁应该具备以下特性:

  1.互斥性:作为锁,需要保证任何时刻只能有一个客户端(用户)持有锁

  2.可重入: 同一个客户端在获得锁后,可以再次进行加锁

  3.高可用:获取锁和释放锁的效率较高,不会出现单点故障

  4.自动重试机制:当客户端加锁失败时,能够提供一种机制让客户端自动重试

2.分布式锁api接口

/**
* 分布式锁 api接口
*/
public interface DistributeLock { /**
* 尝试加锁
* @param lockKey 锁的key
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lock(String lockKey); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param expireTime 过期时间 单位:秒
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lock(String lockKey, int expireTime); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param requestID 用户ID
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lock(String lockKey, String requestID); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param requestID 用户ID
* @param expireTime 过期时间 单位:秒
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lock(String lockKey, String requestID, int expireTime); /**
* 尝试加锁,失败自动重试 会阻塞当前线程
* @param lockKey 锁的key
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lockAndRetry(String lockKey); /**
* 尝试加锁,失败自动重试 会阻塞当前线程 (requestID相等 可重入)
* @param lockKey 锁的key
* @param requestID 用户ID
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lockAndRetry(String lockKey, String requestID); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param expireTime 过期时间 单位:秒
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lockAndRetry(String lockKey, int expireTime); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param expireTime 过期时间 单位:秒
* @param retryCount 重试次数
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lockAndRetry(String lockKey, int expireTime, int retryCount); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param requestID 用户ID
* @param expireTime 过期时间 单位:秒
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lockAndRetry(String lockKey, String requestID, int expireTime); /**
* 尝试加锁 (requestID相等 可重入)
* @param lockKey 锁的key
* @param expireTime 过期时间 单位:秒
* @param requestID 用户ID
* @param retryCount 重试次数
* @return 加锁成功 返回uuid
* 加锁失败 返回null
* */
String lockAndRetry(String lockKey, String requestID, int expireTime, int retryCount); /**
* 释放锁
* @param lockKey 锁的key
* @param requestID 用户ID
* @return true 释放自己所持有的锁 成功
* false 释放自己所持有的锁 失败
* */
boolean unLock(String lockKey, String requestID);
}

3.基于redis的分布式锁的简单实现

3.1 基础代码

  当前实现版本的分布式锁基于redis实现,使用的是jedis连接池来和redis进行交互,并将其封装为redisClient工具类(仅封装了demo所需的少数接口)

redisClient工具类:

public class RedisClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisClient.class);

    private JedisPool pool;

    private static RedisClient instance = new RedisClient();

    private RedisClient() {
init();
} public static RedisClient getInstance(){
return instance;
} public Object eval(String script, List<String> keys, List<String> args) {
Jedis jedis = getJedis();
Object result = jedis.eval(script, keys, args);
jedis.close();
return result;
} public String get(final String key){
Jedis jedis = getJedis();
String result = jedis.get(key);
jedis.close();
return result;
} public String set(final String key, final String value, final String nxxx, final String expx, final int time) {
Jedis jedis = getJedis();
String result = jedis.set(key, value, nxxx, expx, time);
jedis.close();
return result;
} private void init(){
Properties redisConfig = PropsUtil.loadProps("redis.properties");
int maxTotal = PropsUtil.getInt(redisConfig,"maxTotal",10);
String ip = PropsUtil.getString(redisConfig,"ip","127.0.0.1");
int port = PropsUtil.getInt(redisConfig,"port",6379); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
pool = new JedisPool(jedisPoolConfig, ip,port);
LOGGER.info("连接池初始化成功 ip={}, port={}, maxTotal={}",ip,port,maxTotal);
} private Jedis getJedis(){
return pool.getResource();
}
}

所依赖的工具类:

package util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties; /**
* @Author xiongyx
* @Create 2018/4/11.
*/
public final class PropsUtil { private static final Logger LOGGER = LoggerFactory.getLogger(PropsUtil.class); /**
* 读取配置文件
* */
public static Properties loadProps(String fileName){
Properties props = null;
InputStream is = null;
try{
//:::绝对路径获得输入流
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);
if(is == null){
//:::没找到文件,抛出异常
throw new FileNotFoundException(fileName + " is not found");
}
props = new Properties();
props.load(is);
}catch(IOException e){
LOGGER.error("load propertis file fail",e);
}finally {
if(is != null){
try{
//:::关闭io流
is.close();
} catch (IOException e) {
LOGGER.error("close input Stream fail",e);
}
}
} return props;
} /**
* 获取字符串属性(默认为空字符串)
* */
public static String getString(Properties properties,String key){
//:::调用重载函数 默认值为:空字符串
return getString(properties,key,"");
} /**
* 获取字符串属性
* */
public static String getString(Properties properties,String key,String defaultValue){
//:::key对应的value数据是否存在
if(properties.containsKey(key)){
return properties.getProperty(key);
}else{
return defaultValue;
}
} /**
* 获取int属性 默认值为0
* */
public static int getInt(Properties properties,String key){
//:::调用重载函数,默认为:0
return getInt(properties,key,0);
} /**
* 获取int属性
* */
public static int getInt(Properties properties,String key,int defaultValue){
//:::key对应的value数据是否存在
if(properties.containsKey(key)){
return CastUtil.castToInt(properties.getProperty(key));
}else{
return defaultValue;
}
} /**
* 获取boolean属性,默认值为false
*/
public static boolean getBoolean(Properties properties,String key){
return getBoolean(properties,key,false);
} /**
* 获取boolean属性
*/
public static boolean getBoolean(Properties properties,String key,boolean defaultValue){
//:::key对应的value数据是否存在
if(properties.containsKey(key)){
return CastUtil.castToBoolean(properties.getProperty(key));
}else{
return defaultValue;
}
}
} public final class CastUtil { /**
* 转为 string
* */
public static String castToString(Object obj){
return castToString(obj,"");
} /**
* 转为 string 提供默认值
* */
public static String castToString(Object obj,String defaultValue){
if(obj == null){
return defaultValue;
}else{
return obj.toString();
}
} /**
* 转为 int
* */
public static int castToInt(Object obj){
return castToInt(obj,0);
} /**
* 转为 int 提供默认值
* */
public static int castToInt(Object obj,int defaultValue){
if(obj == null){
return defaultValue;
}else{
return Integer.parseInt(obj.toString());
}
} /**
* 转为 double
* */
public static double castToDouble(Object obj){
return castToDouble(obj,0);
} /**
* 转为 double 提供默认值
* */
public static double castToDouble(Object obj,double defaultValue){
if(obj == null){
return defaultValue;
}else{
return Double.parseDouble(obj.toString());
}
} /**
* 转为 long
* */
public static long castToLong(Object obj){
return castToLong(obj,0);
} /**
* 转为 long 提供默认值
* */
public static long castToLong(Object obj,long defaultValue){
if(obj == null){
return defaultValue;
}else{
return Long.parseLong(obj.toString());
}
} /**
* 转为 boolean
* */
public static boolean castToBoolean(Object obj){
return castToBoolean(obj,false);
} /**
* 转为 boolean 提供默认值
* */
public static boolean castToBoolean(Object obj,boolean defaultValue){
if(obj == null){
return defaultValue;
}else{
return Boolean.parseBoolean(obj.toString());
}
}
}

初始化lua脚本 LuaScript.java:

  在分布式锁初始化时,使用init方法读取lua脚本

public class LuaScript {
/**
* 加锁脚本 lock.lua
* */
public static String LOCK_SCRIPT = ""; /**
* 解锁脚本 unlock.lua
* */
public static String UN_LOCK_SCRIPT = ""; public static void init(){
try {
initLockScript();
initUnLockScript();
} catch (IOException e) {
throw new RuntimeException(e);
}
} private static void initLockScript() throws IOException {
String filePath = Objects.requireNonNull(LuaScript.class.getClassLoader().getResource("lock.lua")).getPath();
LOCK_SCRIPT = readFile(filePath);
} private static void initUnLockScript() throws IOException {
String filePath = Objects.requireNonNull(LuaScript.class.getClassLoader().getResource("unlock.lua")).getPath();
UN_LOCK_SCRIPT = readFile(filePath);
} private static String readFile(String filePath) throws IOException {
try (
FileReader reader = new FileReader(filePath);
BufferedReader br = new BufferedReader(reader)
) {
String line;
StringBuilder stringBuilder = new StringBuilder();
while ((line = br.readLine()) != null) {
stringBuilder.append(line).append(System.lineSeparator());
} return stringBuilder.toString();
}
}
}

单例的RedisDistributeLock基础属性

public final class RedisDistributeLock implements DistributeLock {

    /**
* 无限重试
* */
public static final int UN_LIMIT_RETRY = -1; private RedisDistributeLock() {
LuaScript.init();
} private static DistributeLock instance = new RedisDistributeLock(); /**
* 持有锁 成功标识
* */
private static final Long ADD_LOCK_SUCCESS = 1L;
/**
* 释放锁 失败标识
* */
private static final Integer RELEASE_LOCK_SUCCESS = 1; /**
* 默认过期时间 单位:秒
* */
private static final int DEFAULT_EXPIRE_TIME_SECOND = 300;
/**
* 默认加锁重试时间 单位:毫秒
* */
private static final int DEFAULT_RETRY_FIXED_TIME = 3000;
/**
* 默认的加锁浮动时间区间 单位:毫秒
* */
private static final int DEFAULT_RETRY_TIME_RANGE = 1000;
/**
* 默认的加锁重试次数
* */
private static final int DEFAULT_RETRY_COUNT = 30; /**
* lockCount Key前缀
* */
private static final String LOCK_COUNT_KEY_PREFIX = "lock_count:"; public static DistributeLock getInstance(){
return instance;
}
}

3.2 加锁实现

  使用redis实现分布式锁时,加锁操作必须是原子操作,否则多客户端并发操作时会导致各种各样的问题。详情请见:Redis分布式锁的正确实现方式

  由于我们实现的是可重入锁,加锁过程中需要判断客户端ID的正确与否。而redis原生的简单接口没法保证一系列逻辑的原子性执行,因此采用了lua脚本来实现加锁操作。lua脚本可以让redis在执行时将一连串的操作以原子化的方式执行。

加锁lua脚本 lock.lua

-- 获取参数
local requestIDKey = KEYS[] local currentRequestID = ARGV[]
local expireTimeTTL = ARGV[] -- setnx 尝试加锁
local lockSet = redis.call('hsetnx',KEYS[],'lockKey',currentRequestID) if lockSet ==
then
-- 加锁成功 设置过期时间和重入次数=1
redis.call('expire',KEYS[],expireTimeTTL)
redis.call('hset',KEYS[],'lockCount',)
return
else
-- 判断是否是重入加锁
local oldRequestID = redis.call('hget',KEYS[],'lockKey')
if currentRequestID == oldRequestID
then
-- 是重入加锁
redis.call('hincrby',KEYS[],'lockCount',)
-- 重置过期时间
redis.call('expire',KEYS[],expireTimeTTL)
return
else
-- requestID不一致,加锁失败
return
end
end

加锁方法实现:

  加锁时,通过判断eval的返回值来判断加锁是否成功。

   @Override
public String lock(String lockKey) {
String uuid = UUID.randomUUID().toString(); return lock(lockKey,uuid);
} @Override
public String lock(String lockKey, int expireTime) {
String uuid = UUID.randomUUID().toString(); return lock(lockKey,uuid,expireTime);
} @Override
public String lock(String lockKey, String requestID) {
return lock(lockKey,requestID,DEFAULT_EXPIRE_TIME_SECOND);
} @Override
public String lock(String lockKey, String requestID, int expireTime) {
RedisClient redisClient = RedisClient.getInstance(); List<String> keyList = Arrays.asList(
lockKey
); List<String> argsList = Arrays.asList(
requestID,
expireTime + ""
);
Long result = (Long)redisClient.eval(LuaScript.LOCK_SCRIPT, keyList, argsList); if(result.equals(ADD_LOCK_SUCCESS)){
return requestID;
}else{
return null;
}
}

3.3 解锁实现

  解锁操作同样需要一连串的操作,由于原子化操作的需求,因此同样使用lua脚本实现解锁功能。

解锁lua脚本 unlock.lua

-- 获取参数
local requestIDKey = KEYS[] local currentRequestID = ARGV[] -- 判断requestID一致性
if redis.call('hget',KEYS[],'lockKey') == currentRequestID
then
-- requestID相同,重入次数自减
local currentCount = redis.call('hincrby',KEYS[],'lockCount',-)
if currentCount ==
then
-- 重入次数为0,删除锁
redis.call('del',KEYS[])
return
else
return end
else
return end

解锁方法实现:

   @Override
public boolean unLock(String lockKey, String requestID) {
List<String> keyList = Arrays.asList(
lockKey
); List<String> argsList = Collections.singletonList(requestID); Object result = RedisClient.getInstance().eval(LuaScript.UN_LOCK_SCRIPT, keyList, argsList); // 释放锁成功
return RELEASE_LOCK_SUCCESS.equals(result);
}

3.4 自动重试机制实现

  调用lockAndRetry方法进行加锁时,如果加锁失败,则当前客户端线程会短暂的休眠一段时间,并进行重试。在重试了一定的次数后,会终止重试加锁操作,从而加锁失败。

  需要注意的是,加锁失败之后的线程休眠时长是"固定值 + 随机值",引入随机值的主要目的是防止高并发时大量的客户端在几乎同一时间被唤醒并进行加锁重试,给redis服务器带来周期性的、不必要的瞬时压力。

    @Override
public String lockAndRetry(String lockKey) {
String uuid = UUID.randomUUID().toString(); return lockAndRetry(lockKey,uuid);
} @Override
public String lockAndRetry(String lockKey, String requestID) {
return lockAndRetry(lockKey,requestID,DEFAULT_EXPIRE_TIME_SECOND);
} @Override
public String lockAndRetry(String lockKey, int expireTime) {
String uuid = UUID.randomUUID().toString(); return lockAndRetry(lockKey,uuid,expireTime);
} @Override
public String lockAndRetry(String lockKey, int expireTime, int retryCount) {
String uuid = UUID.randomUUID().toString(); return lockAndRetry(lockKey,uuid,expireTime,retryCount);
} @Override
public String lockAndRetry(String lockKey, String requestID, int expireTime) {
return lockAndRetry(lockKey,requestID,expireTime,DEFAULT_RETRY_COUNT);
} @Override
public String lockAndRetry(String lockKey, String requestID, int expireTime, int retryCount) {
if(retryCount <= 0){
// retryCount小于等于0 无限循环,一直尝试加锁
while(true){
String result = lock(lockKey,requestID,expireTime);
if(result != null){
return result;
} // 休眠一会
sleepSomeTime();
}
}else{
// retryCount大于0 尝试指定次数后,退出
for(int i=0; i<retryCount; i++){
String result = lock(lockKey,requestID,expireTime);
if(result != null){
return result;
} // 休眠一会
sleepSomeTime();
} return null;
}
}

4.使用注解切面简化redis分布式锁的使用

  通过在方法上引入RedisLock注解切面,让对应方法被redis分布式锁管理起来,可以简化redis分布式锁的使用。

切面注解 RedisLock 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* 无限重试
* */
int UN_LIMIT_RETRY = RedisDistributeLock.UN_LIMIT_RETRY; String lockKey();
int expireTime();
int retryCount();
}

RedisLock 切面实现

@Component
@Aspect
public class RedisLockAspect { private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockAspect.class); private static final ThreadLocal<String> REQUEST_ID_MAP = new ThreadLocal<>(); @Pointcut("@annotation(annotation.RedisLock)")
public void annotationPointcut() {
} @Around("annotationPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
RedisLock annotation = method.getAnnotation(RedisLock.class); boolean lockSuccess = lock(annotation);
if(lockSuccess){
Object result = joinPoint.proceed();
unlock(annotation);
return result;
}
return null;
} /**
* 加锁
* */
private boolean lock(RedisLock annotation){
DistributeLock distributeLock = RedisDistributeLock.getInstance(); int retryCount = annotation.retryCount(); String requestID = REQUEST_ID_MAP.get();
if(requestID != null){
// 当前线程 已经存在requestID
distributeLock.lockAndRetry(annotation.lockKey(),requestID,annotation.expireTime(),retryCount);
LOGGER.info("重入加锁成功 requestID=" + requestID); return true;
}else{
// 当前线程 不存在requestID
String newRequestID = distributeLock.lockAndRetry(annotation.lockKey(),annotation.expireTime(),retryCount); if(newRequestID != null){
// 加锁成功,设置新的requestID
REQUEST_ID_MAP.set(newRequestID);
LOGGER.info("加锁成功 newRequestID=" + newRequestID); return true;
}else{
LOGGER.info("加锁失败,超过重试次数,直接返回 retryCount={}",retryCount); return false;
}
}
} /**
* 解锁
* */
private void unlock(RedisLock annotation){
DistributeLock distributeLock = RedisDistributeLock.getInstance();
String requestID = REQUEST_ID_MAP.get();
if(requestID != null){
// 解锁成功
boolean unLockSuccess = distributeLock.unLock(annotation.lockKey(),requestID);
if(unLockSuccess){
// 移除 ThreadLocal中的数据
REQUEST_ID_MAP.remove();
LOGGER.info("解锁成功 requestID=" + requestID);
}
}
}
}

使用例子

@Service("testService")
public class TestServiceImpl implements TestService { @Override
@RedisLock(lockKey = "lockKey", expireTime = 100, retryCount = RedisLock.UN_LIMIT_RETRY)
public String method1() {
return "method1";
} @Override
@RedisLock(lockKey = "lockKey", expireTime = 100, retryCount = 3)
public String method2() {
return "method2";
}
}

5.总结

5.1 当前版本缺陷

主从同步可能导致锁的互斥性失效

  在redis主从结构下,出于性能的考虑,redis采用的是主从异步复制的策略,这会导致短时间内主库和从库数据短暂的不一致。

  试想,当某一客户端刚刚加锁完毕,redis主库还没有来得及和从库同步就挂了,之后从库中新选拔出的主库是没有对应锁记录的,这就可能导致多个客户端加锁成功,破坏了锁的互斥性。

休眠并反复尝试加锁效率较低

  lockAndRetry方法在客户端线程加锁失败后,会休眠一段时间之后再进行重试。当锁的持有者持有锁的时间很长时,其它客户端会有大量无效的重试操作,造成系统资源的浪费。

  进一步优化时,可以使用发布订阅的方式。这时加锁失败的客户端会监听锁被释放的信号,在锁真正被释放时才会进行新的加锁操作,从而避免不必要的轮询操作,以提高效率。

不是一个公平的锁

  当前实现版本中,多个客户端同时对锁进行抢占时,是完全随机的,既不遵循先来后到的顺序,客户端之间也没有加锁的优先级区别。

  后续优化时可以提供一个创建公平锁的接口,能指定加锁的优先级,内部使用一个优先级队列维护加锁客户端的顺序。公平锁虽然效率稍低,但在一些场景能更好的控制并发行为。

5.2 经验总结

  前段时间看了一篇关于redis分布式锁的技术文章,发现自己对于分布式锁的了解还很有限。纸上得来终觉浅,为了更好的掌握相关知识,决定尝试着自己实现一个demo级别的redis分布式锁,通过这次实践,更进一步的学习了lua语言和redis相关内容。

  这篇博客的完整代码在我的github上:https://github.com/1399852153/RedisDistributedLock,存在许多不足之处,请多多指教。

基于redis的分布式锁实现的更多相关文章

  1. 基于redis 实现分布式锁的方案

    在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...

  2. 基于redis的分布式锁

    <?php /** * 基于redis的分布式锁 * * 参考开源代码: * http://nleach.com/post/31299575840/redis-mutex-in-php * * ...

  3. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  4. 基于 Redis 的分布式锁

    前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...

  5. 基于redis的分布式锁(转)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  6. 基于redis的分布式锁(不适合用于生产环境)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  7. 基于 redis 的分布式锁实现 Distributed locks with Redis debug 排查错误

    小结: 1. 锁的实现方式,按照应用的实现架构,可能会有以下几种类型: 如果处理程序是单进程多线程的,在 python下,就可以使用 threading 模块的 Lock 对象来限制对共享变量的同步访 ...

  8. 转载:基于Redis实现分布式锁

    转载:基于Redis实现分布式锁  ,出处: http://blog.csdn.net/ugg/article/details/41894947 背景在很多互联网产品应用中,有些场景需要加锁处理,比如 ...

  9. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

随机推荐

  1. HTML表单标签

    <form>标签 1.在HTML中,<form></form>标记对用来创建一个表单,即定义表单的开始和结束位置,在标记对之间的一切都属于表单的内容.每个表单元素开 ...

  2. temp--贵州银行

    -------住宿----泊乐酒店----8905----与朱聿一起住 2018年  1月3日晚 1月4日晚  1月5日晚 1月6日晚  1月7日晚 1月8日晚  1月9日晚 已结清! ======= ...

  3. Springfox与swagger的整合使用

    一.前言 让我们先理一下springfox与swagger的关系. swagger是一个流行的API开发框架,这个框架以“开放API声明”(OpenAPI Specification,OAS)为基础, ...

  4. You just run!

    第一篇博客,无关技术,有关身体. 写一篇跑步干货 装备篇 用过的鞋: 光脚,拖鞋,人字拖,回力板鞋,皮鞋,特步,鸿星尔克,李宁超轻13,ASICS  gt2000,阿迪低端. 1,非常推荐攒钱买一双a ...

  5. 清除Linux日志文件命令

    find /opt/tomcat/logs/catalina_* -mtime +9 -exec rm -rf {} \;

  6. 6 week work 2

    CSS颜色表示法和颜色表(调色板) 1.用颜色名表示 如:white.red.greenyellow.gold等. 2.用十六进制的颜色值表示(红.绿.蓝) #FF0000或者#F00 3.用rgb( ...

  7. git无法同步

    出现问题: fatal: destination path 'test' already exists and is not an empty directory. 解决方法如下: git init ...

  8. FPGA跨时钟域握手信号的结构

    FPGA跨时钟数据传输,是我们经常遇到的问题的,下面给出一种跨时钟握手操作的电路结构.先上图 先对与其他人的结构,这个结构最大的特点是使用 req 从低到高或者高到低的变化 来表示DIN数据有效并开始 ...

  9. 理解 Linux 的虚拟内存

    前言 前不久组内又有一次我比较期待的分享:”Linux 的虚拟内存”.是某天晚上加班时,我们讨论虚拟内存的概念时,leader 发现几位同事对虚拟内存认识不清后,特意给这位同学挑选的主题(笑). 我之 ...

  10. 脑残式网络编程入门(三):HTTP协议必知必会的一些知识

    本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.前言 无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交 ...